feat: add tcp port monitor

This commit is contained in:
moonrailgun 2024-01-11 01:01:48 +08:00
parent e6f02677e5
commit 9892d3ac5a
9 changed files with 141 additions and 7 deletions

View File

@ -82,6 +82,7 @@
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"str2int": "^1.1.0", "str2int": "^1.1.0",
"swagger-ui-express": "^5.0.0", "swagger-ui-express": "^5.0.0",
"tcp-ping": "^0.1.1",
"trpc-openapi": "^1.2.0", "trpc-openapi": "^1.2.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"uuid": "^9.0.0", "uuid": "^9.0.0",
@ -116,6 +117,7 @@
"@types/request-ip": "^0.0.38", "@types/request-ip": "^0.0.38",
"@types/swagger-ui-express": "^4.1.5", "@types/swagger-ui-express": "^4.1.5",
"@types/tar": "^6.1.5", "@types/tar": "^6.1.5",
"@types/tcp-ping": "^0.1.5",
"@vitejs/plugin-react": "^4.0.4", "@vitejs/plugin-react": "^4.0.4",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View File

@ -181,6 +181,9 @@ dependencies:
swagger-ui-express: swagger-ui-express:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0(express@4.18.2) version: 5.0.0(express@4.18.2)
tcp-ping:
specifier: ^0.1.1
version: 0.1.1
trpc-openapi: trpc-openapi:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0(@trpc/server@10.38.4)(zod@3.22.2) version: 1.2.0(@trpc/server@10.38.4)(zod@3.22.2)
@ -279,6 +282,9 @@ devDependencies:
'@types/tar': '@types/tar':
specifier: ^6.1.5 specifier: ^6.1.5
version: 6.1.5 version: 6.1.5
'@types/tcp-ping':
specifier: ^0.1.5
version: 0.1.5
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^4.0.4 specifier: ^4.0.4
version: 4.0.4(vite@4.4.9) version: 4.0.4(vite@4.4.9)
@ -2749,6 +2755,10 @@ packages:
minipass: 4.2.8 minipass: 4.2.8
dev: true dev: true
/@types/tcp-ping@0.1.5:
resolution: {integrity: sha512-79CSV6HXSi53zB7JwEpDMIPa881n7drC+Ed1JtQ5kdVUklYyG1g4GqefuUQy/AblK58Q5JAS7d9LWbdE2xiEqA==}
dev: true
/@types/triple-beam@1.3.4: /@types/triple-beam@1.3.4:
resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==} resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==}
dev: false dev: false
@ -9429,6 +9439,10 @@ packages:
yallist: 4.0.0 yallist: 4.0.0
dev: true dev: true
/tcp-ping@0.1.1:
resolution: {integrity: sha512-7Ed10Ds0hYnF+O1lfiZ2iSZ1bCAj+96Madctebmq7Y1ALPWlBY4YI8C6pCL+UTlshFY5YogixKLpgDP/4BlHrw==}
dev: false
/temp-dir@1.0.0: /temp-dir@1.0.0:
resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==}
engines: {node: '>=4'} engines: {node: '>=4'}

View File

