mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-09-13 15:06:59 +08:00
Compare commits
4 Commits
extracted-
...
async-fs
Author | SHA1 | Date | |
---|---|---|---|
|
fd05aad398 | ||
|
a70a2c6b35 | ||
|
2f5a6d9648 | ||
|
21a6b4a131 |
@@ -1,6 +1,7 @@
|
|||||||
/.idea
|
/.idea
|
||||||
/node_modules
|
/node_modules
|
||||||
/data*
|
/data*
|
||||||
|
/cypress
|
||||||
/out
|
/out
|
||||||
/test
|
/test
|
||||||
/kubernetes
|
/kubernetes
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
"test/*.js",
|
"test/*.js",
|
||||||
|
"test/cypress",
|
||||||
"server/modules/apicache/*",
|
"server/modules/apicache/*",
|
||||||
"src/util.js"
|
"src/util.js"
|
||||||
],
|
],
|
||||||
|
2
.github/workflows/auto-test.yml
vendored
2
.github/workflows/auto-test.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
needs: [ check-linters ]
|
needs: [ check-linters ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
if: ${{ github.repository == 'louislam/uptime-kuma' }}
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ARMv7 ]
|
os: [ ARMv7 ]
|
||||||
|
@@ -427,33 +427,7 @@ Currently, there are 3 maintainers:
|
|||||||
### Procedures
|
### Procedures
|
||||||
|
|
||||||
We have a few procedures we follow. These are documented here:
|
We have a few procedures we follow. These are documented here:
|
||||||
- <details><summary>Set up a Docker Builder</summary>
|
|
||||||
<p>
|
|
||||||
|
|
||||||
- amd64, armv7 using local.
|
|
||||||
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer pass the `npm ci` command.
|
|
||||||
1. Add the public key to the remote server.
|
|
||||||
2. Add the remote context. The remote machine must be arm64 and installed Docker CE.
|
|
||||||
```
|
|
||||||
docker context create oracle-arm64-jp --docker "host=ssh://root@100.107.174.88"
|
|
||||||
```
|
|
||||||
3. Create a new builder.
|
|
||||||
```
|
|
||||||
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
|
|
||||||
docker buildx use kuma-builder
|
|
||||||
docker buildx inspect --bootstrap
|
|
||||||
```
|
|
||||||
4. Append the remote context to the builder.
|
|
||||||
```
|
|
||||||
docker buildx create --append --name kuma-builder --platform linux/arm64 oracle-arm64-jp
|
|
||||||
```
|
|
||||||
5. Verify the builder and check if the builder is using `kuma-builder`.
|
|
||||||
```
|
|
||||||
docker buildx inspect kuma-builder
|
|
||||||
docker buildx ls
|
|
||||||
```
|
|
||||||
</p>
|
|
||||||
</details>
|
|
||||||
- <details><summary>Release</summary>
|
- <details><summary>Release</summary>
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
@@ -516,3 +490,28 @@ We have a few procedures we follow. These are documented here:
|
|||||||
|
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Set up a Docker Builder
|
||||||
|
|
||||||
|
- amd64, armv7 using local.
|
||||||
|
- arm64 using remote arm64 cpu, as the emulator is too slow and can no longer pass the `npm ci` command.
|
||||||
|
1. Add the public key to the remote server.
|
||||||
|
2. Add the remote context. The remote machine must be arm64 and installed Docker CE.
|
||||||
|
```
|
||||||
|
docker context create oracle-arm64-jp --docker "host=ssh://root@100.107.174.88"
|
||||||
|
```
|
||||||
|
3. Create a new builder.
|
||||||
|
```
|
||||||
|
docker buildx create --name kuma-builder --platform linux/amd64,linux/arm/v7
|
||||||
|
docker buildx use kuma-builder
|
||||||
|
docker buildx inspect --bootstrap
|
||||||
|
```
|
||||||
|
4. Append the remote context to the builder.
|
||||||
|
```
|
||||||
|
docker buildx create --append --name kuma-builder --platform linux/arm64 oracle-arm64-jp
|
||||||
|
```
|
||||||
|
5. Verify the builder and check if the builder is using `kuma-builder`.
|
||||||
|
```
|
||||||
|
docker buildx inspect kuma-builder
|
||||||
|
docker buildx ls
|
||||||
|
```
|
||||||
|
28
config/cypress.config.js
Normal file
28
config/cypress.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
projectId: "vyjuem",
|
||||||
|
e2e: {
|
||||||
|
experimentalStudio: true,
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
|
||||||
|
},
|
||||||
|
fixturesFolder: "test/cypress/fixtures",
|
||||||
|
screenshotsFolder: "test/cypress/screenshots",
|
||||||
|
videosFolder: "test/cypress/videos",
|
||||||
|
downloadsFolder: "test/cypress/downloads",
|
||||||
|
supportFile: "test/cypress/support/e2e.js",
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
pageLoadTimeout: 60000,
|
||||||
|
viewportWidth: 1920,
|
||||||
|
viewportHeight: 1080,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/e2e/setup.cy.js",
|
||||||
|
"test/cypress/e2e/**/*.js"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
baseUrl: "http://localhost:3002",
|
||||||
|
},
|
||||||
|
});
|
10
config/cypress.frontend.config.js
Normal file
10
config/cypress.frontend.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
e2e: {
|
||||||
|
supportFile: false,
|
||||||
|
specPattern: [
|
||||||
|
"test/cypress/unit/**/*.js"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
});
|
@@ -1,10 +1,10 @@
|
|||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
const https = require("https");
|
const https = require("https");
|
||||||
const fs = require("fs");
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const Database = require("./database");
|
const Database = require("./database");
|
||||||
const { axiosAbortSignal } = require("./util-server");
|
const { axiosAbortSignal, fileExists } = require("./util-server");
|
||||||
|
|
||||||
class DockerHost {
|
class DockerHost {
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class DockerHost {
|
|||||||
options.socketPath = dockerHost.dockerDaemon;
|
options.socketPath = dockerHost.dockerDaemon;
|
||||||
} else if (dockerHost.dockerType === "tcp") {
|
} else if (dockerHost.dockerType === "tcp") {
|
||||||
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
options.baseURL = DockerHost.patchDockerURL(dockerHost.dockerDaemon);
|
||||||
options.httpsAgent = new https.Agent(DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
options.httpsAgent = new https.Agent(await DockerHost.getHttpsAgentOptions(dockerHost.dockerType, options.baseURL));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -143,7 +143,7 @@ class DockerHost {
|
|||||||
* @param {string} url The docker host URL rewritten to https://
|
* @param {string} url The docker host URL rewritten to https://
|
||||||
* @returns {object} HTTP agent options
|
* @returns {object} HTTP agent options
|
||||||
*/
|
*/
|
||||||
static getHttpsAgentOptions(dockerType, url) {
|
static async getHttpsAgentOptions(dockerType, url) {
|
||||||
let baseOptions = {
|
let baseOptions = {
|
||||||
maxCachedSessions: 0,
|
maxCachedSessions: 0,
|
||||||
rejectUnauthorized: true
|
rejectUnauthorized: true
|
||||||
@@ -156,10 +156,10 @@ class DockerHost {
|
|||||||
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
|
let certPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameCert);
|
||||||
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
|
let keyPath = path.join(Database.dockerTLSDir, dirName, DockerHost.CertificateFileNameKey);
|
||||||
|
|
||||||
if (dockerType === "tcp" && fs.existsSync(caPath) && fs.existsSync(certPath) && fs.existsSync(keyPath)) {
|
if (dockerType === "tcp" && await fileExists(caPath) && await fileExists(certPath) && await fileExists(keyPath)) {
|
||||||
let ca = fs.readFileSync(caPath);
|
let ca = await fs.readFile(caPath);
|
||||||
let key = fs.readFileSync(keyPath);
|
let key = await fs.readFile(keyPath);
|
||||||
let cert = fs.readFileSync(certPath);
|
let cert = await fs.readFile(certPath);
|
||||||
certOptions = {
|
certOptions = {
|
||||||
ca,
|
ca,
|
||||||
key,
|
key,
|
||||||
|
@@ -4,7 +4,7 @@ const { Prometheus } = require("../prometheus");
|
|||||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||||
SQL_DATETIME_FORMAT
|
SQL_DATETIME_FORMAT
|
||||||
} = require("../../src/util");
|
} = require("../../src/util");
|
||||||
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius,
|
const { tcping, ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, grpcQuery,
|
||||||
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
redisPingAsync, kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal
|
||||||
} = require("../util-server");
|
} = require("../util-server");
|
||||||
const { R } = require("redbean-node");
|
const { R } = require("redbean-node");
|
||||||
@@ -747,7 +747,7 @@ class Monitor extends BeanModel {
|
|||||||
} else if (dockerHost._dockerType === "tcp") {
|
} else if (dockerHost._dockerType === "tcp") {
|
||||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||||
options.httpsAgent = new https.Agent(
|
options.httpsAgent = new https.Agent(
|
||||||
DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
await DockerHost.getHttpsAgentOptions(dockerHost._dockerType, options.baseURL)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +773,37 @@ class Monitor extends BeanModel {
|
|||||||
bean.msg = "";
|
bean.msg = "";
|
||||||
bean.status = UP;
|
bean.status = UP;
|
||||||
bean.ping = dayjs().valueOf() - startTime;
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
} else if (this.type === "grpc-keyword") {
|
||||||
|
let startTime = dayjs().valueOf();
|
||||||
|
const options = {
|
||||||
|
grpcUrl: this.grpcUrl,
|
||||||
|
grpcProtobufData: this.grpcProtobuf,
|
||||||
|
grpcServiceName: this.grpcServiceName,
|
||||||
|
grpcEnableTls: this.grpcEnableTls,
|
||||||
|
grpcMethod: this.grpcMethod,
|
||||||
|
grpcBody: this.grpcBody,
|
||||||
|
};
|
||||||
|
const response = await grpcQuery(options);
|
||||||
|
bean.ping = dayjs().valueOf() - startTime;
|
||||||
|
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
|
||||||
|
let responseData = response.data;
|
||||||
|
if (responseData.length > 50) {
|
||||||
|
responseData = responseData.toString().substring(0, 47) + "...";
|
||||||
|
}
|
||||||
|
if (response.code !== 1) {
|
||||||
|
bean.status = DOWN;
|
||||||
|
bean.msg = `Error in send gRPC ${response.code} ${response.errorMessage}`;
|
||||||
|
} else {
|
||||||
|
let keywordFound = response.data.toString().includes(this.keyword);
|
||||||
|
if (keywordFound === !this.isInvertKeyword()) {
|
||||||
|
bean.status = UP;
|
||||||
|
bean.msg = `${responseData}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
||||||
|
} else {
|
||||||
|
log.debug("monitor:", `GRPC response [${response.data}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response.data} + "]"`);
|
||||||
|
bean.status = DOWN;
|
||||||
|
bean.msg = `, but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${responseData} + "]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (this.type === "postgres") {
|
} else if (this.type === "postgres") {
|
||||||
let startTime = dayjs().valueOf();
|
let startTime = dayjs().valueOf();
|
||||||
|
|
||||||
@@ -866,6 +897,7 @@ class Monitor extends BeanModel {
|
|||||||
retries = 0;
|
retries = 0;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
if (error?.name === "CanceledError") {
|
if (error?.name === "CanceledError") {
|
||||||
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
bean.msg = `timeout by AbortSignal (${this.timeout}s)`;
|
||||||
} else {
|
} else {
|
||||||
@@ -938,7 +970,6 @@ class Monitor extends BeanModel {
|
|||||||
} else if (bean.status === MAINTENANCE) {
|
} else if (bean.status === MAINTENANCE) {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Under Maintenance | Type: ${this.type}`);
|
||||||
} else {
|
} else {
|
||||||
beatInterval = this.retryInterval;
|
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,90 +0,0 @@
|
|||||||
const { MonitorType } = require("./monitor-type");
|
|
||||||
const { UP, log } = require("../../src/util");
|
|
||||||
const dayjs = require("dayjs");
|
|
||||||
const grpc = require("@grpc/grpc-js");
|
|
||||||
const protojs = require("protobufjs");
|
|
||||||
|
|
||||||
class GrpcKeywordMonitorType extends MonitorType {
|
|
||||||
name = "grpc-keyword";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
async check(monitor, heartbeat, _server) {
|
|
||||||
const startTime = dayjs().valueOf();
|
|
||||||
const service = this.constructGrpcService(this.grpcUrl, this.grpcProtobuf, this.grpcServiceName, this.grpcEnableTls);
|
|
||||||
let response = await this.grpcQuery(service, this.grpcMethod, this.grpcBody);
|
|
||||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
|
||||||
log.debug(this.name, `gRPC response: ${response}`);
|
|
||||||
if (response.length > 50) {
|
|
||||||
response = response.toString().substring(0, 47) + "...";
|
|
||||||
}
|
|
||||||
let keywordFound = response.toString().includes(this.keyword);
|
|
||||||
if (keywordFound !== !this.isInvertKeyword()) {
|
|
||||||
log.debug(this.name, `GRPC response [${response}] + ", but keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response} + "]"`);
|
|
||||||
throw new Error(`keyword [${this.keyword}] is ${keywordFound ? "present" : "not"} in [" + ${response} + "]`);
|
|
||||||
}
|
|
||||||
heartbeat.status = UP;
|
|
||||||
heartbeat.msg = `${response}, keyword [${this.keyword}] ${keywordFound ? "is" : "not"} found`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create gRPC client
|
|
||||||
* @param {string} url grpc Url
|
|
||||||
* @param {string} protobufData grpc ProtobufData
|
|
||||||
* @param {string} serviceName grpc ServiceName
|
|
||||||
* @param {string} enableTls grpc EnableTls
|
|
||||||
* @returns {grpc.Service} grpc Service
|
|
||||||
*/
|
|
||||||
constructGrpcService(url, protobufData, serviceName, enableTls) {
|
|
||||||
const protocObject = protojs.parse(protobufData);
|
|
||||||
const protoServiceObject = protocObject.root.lookupService(serviceName);
|
|
||||||
const Client = grpc.makeGenericClientConstructor({});
|
|
||||||
const credentials = enableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
|
|
||||||
const client = new Client(url, credentials);
|
|
||||||
return protoServiceObject.create((method, requestData, cb) => {
|
|
||||||
const fullServiceName = method.fullName;
|
|
||||||
const serviceFQDN = fullServiceName.split(".");
|
|
||||||
const serviceMethod = serviceFQDN.pop();
|
|
||||||
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
|
|
||||||
log.debug(this.name, `gRPC method ${serviceMethodClientImpl}`);
|
|
||||||
client.makeUnaryRequest(
|
|
||||||
serviceMethodClientImpl,
|
|
||||||
arg => arg,
|
|
||||||
arg => arg,
|
|
||||||
requestData,
|
|
||||||
cb);
|
|
||||||
}, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create gRPC client stib
|
|
||||||
* @param {grpc.Service} service grpc Url
|
|
||||||
* @param {string} method grpc Method
|
|
||||||
* @param {string} body grpc Body
|
|
||||||
* @returns {Promise<string>} Result of gRPC query
|
|
||||||
*/
|
|
||||||
async grpcQuery(service, method, body) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
service[`${method}`](JSON.parse(body), (err, response) => {
|
|
||||||
if (err) {
|
|
||||||
if (err.code !== 1) {
|
|
||||||
reject(`Error in send gRPC ${err.code} ${err.details}`);
|
|
||||||
}
|
|
||||||
log.debug(this.name, `ignoring ${err.code} ${err.details}, as code=1 is considered OK`);
|
|
||||||
resolve(`${err.code} is considered OK because ${err.details}`);
|
|
||||||
}
|
|
||||||
resolve(JSON.stringify(response));
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
reject(`Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
GrpcKeywordMonitorType,
|
|
||||||
};
|
|
@@ -25,29 +25,25 @@ class Feishu extends NotificationProvider {
|
|||||||
|
|
||||||
if (heartbeatJSON["status"] === DOWN) {
|
if (heartbeatJSON["status"] === DOWN) {
|
||||||
let downdata = {
|
let downdata = {
|
||||||
msg_type: "interactive",
|
msg_type: "post",
|
||||||
card: {
|
content: {
|
||||||
config: {
|
post: {
|
||||||
update_multi: false,
|
zh_cn: {
|
||||||
wide_screen_mode: true,
|
title: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
||||||
},
|
content: [
|
||||||
header: {
|
[
|
||||||
title: {
|
{
|
||||||
tag: "plain_text",
|
tag: "text",
|
||||||
content: "UptimeKuma Alert: [Down] " + monitorJSON["name"],
|
text:
|
||||||
|
"[Down] " +
|
||||||
|
heartbeatJSON["msg"] +
|
||||||
|
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
template: "red",
|
|
||||||
},
|
},
|
||||||
elements: [
|
},
|
||||||
{
|
|
||||||
tag: "div",
|
|
||||||
text: {
|
|
||||||
tag: "lark_md",
|
|
||||||
content: getContent(heartbeatJSON),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
await axios.post(notification.feishuWebHookUrl, downdata);
|
await axios.post(notification.feishuWebHookUrl, downdata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
@@ -55,29 +51,25 @@ class Feishu extends NotificationProvider {
|
|||||||
|
|
||||||
if (heartbeatJSON["status"] === UP) {
|
if (heartbeatJSON["status"] === UP) {
|
||||||
let updata = {
|
let updata = {
|
||||||
msg_type: "interactive",
|
msg_type: "post",
|
||||||
card: {
|
content: {
|
||||||
config: {
|
post: {
|
||||||
update_multi: false,
|
zh_cn: {
|
||||||
wide_screen_mode: true,
|
title: "UptimeKuma Alert: [Up] " + monitorJSON["name"],
|
||||||
},
|
content: [
|
||||||
header: {
|
[
|
||||||
title: {
|
{
|
||||||
tag: "plain_text",
|
tag: "text",
|
||||||
content: "UptimeKuma Alert: [UP] " + monitorJSON["name"],
|
text:
|
||||||
|
"[Up] " +
|
||||||
|
heartbeatJSON["msg"] +
|
||||||
|
`\nTime (${heartbeatJSON["timezone"]}): ${heartbeatJSON["localDateTime"]}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
},
|
},
|
||||||
template: "green",
|
|
||||||
},
|
},
|
||||||
elements: [
|
},
|
||||||
{
|
|
||||||
tag: "div",
|
|
||||||
text: {
|
|
||||||
tag: "lark_md",
|
|
||||||
content: getContent(heartbeatJSON),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
await axios.post(notification.feishuWebHookUrl, updata);
|
await axios.post(notification.feishuWebHookUrl, updata);
|
||||||
return okMsg;
|
return okMsg;
|
||||||
@@ -88,17 +80,4 @@ class Feishu extends NotificationProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get content
|
|
||||||
* @param {?object} heartbeatJSON Heartbeat details (For Up/Down only)
|
|
||||||
* @returns {string} Return Successful Message
|
|
||||||
*/
|
|
||||||
function getContent(heartbeatJSON) {
|
|
||||||
return [
|
|
||||||
"**Message**: " + heartbeatJSON["msg"],
|
|
||||||
"**Ping**: " + (heartbeatJSON["ping"] == null ? "N/A" : heartbeatJSON["ping"] + " ms"),
|
|
||||||
`**Time (${heartbeatJSON["timezone"]})**: ${heartbeatJSON["localDateTime"]}`
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = Feishu;
|
module.exports = Feishu;
|
||||||
|
@@ -44,8 +44,9 @@ router.get("/api/entry-page", async (request, response) => {
|
|||||||
response.json(result);
|
response.json(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
router.all("/api/push/:pushToken", async (request, response) => {
|
router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let pushToken = request.params.pushToken;
|
let pushToken = request.params.pushToken;
|
||||||
let msg = request.query.msg || "OK";
|
let msg = request.query.msg || "OK";
|
||||||
let ping = parseFloat(request.query.ping) || null;
|
let ping = parseFloat(request.query.ping) || null;
|
||||||
|
@@ -4,7 +4,7 @@ const { sendInfo } = require("../client");
|
|||||||
const { checkLogin } = require("../util-server");
|
const { checkLogin } = require("../util-server");
|
||||||
const GameResolver = require("gamedig/lib/GameResolver");
|
const GameResolver = require("gamedig/lib/GameResolver");
|
||||||
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
const { testChrome } = require("../monitor-types/real-browser-monitor-type");
|
||||||
const fs = require("fs");
|
const fs = require("fs/promises");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
let gameResolver = new GameResolver();
|
let gameResolver = new GameResolver();
|
||||||
@@ -85,17 +85,17 @@ module.exports.generalSocketHandler = (socket, server) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("getPushExample", (language, callback) => {
|
socket.on("getPushExample", async (language, callback) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let dir = path.join("./extra/push-examples", language);
|
let dir = path.join("./extra/push-examples", language);
|
||||||
let files = fs.readdirSync(dir);
|
let files = await fs.readdir(dir);
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
if (file.startsWith("index.")) {
|
if (file.startsWith("index.")) {
|
||||||
callback({
|
callback({
|
||||||
ok: true,
|
ok: true,
|
||||||
code: fs.readFileSync(path.join(dir, file), "utf8"),
|
code: await fs.readFile(path.join(dir, file), "utf8"),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -113,7 +113,6 @@ class UptimeKumaServer {
|
|||||||
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
|
||||||
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
|
||||||
UptimeKumaServer.monitorTypeList["grpc-keyword"] = new GrpcKeywordMonitorType();
|
|
||||||
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
UptimeKumaServer.monitorTypeList["mongodb"] = new MongodbMonitorType();
|
||||||
|
|
||||||
// Allow all CORS origins (polling) in development
|
// Allow all CORS origins (polling) in development
|
||||||
@@ -518,5 +517,4 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
|
|||||||
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
const { TailscalePing } = require("./monitor-types/tailscale-ping");
|
||||||
const { DnsMonitorType } = require("./monitor-types/dns");
|
const { DnsMonitorType } = require("./monitor-types/dns");
|
||||||
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
const { MqttMonitorType } = require("./monitor-types/mqtt");
|
||||||
const { GrpcKeywordMonitorType } = require("./monitor-types/grpc");
|
|
||||||
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
const { MongodbMonitorType } = require("./monitor-types/mongodb");
|
||||||
|
@@ -13,10 +13,13 @@ const postgresConParse = require("pg-connection-string").parse;
|
|||||||
const mysql = require("mysql2");
|
const mysql = require("mysql2");
|
||||||
const { NtlmClient } = require("axios-ntlm");
|
const { NtlmClient } = require("axios-ntlm");
|
||||||
const { Settings } = require("./settings");
|
const { Settings } = require("./settings");
|
||||||
|
const grpc = require("@grpc/grpc-js");
|
||||||
|
const protojs = require("protobufjs");
|
||||||
const radiusClient = require("node-radius-client");
|
const radiusClient = require("node-radius-client");
|
||||||
const redis = require("redis");
|
const redis = require("redis");
|
||||||
const oidc = require("openid-client");
|
const oidc = require("openid-client");
|
||||||
const tls = require("tls");
|
const tls = require("tls");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dictionaries: {
|
dictionaries: {
|
||||||
@@ -917,6 +920,64 @@ module.exports.timeObjectToLocal = (obj, timezone = undefined) => {
|
|||||||
return timeObjectConvertTimezone(obj, timezone, false);
|
return timeObjectConvertTimezone(obj, timezone, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create gRPC client stib
|
||||||
|
* @param {object} options from gRPC client
|
||||||
|
* @returns {Promise<object>} Result of gRPC query
|
||||||
|
*/
|
||||||
|
module.exports.grpcQuery = async (options) => {
|
||||||
|
const { grpcUrl, grpcProtobufData, grpcServiceName, grpcEnableTls, grpcMethod, grpcBody } = options;
|
||||||
|
const protocObject = protojs.parse(grpcProtobufData);
|
||||||
|
const protoServiceObject = protocObject.root.lookupService(grpcServiceName);
|
||||||
|
const Client = grpc.makeGenericClientConstructor({});
|
||||||
|
const credentials = grpcEnableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure();
|
||||||
|
const client = new Client(
|
||||||
|
grpcUrl,
|
||||||
|
credentials
|
||||||
|
);
|
||||||
|
const grpcService = protoServiceObject.create(function (method, requestData, cb) {
|
||||||
|
const fullServiceName = method.fullName;
|
||||||
|
const serviceFQDN = fullServiceName.split(".");
|
||||||
|
const serviceMethod = serviceFQDN.pop();
|
||||||
|
const serviceMethodClientImpl = `/${serviceFQDN.slice(1).join(".")}/${serviceMethod}`;
|
||||||
|
log.debug("monitor", `gRPC method ${serviceMethodClientImpl}`);
|
||||||
|
client.makeUnaryRequest(
|
||||||
|
serviceMethodClientImpl,
|
||||||
|
arg => arg,
|
||||||
|
arg => arg,
|
||||||
|
requestData,
|
||||||
|
cb);
|
||||||
|
}, false, false);
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
try {
|
||||||
|
return grpcService[`${grpcMethod}`](JSON.parse(grpcBody), function (err, response) {
|
||||||
|
const responseData = JSON.stringify(response);
|
||||||
|
if (err) {
|
||||||
|
return resolve({
|
||||||
|
code: err.code,
|
||||||
|
errorMessage: err.details,
|
||||||
|
data: ""
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug("monitor:", `gRPC response: ${JSON.stringify(response)}`);
|
||||||
|
return resolve({
|
||||||
|
code: 1,
|
||||||
|
errorMessage: "",
|
||||||
|
data: responseData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return resolve({
|
||||||
|
code: -1,
|
||||||
|
errorMessage: `Error ${err}. Please review your gRPC configuration option. The service name must not include package name value, and the method name must follow camelCase format`,
|
||||||
|
data: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of SHA256 fingerprints for all known root certificates.
|
* Returns an array of SHA256 fingerprints for all known root certificates.
|
||||||
* @returns {Set} A set of SHA256 fingerprints.
|
* @returns {Set} A set of SHA256 fingerprints.
|
||||||
@@ -1002,3 +1063,9 @@ module.exports.axiosAbortSignal = (timeoutMs) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.fileExists = (file) => {
|
||||||
|
return fs.promises.access(file, fs.constants.F_OK)
|
||||||
|
.then(() => true)
|
||||||
|
.catch(() => false);
|
||||||
|
};
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
<label for="smspartner-key" class="form-label">{{ $t("API Key") }}</label>
|
<label for="smspartner-key" class="form-label">{{ $t("API Key") }}</label>
|
||||||
<HiddenInput id="smspartner-key" v-model="$parent.notification.smspartnerApikey" :required="true" autocomplete="new-password"></HiddenInput>
|
<HiddenInput id="smspartner-key" v-model="$parent.notification.smspartnerApikey" :required="true" autocomplete="new-password"></HiddenInput>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<i18n-t keypath="smspartnerApiurl" tag="div" class="form-text">
|
<i18n-t keypath="smspartnerApiurl" as="div" class="form-text">
|
||||||
<a href="https://my.smspartner.fr/dashboard/api" target="_blank">my.smspartner.fr/dashboard/api</a>
|
<a href="https://my.smspartner.fr/dashboard/api" target="_blank">my.smspartner.fr/dashboard/api</a>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<label for="smspartner-phone-number" class="form-label">{{ $t("smspartnerPhoneNumber") }}</label>
|
<label for="smspartner-phone-number" class="form-label">{{ $t("smspartnerPhoneNumber") }}</label>
|
||||||
<input id="smspartner-phone-number" v-model="$parent.notification.smspartnerPhoneNumber" type="text" minlength="3" maxlength="20" pattern="^[\d+,]+$" class="form-control" required>
|
<input id="smspartner-phone-number" v-model="$parent.notification.smspartnerPhoneNumber" type="text" minlength="3" maxlength="20" pattern="^[\d+,]+$" class="form-control" required>
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<i18n-t keypath="smspartnerPhoneNumberHelptext" tag="div" class="form-text">
|
<i18n-t keypath="smspartnerPhoneNumberHelptext" as="div" class="form-text">
|
||||||
<code>+336xxxxxxxx</code>
|
<code>+336xxxxxxxx</code>
|
||||||
<code>+496xxxxxxxx</code>
|
<code>+496xxxxxxxx</code>
|
||||||
<code>,</code>
|
<code>,</code>
|
||||||
|
@@ -211,20 +211,6 @@ export default {
|
|||||||
@import "../assets/vars.scss";
|
@import "../assets/vars.scss";
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
&:hover {
|
|
||||||
background-color: $primary;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
.dark & {
|
|
||||||
background-color: $primary;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: $highlight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.status-page {
|
&.status-page {
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
@@ -370,10 +370,9 @@
|
|||||||
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
<label for="jsonPath" class="form-label">{{ $t("Json Query") }}</label>
|
||||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" required>
|
||||||
|
|
||||||
<i18n-t tag="div" class="form-text" keypath="jsonQueryDescription">
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<a href="https://jsonata.org/">jsonata.org</a>
|
<div class="form-text" v-html="$t('jsonQueryDescription')">
|
||||||
<a href="https://try.jsonata.org/">{{ $t('here') }}</a>
|
</div>
|
||||||
</i18n-t>
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||||
|
Reference in New Issue
Block a user