feat: add http monitor which include cert exp date

This commit is contained in:
moonrailgun 2023-10-23 21:39:24 +08:00
parent 594f3124ef
commit 31ba529022
15 changed files with 568 additions and 59 deletions

View File

@ -75,6 +75,7 @@
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vite-express": "^0.10.0", "vite-express": "^0.10.0",
"winston": "^3.11.0",
"yup": "^1.2.0", "yup": "^1.2.0",
"zod": "^3.22.2", "zod": "^3.22.2",
"zustand": "^4.4.1" "zustand": "^4.4.1"

122
pnpm-lock.yaml generated
View File

@ -166,6 +166,9 @@ dependencies:
vite-express: vite-express:
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0 version: 0.10.0
winston:
specifier: ^3.11.0
version: 3.11.0
yup: yup:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
@ -1361,6 +1364,11 @@ packages:
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
dev: true dev: true
/@colors/colors@1.6.0:
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
dev: false
/@cspotcode/source-map-support@0.8.1: /@cspotcode/source-map-support@0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -1372,6 +1380,14 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: false dev: false
/@dabh/diagnostics@2.0.3:
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
dependencies:
colorspace: 1.1.4
enabled: 2.0.0
kuler: 2.0.0
dev: false
/@dagrejs/graphlib@2.1.4: /@dagrejs/graphlib@2.1.4:
resolution: {integrity: sha512-QCg9sL4uhjn468FDEsb/S9hS2xUZSrv/+dApb1Ze5VKO96pTXKNJZ6MGhIpgWkc1TVhbVGH9/7rq/Mf8/jWicw==} resolution: {integrity: sha512-QCg9sL4uhjn468FDEsb/S9hS2xUZSrv/+dApb1Ze5VKO96pTXKNJZ6MGhIpgWkc1TVhbVGH9/7rq/Mf8/jWicw==}
dependencies: dependencies:
@ -2204,6 +2220,10 @@ packages:
minipass: 4.2.8 minipass: 4.2.8
dev: true dev: true
/@types/triple-beam@1.3.4:
resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==}
dev: false
/@types/uuid@9.0.3: /@types/uuid@9.0.3:
resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==} resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==}
dev: false dev: false
@ -2805,6 +2825,13 @@ packages:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
dev: false dev: false
/colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
dependencies:
color: 3.2.1
text-hex: 1.0.0
dev: false
/combined-stream@1.0.8: /combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -3295,6 +3322,10 @@ packages:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false dev: false
/enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
dev: false
/encodeurl@1.0.2: /encodeurl@1.0.2:
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -3676,6 +3707,10 @@ packages:
uglify-js: 2.8.29 uglify-js: 2.8.29
dev: false dev: false
/fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
dev: false
/follow-redirects@1.15.2: /follow-redirects@1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'} engines: {node: '>=4.0'}
@ -4196,6 +4231,11 @@ packages:
call-bind: 1.0.2 call-bind: 1.0.2
dev: false dev: false
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: false
/is-string@1.0.7: /is-string@1.0.7:
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -4339,6 +4379,10 @@ packages:
is-buffer: 1.1.6 is-buffer: 1.1.6
dev: false dev: false
/kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
dev: false
/l7-tiny-sdf@0.0.4: /l7-tiny-sdf@0.0.4:
resolution: {integrity: sha512-hMuA5jolQCyhK+QufHMy+qxZQlc9uD/S7jVWFDyVy5TKb3HxMOGc1RcqMwcvlgDXzmVqNWkxAN8LracSEwqYIw==} resolution: {integrity: sha512-hMuA5jolQCyhK+QufHMy+qxZQlc9uD/S7jVWFDyVy5TKb3HxMOGc1RcqMwcvlgDXzmVqNWkxAN8LracSEwqYIw==}
dev: false dev: false
@ -4421,6 +4465,18 @@ packages:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: false dev: false
/logform@2.6.0:
resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==}
engines: {node: '>= 12.0.0'}
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.4
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.4.3
triple-beam: 1.4.1
dev: false
/longest@1.0.1: /longest@1.0.1:
resolution: {integrity: sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==} resolution: {integrity: sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -4857,6 +4913,12 @@ packages:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
/one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
dependencies:
fn.name: 1.1.0
dev: false
/openapi-types@12.1.3: /openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
dev: false dev: false
@ -6020,6 +6082,15 @@ packages:
pify: 2.3.0 pify: 2.3.0
dev: true dev: true
/readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
dev: false
/readdirp@3.6.0: /readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
@ -6169,6 +6240,11 @@ packages:
is-regex: 1.1.4 is-regex: 1.1.4
dev: false dev: false
/safe-stable-stringify@2.4.3:
resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
engines: {node: '>=10'}
dev: false
/safer-buffer@2.1.2: /safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
dev: false dev: false
@ -6418,6 +6494,10 @@ packages:
stackframe: 1.3.4 stackframe: 1.3.4
dev: false dev: false
/stack-trace@0.0.10:
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
dev: false
/stackframe@1.3.4: /stackframe@1.3.4:
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
dev: false dev: false
@ -6491,6 +6571,12 @@ packages:
es-abstract: 1.22.1 es-abstract: 1.22.1
dev: false dev: false
/string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
safe-buffer: 5.2.1
dev: false
/strip-ansi@3.0.1: /strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==} resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -6638,6 +6724,10 @@ packages:
yallist: 4.0.0 yallist: 4.0.0
dev: true dev: true
/text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
dev: false
/thenify-all@1.6.0: /thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
@ -6725,6 +6815,11 @@ packages:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: false dev: false
/triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
dev: false
/trpc-openapi@1.2.0(@trpc/server@10.38.4)(zod@3.22.2): /trpc-openapi@1.2.0(@trpc/server@10.38.4)(zod@3.22.2):
resolution: {integrity: sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==} resolution: {integrity: sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==}
peerDependencies: peerDependencies:
@ -6947,7 +7042,6 @@ packages:
/util-deprecate@1.0.2: /util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/utility-types@3.10.0: /utility-types@3.10.0:
resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==} resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==}
@ -7083,6 +7177,32 @@ packages:
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
dev: false dev: false
/winston-transport@4.6.0:
resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==}
engines: {node: '>= 12.0.0'}
dependencies:
logform: 2.6.0
readable-stream: 3.6.2
triple-beam: 1.4.1
dev: false
/winston@3.11.0:
resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==}
engines: {node: '>= 12.0.0'}
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.3
async: 3.2.4
is-stream: 2.0.1
logform: 2.6.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.4.3
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.6.0
dev: false
/wordwrap@0.0.2: /wordwrap@0.0.2:
resolution: {integrity: sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==} resolution: {integrity: sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}

