feat: add tcp port monitor
This commit is contained in:
parent
e6f02677e5
commit
9892d3ac5a
@ -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",
|
||||||
|
@ -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'}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
];
|
];
|
||||||
|
|
||||||
|
43
src/client/components/monitor/provider/tcp.tsx
Normal file
43
src/client/components/monitor/provider/tcp.tsx
Normal 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,
|
||||||
|
};
|
@ -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];
|
||||||
|
}
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
42
src/server/model/monitor/provider/tcp.ts
Normal file
42
src/server/model/monitor/provider/tcp.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user