feat: add http monitor which include cert exp date
This commit is contained in:
parent
594f3124ef
commit
31ba529022
@ -75,6 +75,7 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-express": "^0.10.0",
|
||||
"winston": "^3.11.0",
|
||||
"yup": "^1.2.0",
|
||||
"zod": "^3.22.2",
|
||||
"zustand": "^4.4.1"
|
||||
|
122
pnpm-lock.yaml
generated
122
pnpm-lock.yaml
generated
@ -166,6 +166,9 @@ dependencies:
|
||||
vite-express:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
winston:
|
||||
specifier: ^3.11.0
|
||||
version: 3.11.0
|
||||
yup:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0
|
||||
@ -1361,6 +1364,11 @@ packages:
|
||||
to-fast-properties: 2.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
@ -1372,6 +1380,14 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
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:
|
||||
resolution: {integrity: sha512-QCg9sL4uhjn468FDEsb/S9hS2xUZSrv/+dApb1Ze5VKO96pTXKNJZ6MGhIpgWkc1TVhbVGH9/7rq/Mf8/jWicw==}
|
||||
dependencies:
|
||||
@ -2204,6 +2220,10 @@ packages:
|
||||
minipass: 4.2.8
|
||||
dev: true
|
||||
|
||||
/@types/triple-beam@1.3.4:
|
||||
resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==}
|
||||
dev: false
|
||||
|
||||
/@types/uuid@9.0.3:
|
||||
resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==}
|
||||
dev: false
|
||||
@ -2805,6 +2825,13 @@ packages:
|
||||
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
|
||||
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:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -3295,6 +3322,10 @@ packages:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
dev: false
|
||||
|
||||
/enabled@2.0.0:
|
||||
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
|
||||
dev: false
|
||||
|
||||
/encodeurl@1.0.2:
|
||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -3676,6 +3707,10 @@ packages:
|
||||
uglify-js: 2.8.29
|
||||
dev: false
|
||||
|
||||
/fn.name@1.1.0:
|
||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.2:
|
||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
||||
engines: {node: '>=4.0'}
|
||||
@ -4196,6 +4231,11 @@ packages:
|
||||
call-bind: 1.0.2
|
||||
dev: false
|
||||
|
||||
/is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/is-string@1.0.7:
|
||||
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -4339,6 +4379,10 @@ packages:
|
||||
is-buffer: 1.1.6
|
||||
dev: false
|
||||
|
||||
/kuler@2.0.0:
|
||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||
dev: false
|
||||
|
||||
/l7-tiny-sdf@0.0.4:
|
||||
resolution: {integrity: sha512-hMuA5jolQCyhK+QufHMy+qxZQlc9uD/S7jVWFDyVy5TKb3HxMOGc1RcqMwcvlgDXzmVqNWkxAN8LracSEwqYIw==}
|
||||
dev: false
|
||||
@ -4421,6 +4465,18 @@ packages:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -4857,6 +4913,12 @@ packages:
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
dev: false
|
||||
@ -6020,6 +6082,15 @@ packages:
|
||||
pify: 2.3.0
|
||||
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:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@ -6169,6 +6240,11 @@ packages:
|
||||
is-regex: 1.1.4
|
||||
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:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
@ -6418,6 +6494,10 @@ packages:
|
||||
stackframe: 1.3.4
|
||||
dev: false
|
||||
|
||||
/stack-trace@0.0.10:
|
||||
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||
dev: false
|
||||
|
||||
/stackframe@1.3.4:
|
||||
resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==}
|
||||
dev: false
|
||||
@ -6491,6 +6571,12 @@ packages:
|
||||
es-abstract: 1.22.1
|
||||
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:
|
||||
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -6638,6 +6724,10 @@ packages:
|
||||
yallist: 4.0.0
|
||||
dev: true
|
||||
|
||||
/text-hex@1.0.0:
|
||||
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
|
||||
dev: false
|
||||
|
||||
/thenify-all@1.6.0:
|
||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||
engines: {node: '>=0.8'}
|
||||
@ -6725,6 +6815,11 @@ packages:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
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):
|
||||
resolution: {integrity: sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==}
|
||||
peerDependencies:
|
||||
@ -6947,7 +7042,6 @@ packages:
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
||||
/utility-types@3.10.0:
|
||||
resolution: {integrity: sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==}
|
||||
@ -7083,6 +7177,32 @@ packages:
|
||||
engines: {node: '>= 0.8.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
@ -249,6 +249,7 @@ model Monitor {
|
||||
notifications Notification[]
|
||||
events MonitorEvent[]
|
||||
datas MonitorData[]
|
||||
status MonitorStatus[]
|
||||
|
||||
@@index([workspaceId])
|
||||
}
|
||||
@ -271,3 +272,16 @@ model MonitorData {
|
||||
|
||||
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])
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import type { Monitor } from '@prisma/client';
|
||||
import { Button, Form, Input, InputNumber, Select } from 'antd';
|
||||
import { monitorProviders } from './provider';
|
||||
import { getMonitorProvider, monitorProviders } from './provider';
|
||||
import { useEvent } from '../../../hooks/useEvent';
|
||||
import { NotificationPicker } from '../../notification/NotificationPicker';
|
||||
|
||||
@ -31,7 +31,7 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
|
||||
const typeValue = Form.useWatch('type', form);
|
||||
|
||||
const formEl = useMemo(() => {
|
||||
const provider = monitorProviders.find((s) => s.name === typeValue);
|
||||
const provider = getMonitorProvider(typeValue);
|
||||
|
||||
if (!provider) {
|
||||
return null;
|
||||
|
96
src/client/components/modals/monitor/provider/http.tsx
Normal file
96
src/client/components/modals/monitor/provider/http.tsx
Normal 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],
|
||||
};
|
@ -1,25 +1,25 @@
|
||||
import React from 'react';
|
||||
import { MonitorInfo } from '../../../../../types';
|
||||
import { MonitorPing } from './ping';
|
||||
|
||||
interface MonitorProvider {
|
||||
label: string;
|
||||
name: string;
|
||||
link: (info: MonitorInfo) => React.ReactNode;
|
||||
form: React.ComponentType;
|
||||
}
|
||||
import { pingProvider } from './ping';
|
||||
import { httpProvider } from './http';
|
||||
import { MonitorProvider } from './types';
|
||||
|
||||
export const monitorProviders: MonitorProvider[] = [
|
||||
{
|
||||
label: 'Ping',
|
||||
name: 'ping',
|
||||
link: (info) => String(info.payload.hostname),
|
||||
form: MonitorPing,
|
||||
},
|
||||
pingProvider, // ping
|
||||
httpProvider, // http
|
||||
];
|
||||
|
||||
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 {
|
||||
const provider = monitorProviders.find((m) => m.name === info.type);
|
||||
const provider = getMonitorProvider(info.type);
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Form, Input } from 'antd';
|
||||
import React from 'react';
|
||||
import { MonitorProvider } from './types';
|
||||
|
||||
export const MonitorPing: React.FC = React.memo(() => {
|
||||
return (
|
||||
@ -15,3 +16,10 @@ export const MonitorPing: React.FC = React.memo(() => {
|
||||
);
|
||||
});
|
||||
MonitorPing.displayName = 'MonitorPing';
|
||||
|
||||
export const pingProvider: MonitorProvider = {
|
||||
label: 'Ping',
|
||||
name: 'ping',
|
||||
link: (info) => String(info.payload.hostname),
|
||||
form: MonitorPing,
|
||||
};
|
||||
|
13
src/client/components/modals/monitor/provider/types.ts
Normal file
13
src/client/components/modals/monitor/provider/types.ts
Normal 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;
|
||||
}>;
|
@ -19,7 +19,7 @@ export const MonitorEventList: React.FC<MonitorEventListProps> = React.memo(
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (isLoading === false && data.length === 0) {
|
||||
return <Empty />;
|
||||
return <Empty description="No events" />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -4,7 +4,7 @@ import React, { useMemo, useState } from 'react';
|
||||
import { trpc } from '../../api/trpc';
|
||||
import { useCurrentWorkspaceId } from '../../store/user';
|
||||
import { Loading } from '../Loading';
|
||||
import { getMonitorLink } from '../modals/monitor/provider';
|
||||
import { getMonitorLink, getMonitorProvider } from '../modals/monitor/provider';
|
||||
import { NotFoundTip } from '../NotFoundTip';
|
||||
import { MonitorInfo as MonitorInfoType } from '../../../types';
|
||||
import { Area, AreaConfig } from '@ant-design/charts';
|
||||
@ -14,6 +14,7 @@ import { last, uniqBy } from 'lodash-es';
|
||||
import { ErrorTip } from '../ErrorTip';
|
||||
import { ColorTag } from '../ColorTag';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { MonitorStatsBlock } from './MonitorStatsBlock';
|
||||
|
||||
interface MonitorInfoProps {
|
||||
monitorId: string;
|
||||
@ -88,6 +89,7 @@ export const MonitorInfo: React.FC<MonitorInfoProps> = React.memo((props) => {
|
||||
<Card>
|
||||
<MonitorDataMetrics
|
||||
monitorId={monitorId}
|
||||
monitorType={monitorInfo.type}
|
||||
currectResponse={currectResponse}
|
||||
/>
|
||||
</Card>
|
||||
@ -103,14 +105,29 @@ MonitorInfo.displayName = 'MonitorInfo';
|
||||
|
||||
export const MonitorDataMetrics: React.FC<{
|
||||
monitorId: string;
|
||||
monitorType: string;
|
||||
currectResponse: number;
|
||||
}> = React.memo((props) => {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { monitorId, currectResponse } = props;
|
||||
const { monitorId, monitorType, currectResponse } = props;
|
||||
const { data, isLoading } = trpc.monitor.dataMetrics.useQuery({
|
||||
workspaceId,
|
||||
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) {
|
||||
return <Loading />;
|
||||
@ -122,44 +139,40 @@ export const MonitorDataMetrics: React.FC<{
|
||||
|
||||
return (
|
||||
<div className="flex justify-between text-center">
|
||||
<div>
|
||||
<div className="font-bold mb-0.5">Response</div>
|
||||
<div className="text-gray-500">(Current)</div>
|
||||
<div>{currectResponse} ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold mb-0.5">Avg. Response</div>
|
||||
<div className="text-gray-500">(24 hour)</div>
|
||||
<div>{parseFloat(data.recent1DayAvg.toFixed(0))} ms</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold mb-0.5">Uptime</div>
|
||||
<div className="text-gray-500 mb-2 text-xs">(24 hour)</div>
|
||||
<div>
|
||||
{parseFloat(
|
||||
(
|
||||
(data.recent1DayOnlineCount /
|
||||
(data.recent1DayOnlineCount + data.recent1DayOfflineCount)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold mb-0.5">Uptime</div>
|
||||
<div className="text-gray-500">(30 days)</div>
|
||||
<div>
|
||||
{parseFloat(
|
||||
(
|
||||
(data.recent30DayOnlineCount /
|
||||
(data.recent30DayOnlineCount + data.recent30DayOfflineCount)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
)}
|
||||
%
|
||||
</div>
|
||||
</div>
|
||||
<MonitorStatsBlock
|
||||
title="Response"
|
||||
desc="(Current)"
|
||||
text={`${currectResponse} ms`}
|
||||
/>
|
||||
<MonitorStatsBlock
|
||||
title="Avg. Response"
|
||||
desc="(24 hour)"
|
||||
text={`${parseFloat(data.recent1DayAvg.toFixed(0))} ms`}
|
||||
/>
|
||||
<MonitorStatsBlock
|
||||
title="Uptime"
|
||||
desc="(24 hour)"
|
||||
text={`${parseFloat(
|
||||
(
|
||||
(data.recent1DayOnlineCount /
|
||||
(data.recent1DayOnlineCount + data.recent1DayOfflineCount)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
)} %`}
|
||||
/>
|
||||
<MonitorStatsBlock
|
||||
title="Uptime"
|
||||
desc="(30 days)"
|
||||
text={`${parseFloat(
|
||||
(
|
||||
(data.recent30DayOnlineCount /
|
||||
(data.recent30DayOnlineCount + data.recent30DayOfflineCount)) *
|
||||
100
|
||||
).toFixed(2)
|
||||
)} %`}
|
||||
/>
|
||||
|
||||
{providerOverview}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
19
src/client/components/monitor/MonitorStatsBlock.tsx
Normal file
19
src/client/components/monitor/MonitorStatsBlock.tsx
Normal 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';
|
164
src/server/model/monitor/provider/http.ts
Normal file
164
src/server/model/monitor/provider/http.ts
Normal 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;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import { http } from './http';
|
||||
import { ping } from './ping';
|
||||
import type { MonitorProvider } from './type';
|
||||
|
||||
export const monitorProviders: Record<string, MonitorProvider<any>> = {
|
||||
ping,
|
||||
http,
|
||||
};
|
||||
|
@ -239,4 +239,23 @@ export const monitorRouter = router({
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
40
src/server/utils/logger.ts
Normal file
40
src/server/utils/logger.ts
Normal 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',
|
||||
}),
|
||||
],
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user