@ -2,7 +2,7 @@ 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 { getMonitorProvider, monitorProviders } from './provider'; import { getMonitorProvider, monitorProviders } from './provider';
import { useEvent } from '../../hooks/useEvent'; import { useEvent, useEventWithLoading } from '../../hooks/useEvent';
import { NotificationPicker } from '../notification/NotificationPicker'; import { NotificationPicker } from '../notification/NotificationPicker';
export type MonitorInfoEditorValues = Omit< export type MonitorInfoEditorValues = Omit<
@ -23,7 +23,7 @@ const defaultValues: Omit<MonitorInfoEditorValues, 'payload'> = {
interface MonitorInfoEditorProps { interface MonitorInfoEditorProps {
initialValues?: MonitorInfoEditorValues; initialValues?: MonitorInfoEditorValues;
onSave: (value: MonitorInfoEditorValues) => void; onSave: (value: MonitorInfoEditorValues) => Promise<void>;
} }
export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo( export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
(props) => { (props) => {
@ -46,8 +46,8 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
return <Component />; return <Component />;
}, [provider]); }, [provider]);
const handleSubmit = useEvent((values) => { const [handleSubmit, isLoading] = useEventWithLoading(async (values) => {
props.onSave({ await props.onSave({
...values, ...values,
active: true, active: true,
}); });
@ -96,7 +96,7 @@ export const MonitorInfoEditor: React.FC<MonitorInfoEditorProps> = React.memo(
<NotificationPicker allowClear={true} mode="multiple" /> <NotificationPicker allowClear={true} mode="multiple" />
</Form.Item> </Form.Item>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit" loading={isLoading}>
Save Save
</Button> </Button>
</Form> </Form>

View File

@ -5,11 +5,13 @@ import { httpProvider } from './http';
import { MonitorProvider } from './types'; import { MonitorProvider } from './types';
import { openaiProvider } from './openai'; import { openaiProvider } from './openai';
import { customProvider } from './custom'; import { customProvider } from './custom';
import { tcpProvider } from './tcp';
export const monitorProviders: MonitorProvider[] = [ export const monitorProviders: MonitorProvider[] = [
pingProvider, // ping pingProvider, // ping
tcpProvider, // tcp
httpProvider, // http httpProvider, // http
openaiProvider, // http openaiProvider, // openai
customProvider, // custom node script customProvider, // custom node script
]; ];

View File

@ -0,0 +1,43 @@
import { Form, Input, InputNumber } from 'antd';
import React from 'react';
import { MonitorProvider } from './types';
import { hostnameValidator, portValidator } from '../../../utils/validator';
export const MonitorTCP: React.FC = React.memo(() => {
return (
<>
<Form.Item
label="Host"
name={['payload', 'hostname']}
rules={[
{ required: true },
{
validator: hostnameValidator,
},
]}
>
<Input placeholder="example.com or 1.2.3.4" />
</Form.Item>
<Form.Item
label="Host"
name={['payload', 'port']}
rules={[
{ required: true },
{
validator: portValidator,
},
]}
>
<InputNumber placeholder="80" min={1} max={65535} />
</Form.Item>
</>
);
});
MonitorTCP.displayName = 'MonitorTCP';
export const tcpProvider: MonitorProvider = {
label: 'TCP Port',
name: 'tcp',
link: (info) => `${info.payload.hostname}:${info.payload.port}`,
form: MonitorTCP,
};

View File

@ -1,4 +1,4 @@
import { useMemo, useRef } from 'react'; import { useMemo, useRef, useState } from 'react';
// From https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts // From https://github.com/alibaba/hooks/blob/master/packages/hooks/src/useMemoizedFn/index.ts
@ -33,3 +33,23 @@ export function useEvent<T extends Noop>(fn: T) {
return memoizedFn.current as T; return memoizedFn.current as T;
} }
/**
* Same with useEvent but return loading state
*/
export function useEventWithLoading<T extends (...args: any[]) => Promise<any>>(
fn: T
): [T, boolean] {
const [isLoading, setIsLoading] = useState(false);
const _fn = useEvent(async (...args: Parameters<T>) => {
setIsLoading(true);
try {
return await fn(...args);
} finally {
setIsLoading(false);
}
}) as T;
return [_fn as T, isLoading];
}

View File

@ -25,3 +25,12 @@ export const urlSlugValidator: Validator = (rule, value, callback) => {
callback('Not valid slug'); callback('Not valid slug');
} }
}; };
export const portValidator: Validator = (rule, value, callback) => {
try {
z.number().min(1).max(65535).parse(value);
callback();
} catch (err) {
callback('Not valid port, it should be 1 ~ 65535');
}
};

View File

@ -3,10 +3,12 @@ import { ping } from './ping';
import { openai } from './openai'; import { openai } from './openai';
import type { MonitorProvider } from './type'; import type { MonitorProvider } from './type';
import { custom } from './custom'; import { custom } from './custom';
import { tcp } from './tcp';
export const monitorProviders: Record<string, MonitorProvider<any>> = { export const monitorProviders: Record<string, MonitorProvider<any>> = {
ping, ping,
http, http,
tcp,
openai, openai,
custom, custom,
}; };

View File

@ -0,0 +1,42 @@
import { MonitorProvider } from './type';
import tcpp from 'tcp-ping';
export const tcp: MonitorProvider<{
hostname: string;
port: number;
}> = {
run: async (monitor) => {
if (typeof monitor.payload !== 'object') {
throw new Error('monitor.payload should be object');
}
const { hostname, port } = monitor.payload;
const res = await pingAction(hostname, port);
return res;
},
};
function pingAction(hostname: string, port: number) {
return new Promise<number>((resolve, reject) => {
tcpp.ping(
{
address: hostname,
port,
attempts: 1,
},
(err, result) => {
if (err) {
reject(err);
} else {
if (result.results.length >= 1 && result.results[0].err) {
reject(result.results[0].err);
}
resolve(Math.round(result.max));
}
}
);
});
}