View File

@ -249,6 +249,7 @@ model Monitor {
notifications Notification[] notifications Notification[]
events MonitorEvent[] events MonitorEvent[]
datas MonitorData[] datas MonitorData[]
status MonitorStatus[]
@@index([workspaceId]) @@index([workspaceId])
} }
@ -271,3 +272,16 @@ model MonitorData {
monitor Monitor @relation(fields: [monitorId], references: [id], onUpdate: Cascade, onDelete: Cascade) monitor Monitor @relation(fields: [monitorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
} }
// Use for record latest monitor status, for example tls status
model MonitorStatus {
monitorId String @db.VarChar(30)
statusName String @db.VarChar(50)
payload Json @db.Json
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
monitor Monitor @relation(fields: [monitorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
@@id([monitorId, statusName])
}

View File

@ -1,7 +1,7 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import type { Monitor } from '@prisma/client'; import type { Monitor } from '@prisma/client';
import { Button, Form, Input, InputNumber, Select } from 'antd'; import { Button, Form, Input, InputNumber, Select } from 'antd';
import { monitorProviders } from './provider'; import { getMonitorProvider, monitorProviders } from './provider';
import { useEvent } from '../../../hooks/useEvent'; import { useEvent } from '../../../hooks/useEvent';
import { NotificationPicker } from '../../notification/NotificationPicker'; import { NotificationPicker } from '../../notification/NotificationPicker';
@ -31,7 +31,7 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
const typeValue = Form.useWatch('type', form); const typeValue = Form.useWatch('type', form);
const formEl = useMemo(() => { const formEl = useMemo(() => {
const provider = monitorProviders.find((s) => s.name === typeValue); const provider = getMonitorProvider(typeValue);
if (!provider) { if (!provider) {
return null; return null;

View File

@ -0,0 +1,96 @@
import { Form, Input, Select } from 'antd';
import React from 'react';
import { MonitorOverviewComponent, MonitorProvider } from './types';
import { trpc } from '../../../../api/trpc';
import { MonitorStatsBlock } from '../../../monitor/MonitorStatsBlock';
import dayjs from 'dayjs';
import { isEmpty } from 'lodash-es';
import { useCurrentWorkspaceId } from '../../../../store/user';
const MonitorHttp: React.FC = React.memo(() => {
return (
<>
<Form.Item
label="Url"
name={['payload', 'url']}
rules={[{ required: true }]}
>
<Input placeholder="https://example.com" />
</Form.Item>
<Form.Item
label="Method"
name={['payload', 'method']}
initialValue={'get'}
>
<Select>
<Select.Option value="get">GET</Select.Option>
<Select.Option value="post">POST</Select.Option>
<Select.Option value="put">PUT</Select.Option>
<Select.Option value="patch">PATCH</Select.Option>
<Select.Option value="delete">DELETE</Select.Option>
<Select.Option value="head">HEAD</Select.Option>
<Select.Option value="options">OPTIONS</Select.Option>
</Select>
</Form.Item>
<Form.Item
label="Content-Type"
name={['payload', 'contentType']}
initialValue={'application/json'}
>
<Select>
<Select.Option value="application/json">
application/json
</Select.Option>
<Select.Option value="application/x-www-form-urlencoded">
application/x-www-form-urlencoded
</Select.Option>
<Select.Option value="text/xml; charset=utf-8">
text/xml
</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Content-Type" name={['payload', 'bodyValue']}>
<Input.TextArea placeholder='For example:\n{ "key": "value" }' />
</Form.Item>
</>
);
});
MonitorHttp.displayName = 'MonitorHttp';
export const MonitorHttpOverview: MonitorOverviewComponent = React.memo(
(props) => {
const workspaceId = useCurrentWorkspaceId();
const { data } = trpc.monitor.getStatus.useQuery({
workspaceId,
monitorId: props.monitorId,
statusName: 'tls',
});
if (!data || !data.payload || typeof data.payload !== 'object') {
return null;
}
const payload = data.payload as Record<string, any>;
if (isEmpty(payload.certInfo)) {
return null;
}
return (
<MonitorStatsBlock
title="Cert Exp."
desc={dayjs(payload.certInfo?.validTo).format('YYYY-MM-DD')}
text={`${payload.certInfo?.daysRemaining} days`}
/>
);
}
);
MonitorHttpOverview.displayName = 'MonitorHttpOverview';
export const httpProvider: MonitorProvider = {
label: 'HTTP',
name: 'http',
link: (info) => String(info.payload.url),
form: MonitorHttp,
overview: [MonitorHttpOverview],
};

View File

@ -1,25 +1,25 @@
import React from 'react'; import React from 'react';
import { MonitorInfo } from '../../../../../types'; import { MonitorInfo } from '../../../../../types';
import { MonitorPing } from './ping'; import { pingProvider } from './ping';
import { httpProvider } from './http';
interface MonitorProvider { import { MonitorProvider } from './types';
label: string;
name: string;
link: (info: MonitorInfo) => React.ReactNode;
form: React.ComponentType;
}
export const monitorProviders: MonitorProvider[] = [ export const monitorProviders: MonitorProvider[] = [
{ pingProvider, // ping
label: 'Ping', httpProvider, // http
name: 'ping',
link: (info) => String(info.payload.hostname),
form: MonitorPing,
},
]; ];
export function getMonitorProvider(type: string) {
const provider = monitorProviders.find((m) => m.name === type);
if (!provider) {
return null;
}
return provider;
}
export function getMonitorLink(info: MonitorInfo): React.ReactNode { export function getMonitorLink(info: MonitorInfo): React.ReactNode {
const provider = monitorProviders.find((m) => m.name === info.type); const provider = getMonitorProvider(info.type);
if (!provider) { if (!provider) {
return null; return null;
} }

View File

@ -1,5 +1,6 @@
import { Form, Input } from 'antd'; import { Form, Input } from 'antd';
import React from 'react'; import React from 'react';
import { MonitorProvider } from './types';
export const MonitorPing: React.FC = React.memo(() => { export const MonitorPing: React.FC = React.memo(() => {
return ( return (
@ -15,3 +16,10 @@ export const MonitorPing: React.FC = React.memo(() => {
); );
}); });
MonitorPing.displayName = 'MonitorPing'; MonitorPing.displayName = 'MonitorPing';
export const pingProvider: MonitorProvider = {
label: 'Ping',
name: 'ping',
link: (info) => String(info.payload.hostname),
form: MonitorPing,
};

View File

@ -0,0 +1,13 @@
import { MonitorInfo } from '../../../../../types';
export interface MonitorProvider {
label: string;
name: string;
link: (info: MonitorInfo) => React.ReactNode;
form: React.ComponentType;
overview?: MonitorOverviewComponent[];
}
export type MonitorOverviewComponent = React.ComponentType<{
monitorId: string;
}>;

View File

@ -19,7 +19,7 @@ export const MonitorEventList: React.FC<MonitorEventListProps> = React.memo(
const navigate = useNavigate(); const navigate = useNavigate();
if (isLoading === false && data.length === 0) { if (isLoading === false && data.length === 0) {
return <Empty />; return <Empty description="No events" />;
} }
return ( return (

View File

@ -4,7 +4,7 @@ import React, { useMemo, useState } from 'react';
import { trpc } from '../../api/trpc'; import { trpc } from '../../api/trpc';
import { useCurrentWorkspaceId } from '../../store/user'; import { useCurrentWorkspaceId } from '../../store/user';
import { Loading } from '../Loading'; import { Loading } from '../Loading';
import { getMonitorLink } from '../modals/monitor/provider'; import { getMonitorLink, getMonitorProvider } from '../modals/monitor/provider';
import { NotFoundTip } from '../NotFoundTip'; import { NotFoundTip } from '../NotFoundTip';
import { MonitorInfo as MonitorInfoType } from '../../../types'; import { MonitorInfo as MonitorInfoType } from '../../../types';
import { Area, AreaConfig } from '@ant-design/charts'; import { Area, AreaConfig } from '@ant-design/charts';
@ -14,6 +14,7 @@ import { last, uniqBy } from 'lodash-es';
import { ErrorTip } from '../ErrorTip'; import { ErrorTip } from '../ErrorTip';
import { ColorTag } from '../ColorTag'; import { ColorTag } from '../ColorTag';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { MonitorStatsBlock } from './MonitorStatsBlock';
interface MonitorInfoProps { interface MonitorInfoProps {
monitorId: string; monitorId: string;
@ -88,6 +89,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
<Card> <Card>
<MonitorDataMetrics <MonitorDataMetrics
monitorId={monitorId} monitorId={monitorId}
monitorType={monitorInfo.type}
currectResponse={currectResponse} currectResponse={currectResponse}
/> />
</Card> </Card>
@ -103,14 +105,29 @@ MonitorInfo.displayName = 'MonitorInfo';
export const MonitorDataMetrics: React.FC<{ export const MonitorDataMetrics: React.FC<{
monitorId: string; monitorId: string;
monitorType: string;
currectResponse: number; currectResponse: number;
}> = React.memo((props) => { }> = React.memo((props) => {
const workspaceId = useCurrentWorkspaceId(); const workspaceId = useCurrentWorkspaceId();
const { monitorId, currectResponse } = props; const { monitorId, monitorType, currectResponse } = props;
const { data, isLoading } = trpc.monitor.dataMetrics.useQuery({ const { data, isLoading } = trpc.monitor.dataMetrics.useQuery({
workspaceId, workspaceId,
monitorId, monitorId,
}); });
const providerOverview = useMemo(() => {
const provider = getMonitorProvider(monitorType);
if (!provider || !provider.overview) {
return null;
}
return (
<>
{provider.overview.map((Component) => (
<Component monitorId={monitorId} />
))}
</>
);
}, [monitorType]);
if (isLoading) { if (isLoading) {
return <Loading />; return <Loading />;
@ -122,44 +139,40 @@ export const MonitorDataMetrics: React.FC<{
return ( return (
<div className="flex justify-between text-center"> <div className="flex justify-between text-center">
<div> <MonitorStatsBlock
<div className="font-bold mb-0.5">Response</div> title="Response"
<div className="text-gray-500">(Current)</div> desc="(Current)"
<div>{currectResponse} ms</div> text={`${currectResponse} ms`}
</div> />
<div> <MonitorStatsBlock
<div className="font-bold mb-0.5">Avg. Response</div> title="Avg. Response"
<div className="text-gray-500">(24 hour)</div> desc="(24 hour)"
<div>{parseFloat(data.recent1DayAvg.toFixed(0))} ms</div> text={`${parseFloat(data.recent1DayAvg.toFixed(0))} ms`}
</div> />
<div> <MonitorStatsBlock
<div className="font-bold mb-0.5">Uptime</div> title="Uptime"
<div className="text-gray-500 mb-2 text-xs">(24 hour)</div> desc="(24 hour)"
<div> text={`${parseFloat(
{parseFloat( (
( (data.recent1DayOnlineCount /
(data.recent1DayOnlineCount / (data.recent1DayOnlineCount + data.recent1DayOfflineCount)) *
(data.recent1DayOnlineCount + data.recent1DayOfflineCount)) * 100
100 ).toFixed(2)
).toFixed(2) )} %`}
)} />
% <MonitorStatsBlock
</div> title="Uptime"
</div> desc="(30 days)"
<div> text={`${parseFloat(
<div className="font-bold mb-0.5">Uptime</div> (
<div className="text-gray-500">(30 days)</div> (data.recent30DayOnlineCount /
<div> (data.recent30DayOnlineCount + data.recent30DayOfflineCount)) *
{parseFloat( 100
( ).toFixed(2)
(data.recent30DayOnlineCount / )} %`}
(data.recent30DayOnlineCount + data.recent30DayOfflineCount)) * />
100
).toFixed(2) {providerOverview}
)}
%
</div>
</div>
</div> </div>
); );
}); });

View File

@ -0,0 +1,19 @@
import React from 'react';
interface MonitorStatsBlockProps {
title: string;
desc: string;
text: string;
}
export const MonitorStatsBlock: React.FC<MonitorStatsBlockProps> = React.memo(
(props) => {
return (
<div>
<div className="font-bold mb-0.5">{props.title}</div>
<div className="text-gray-500">{props.desc}</div>
<div>{props.text}</div>
</div>
);
}
);
MonitorStatsBlock.displayName = 'MonitorStatsBlock';

View File

@ -0,0 +1,164 @@
import { MonitorProvider } from './type';
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { logger } from '../../../utils/logger';
import dayjs from 'dayjs';
import { prisma } from '../../_client';
export const http: MonitorProvider<{
url: string;
method?: string;
timeout?: number; // second
contentType?: string;
bodyValue?: string;
maxRedirects?: number;
}> = {
run: async (monitor) => {
if (typeof monitor.payload !== 'object') {
throw new Error('monitor.payload should be object');
}
const {
url,
method = 'get',
timeout = 30,
contentType,
bodyValue,
maxRedirects,
} = monitor.payload;
const config: AxiosRequestConfig = {
url: url,
method: (method || 'get').toLowerCase(),
timeout: timeout * 1000,
headers: {
Accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
...(contentType ? { 'Content-Type': contentType } : {}),
},
maxRedirects: maxRedirects,
// validateStatus: (status) => {
// return checkStatusCode(status, this.getAcceptedStatuscodes());
// },
};
if (bodyValue) {
config.data = bodyValue;
}
try {
const startTime = dayjs();
const res = await axios(config);
const diff = dayjs().diff(startTime, 'ms');
if (url.startsWith('https:')) {
try {
const { valid, certInfo } = checkCertificate(res);
await prisma.monitorStatus.upsert({
where: {
monitorId_statusName: {
monitorId: monitor.id,
statusName: 'tls',
},
},
update: {
payload: {
valid,
certInfo,
},
},
create: {
monitorId: monitor.id,
statusName: 'tls',
payload: {
valid,
certInfo,
},
},
});
} catch (err) {}
}
return diff;
} catch (err) {
logger.error('run monitor http error', err);
return -1;
}
},
};
function checkCertificate(res: AxiosResponse<any, any>) {
if (!res.request.res.socket) {
throw new Error('No socket found');
}
const info = res.request.res.socket.getPeerCertificate(true);
const valid = res.request.res.socket.authorized || false;
logger.info('cert', 'Parsing Certificate Info', info);
const parsedInfo = parseCertificateInfo(info);
return {
valid: valid,
certInfo: parsedInfo,
};
}
function parseCertificateInfo(info: any) {
let link = info;
let i = 0;
const existingList: Record<string, boolean> = {};
while (link) {
logger.debug('cert', `[${i}] ${link.fingerprint}`);
if (!link.valid_from || !link.valid_to) {
break;
}
link.validTo = new Date(link.valid_to);
link.validFor = link.subjectaltname
?.replace(/DNS:|IP Address:/g, '')
.split(', ');
link.daysRemaining = getDaysRemaining(new Date(), link.validTo);
existingList[link.fingerprint] = true;
// Move up the chain until loop is encountered
if (link.issuerCertificate == null) {
link.certType = i === 0 ? 'self-signed' : 'root CA';
break;
} else if (link.issuerCertificate.fingerprint in existingList) {
// a root CA certificate is typically "signed by itself" (=> "self signed certificate") and thus the "issuerCertificate" is a reference to itself.
logger.debug('cert', `[Last] ${link.issuerCertificate.fingerprint}`);
link.certType = i === 0 ? 'self-signed' : 'root CA';
link.issuerCertificate = null;
break;
} else {
link.certType = i === 0 ? 'server' : 'intermediate CA';
link = link.issuerCertificate;
}
// Should be no use, but just in case.
if (i > 500) {
throw new Error('Dead loop occurred in parseCertificateInfo');
}
i++;
}
return info;
}
/**
* Get days remaining from a time range
* @param {Date} validFrom Start date
* @param {Date} validTo End date
* @returns {number} Number of days remaining
*/
function getDaysRemaining(validFrom: Date, validTo: Date) {
const daysRemaining = dayjs(validTo).diff(validFrom, 'days');
return daysRemaining;
}

View File

@ -1,6 +1,8 @@
import { http } from './http';
import { ping } from './ping'; import { ping } from './ping';
import type { MonitorProvider } from './type'; import type { MonitorProvider } from './type';
export const monitorProviders: Record<string, MonitorProvider<any>> = { export const monitorProviders: Record<string, MonitorProvider<any>> = {
ping, ping,
http,
}; };

View File

@ -239,4 +239,23 @@ export const monitorRouter = router({
return list; return list;
}), }),
getStatus: workspaceProcedure
.input(
z.object({
monitorId: z.string().cuid2(),
statusName: z.string(),
})
)
.query(async ({ input }) => {
const { monitorId, statusName } = input;
return prisma.monitorStatus.findUnique({
where: {
monitorId_statusName: {
monitorId,
statusName,
},
},
});
}),
}); });

View File

@ -0,0 +1,40 @@
import winston, { format } from 'winston';
import util from 'util';
type Format = ReturnType<typeof format.cli>;
function utilFormatter(): Format {
return {
transform(info) {
const args = info[Symbol.for('splat')];
if (args) {
info.message = util.format(info.message, ...args);
}
return info;
},
};
}
export const logger = winston.createLogger({
level: 'info',
format: format.json(),
transports: [
//
// - Write all logs with importance level of `error` or less to `error.log`
// - Write all logs with importance level of `info` or less to `combined.log`
//
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.Console({
format: format.combine(
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
utilFormatter(),
format.colorize(),
format.printf(
({ level, message, label, timestamp }) =>
`${timestamp} ${label || '-'} ${level}: ${message}`
)
),
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
}),
],
});