Merge pull request #1633 from OneUptime/merge-dir

Add Log Monitors
This commit is contained in:
Simon Larsen
2024-08-06 22:10:56 -06:00
committed by GitHub
33 changed files with 935 additions and 317 deletions

View File

@@ -37,6 +37,12 @@ const OSSFriends: OSSFriend[] = [
"PostHog is open-source product analytics, built for developers.",
repositoryUrl: URL.fromString("https://github.com/PostHog/posthog"),
},
{
name: "Twenty",
description:
"Twenty is building an open-source modern CRM. It's alternative to Salesforce and HubSpot.",
repositoryUrl: URL.fromString("https://github.com/twentyhq/twenty"),
},
];
export default OSSFriends;

View File

@@ -5,16 +5,6 @@ export interface Review {
}
const reviews: Review[] = [
{
name: "Alice",
text: "OneUptime has been a game-changer for our team. The alerts are timely and the status page keeps our customers informed.",
title: "Game-changer!",
},
{
name: "Bob",
text: "The incident management feature is top-notch. It has streamlined our response process significantly.",
title: "Top-notch incident management",
},
{
name: "Charlie",
text: "I love the on-call rotation setup. It has made scheduling so much easier.",
@@ -75,6 +65,16 @@ const reviews: Review[] = [
text: "The log analysis feature is very detailed and insightful.",
title: "Detailed log analysis",
},
{
name: "Anderson, GK2 Cloud",
text: "Thanks for building OneUptime, it really is fantastic. We are getting more excited every day!",
title: "OneUptime is fantastic!",
},
{
name: "Reg, Skillable",
text: "We use OneUptime to reliably monitor endpoint availability globally, and it delivers.",
title: "OneUptime delivers!",
},
{
name: "Oscar",
text: "OneUptime has made our monitoring process much more efficient.",

View File

@@ -111,7 +111,7 @@
</svg>
<span><strong class="font-semibold text-gray-900">
We don't store or train on your code.
We don't see, store or train on your code.
</strong>
Regardless whether you are on the free tier or enterprise tier, we don't store or train on your code. No part of your code is sent to us.
</span>

View File

@@ -67,6 +67,9 @@ import logger from "CommonServer/Utils/Logger";
import "./Jobs/Probe/SendOwnerAddedNotification";
import "./Jobs/Probe/UpdateConnectionStatus";
// Telemetry Monitors.
import "./Jobs/TelemetryMonitor/MonitorTelemetryMonitor";
const WorkersFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
try {

View File

@@ -186,8 +186,10 @@ const monitorLogs: MonitorLogsFunction = async (data: {
query.body = new Search(logQuery.body);
}
if (logQuery.severityText) {
query.severityText = logQuery.severityText;
if (logQuery.severityTexts && logQuery.severityTexts.length > 0) {
query.severityText = QueryHelper.any(
logQuery.severityTexts as Array<string>,
);
}
if (logQuery.telemetryServiceIds && logQuery.telemetryServiceIds.length > 0) {

View File

@@ -849,6 +849,49 @@ export default class OneUptimeDate {
return months;
}
public static convertSecondsToDaysHoursMinutesAndSeconds(
seconds: number,
): string {
// should output 2 days, 3 hours, 4 minutes and 5 seconds. If the days are 0, it should not show the days. If the hours are 0, it should not show the hours. If the minutes are 0, it should not show the minutes. If the seconds are 0, it should not show the seconds.
const days: number = Math.floor(seconds / (24 * 60 * 60));
const hours: number = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60));
const minutes: number = Math.floor((seconds % (60 * 60)) / 60);
const secs: number = seconds % 60;
let formattedString: string = "";
if (days > 0) {
formattedString += days + " days";
}
if (hours > 0) {
if (formattedString.length > 0) {
formattedString += ", ";
}
formattedString += hours + " hours";
}
if (minutes > 0) {
if (formattedString.length > 0) {
formattedString += ", ";
}
formattedString += minutes + " minutes";
}
if (secs > 0) {
if (formattedString.length > 0) {
formattedString += ", ";
}
formattedString += secs + " seconds";
}
return formattedString;
}
public static convertMinutesToDaysHoursAndMinutes(minutes: number): string {
// should output 2 days, 3 hours and 4 minutes. If the days are 0, it should not show the days. If the hours are 0, it should not show the hours. If the minutes are 0, it should not show the minutes.

View File

@@ -196,16 +196,6 @@ export default class MonitorStep extends DatabaseProperty {
return "Monitor Destination is required.";
}
if (monitorType === MonitorType.Logs) {
if (!value.data.logMonitor) {
return "Log Monitor is required";
}
if (!value.data.logMonitor.lastXSecondsOfLogs) {
return "Monitor Last Minutes of Logs is required.";
}
}
if (
!value.data.customCode &&
(monitorType === MonitorType.CustomJavaScriptCode ||
@@ -265,7 +255,9 @@ export default class MonitorStep extends DatabaseProperty {
screenSizeTypes: this.data.screenSizeTypes || undefined,
browserTypes: this.data.browserTypes || undefined,
logMonitor: this.data.logMonitor
? MonitorStepLogMonitorUtil.toJSON(this.data.logMonitor)
? MonitorStepLogMonitorUtil.toJSON(
this.data.logMonitor || MonitorStepLogMonitorUtil.getDefault(),
)
: undefined,
},
});
@@ -360,6 +352,10 @@ export default class MonitorStep extends DatabaseProperty {
: undefined,
}) as any;
if (monitorStep.data && !monitorStep.data?.logMonitor) {
monitorStep.data.logMonitor = MonitorStepLogMonitorUtil.getDefault();
}
return monitorStep;
}

View File

@@ -1,3 +1,9 @@
import Log from "../../Models/AnalyticsModels/Log";
import InBetween from "../BaseDatabase/InBetween";
import Includes from "../BaseDatabase/Includes";
import Query from "../BaseDatabase/Query";
import Search from "../BaseDatabase/Search";
import OneUptimeDate from "../Date";
import Dictionary from "../Dictionary";
import { JSONObject } from "../JSON";
import LogSeverity from "../Log/LogSeverity";
@@ -6,17 +12,59 @@ import ObjectID from "../ObjectID";
export default interface MonitorStepLogMonitor {
attributes: Dictionary<string | number | boolean>;
body: string;
severityText: Array<LogSeverity>;
severityTexts: Array<LogSeverity>;
telemetryServiceIds: Array<ObjectID>;
lastXSecondsOfLogs: number;
}
export class MonitorStepLogMonitorUtil {
public static toQuery(
monitorStepLogMonitor: MonitorStepLogMonitor,
): Query<Log> {
const query: Query<Log> = {};
if (
monitorStepLogMonitor.telemetryServiceIds &&
monitorStepLogMonitor.telemetryServiceIds.length > 0
) {
query.serviceId = new Includes(monitorStepLogMonitor.telemetryServiceIds);
}
if (
monitorStepLogMonitor.attributes &&
Object.keys(monitorStepLogMonitor.attributes).length > 0
) {
query.attributes = monitorStepLogMonitor.attributes;
}
if (
monitorStepLogMonitor.severityTexts &&
monitorStepLogMonitor.severityTexts.length > 0
) {
query.severityText = new Includes(monitorStepLogMonitor.severityTexts);
}
if (monitorStepLogMonitor.body) {
query.body = new Search(monitorStepLogMonitor.body);
}
if (monitorStepLogMonitor.lastXSecondsOfLogs) {
const endDate: Date = OneUptimeDate.getCurrentDate();
const startDate: Date = OneUptimeDate.addRemoveSeconds(
endDate,
monitorStepLogMonitor.lastXSecondsOfLogs * -1,
);
query.time = new InBetween(startDate, endDate);
}
return query;
}
public static getDefault(): MonitorStepLogMonitor {
return {
attributes: {},
body: "",
severityText: [],
severityTexts: [],
telemetryServiceIds: [],
lastXSecondsOfLogs: 60,
};
@@ -27,7 +75,7 @@ export class MonitorStepLogMonitorUtil {
attributes:
(json["attributes"] as Dictionary<string | number | boolean>) || {},
body: json["body"] as string,
severityText: json["severityText"] as Array<LogSeverity>,
severityTexts: json["severityTexts"] as Array<LogSeverity>,
telemetryServiceIds: ObjectID.fromJSONArray(
json["telemetryServiceIds"] as Array<JSONObject>,
),
@@ -39,8 +87,8 @@ export class MonitorStepLogMonitorUtil {
return {
attributes: monitor.attributes,
body: monitor.body,
severityText: monitor.severityText,
telemetryServiceId: ObjectID.toJSONArray(monitor.telemetryServiceIds),
severityTexts: monitor.severityTexts,
telemetryServiceIds: ObjectID.toJSONArray(monitor.telemetryServiceIds),
lastXSecondsOfLogs: monitor.lastXSecondsOfLogs,
};
}

View File

@@ -109,18 +109,19 @@ export class MonitorTypeHelper {
title: "Logs",
description: "This monitor type lets you monitor logs from any source.",
},
{
monitorType: MonitorType.Metrics,
title: "Metrics",
description:
"This monitor type lets you monitor metrics from any source.",
},
{
monitorType: MonitorType.Traces,
title: "Traces",
description:
"This monitor type lets you monitor traces from any source.",
},
// ,
// {
// monitorType: MonitorType.Metrics,
// title: "Metrics",
// description:
// "This monitor type lets you monitor metrics from any source.",
// },
// {
// monitorType: MonitorType.Traces,
// title: "Traces",
// description:
// "This monitor type lets you monitor traces from any source.",
// },
];
return monitorTypeProps;

View File

@@ -48,6 +48,10 @@ export default class ObjectID extends DatabaseProperty {
}
public static toJSONArray(ids: Array<ObjectID>): Array<JSONObject> {
if (!ids || ids.length === 0) {
return [];
}
return ids.map((id: ObjectID) => {
if (typeof id === "string") {
id = new ObjectID(id);

View File

@@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1722892318363 implements MigrationInterface {
public name = "MigrationName1722892318363";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "Monitor" ADD "telemetryMonitorNextMonitorAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`ALTER TABLE "Monitor" ADD "telemetryMonitorLastMonitorAt" TIMESTAMP WITH TIME ZONE`,
);
await queryRunner.query(
`ALTER TABLE "Incident" ADD "telemetryQuery" jsonb`,
);
await queryRunner.query(
`CREATE INDEX "IDX_540a4857bf04eabc59775ca210" ON "Monitor" ("telemetryMonitorNextMonitorAt") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_02bae6cdc6d5b3092ac393fbb3" ON "Monitor" ("telemetryMonitorLastMonitorAt") `,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX "public"."IDX_02bae6cdc6d5b3092ac393fbb3"`,
);
await queryRunner.query(
`DROP INDEX "public"."IDX_540a4857bf04eabc59775ca210"`,
);
await queryRunner.query(
`ALTER TABLE "Incident" DROP COLUMN "telemetryQuery"`,
);
await queryRunner.query(
`ALTER TABLE "Monitor" DROP COLUMN "telemetryMonitorLastMonitorAt"`,
);
await queryRunner.query(
`ALTER TABLE "Monitor" DROP COLUMN "telemetryMonitorNextMonitorAt"`,
);
}
}

View File

@@ -37,6 +37,7 @@ import { MigrationName1721754545771 } from "./1721754545771-MigrationName";
import { MigrationName1721779190475 } from "./1721779190475-MigrationName";
import { MigrationName1722031205897 } from "./1722031205897-MigrationName";
import { MigrationName1722543640526 } from "./1722543640526-MigrationName";
import { MigrationName1722892318363 } from "./1722892318363-MigrationName";
export default [
InitialMigration,
@@ -78,4 +79,5 @@ export default [
MigrationName1721779190475,
MigrationName1722031205897,
MigrationName1722543640526,
MigrationName1722892318363,
];

View File

@@ -26,7 +26,7 @@ export interface ComponentProps {
initialValue?: Dictionary<string | boolean | number>;
keyPlaceholder?: string;
valuePlaceholder?: string;
addButtonSuffix?: string;
addButtonSuffix?: string | undefined;
valueTypes?: Array<ValueType>; // by default it'll be Text
autoConvertValueTypes?: boolean | undefined;
keys?: Array<string> | undefined;

View File

@@ -48,7 +48,7 @@ export const DefaultValidateFunction: DefaultValidateFunctionType = (
return {};
};
export interface BaseComponentProps<T extends GenericObject> {
export interface BaseComponentProps<T> {
submitButtonStyleType?: ButtonStyleType | undefined;
initialValues?: FormValues<T> | undefined;
onValidate?: undefined | ((values: FormValues<T>) => JSONObject);

View File

@@ -8,6 +8,7 @@ export interface ComponentProps {
sideLink?: FormFieldSideLink | undefined;
description?: string | ReactElement | undefined;
isHeading?: boolean | undefined;
hideOptionalLabel?: boolean | undefined;
}
const FieldLabelElement: FunctionComponent<ComponentProps> = (
@@ -23,7 +24,7 @@ const FieldLabelElement: FunctionComponent<ComponentProps> = (
<span>
{props.title}{" "}
<span className="text-gray-400 text-xs">
{props.required ? "" : "(Optional)"}
{props.required || props.hideOptionalLabel ? "" : "(Optional)"}
</span>
</span>
{props.sideLink && props.sideLink?.text && props.sideLink?.url && (

View File

@@ -1,9 +1,11 @@
import Dictionary from "Common/Types/Dictionary";
import { GetReactElementFunction } from "../../../Types/FunctionTypes";
import CategoryCheckbox from "../../CategoryCheckbox/Index";
import CheckboxElement, {
CategoryCheckboxValue,
} from "../../Checkbox/Checkbox";
import CodeEditor from "../../CodeEditor/CodeEditor";
import DictionaryForm from "../../Dictionary/Dictionary";
import Dropdown, { DropdownValue } from "../../Dropdown/Dropdown";
import FilePicker from "../../FilePicker/FilePicker";
import Input, { InputType } from "../../Input/Input";
@@ -238,6 +240,7 @@ const FormField: <T extends GenericObject>(
description={getFieldDescription()}
sideLink={props.field.sideLink}
required={required}
hideOptionalLabel={props.field.hideOptionalLabel}
isHeading={props.field.styleType === FormFieldStyleType.Heading}
/>
)}
@@ -317,6 +320,26 @@ const FormField: <T extends GenericObject>(
/>
)}
{props.field.fieldType === FormFieldSchemaType.Dictionary && (
<DictionaryForm
keys={props.field.jsonKeysForDictionary}
addButtonSuffix={props.field.title}
keyPlaceholder={"Key"}
valuePlaceholder={"Value"}
autoConvertValueTypes={true}
initialValue={
props.currentValues &&
(props.currentValues as any)[props.fieldName]
? (props.currentValues as any)[props.fieldName]
: props.field.defaultValue || {}
}
onChange={(value: Dictionary<string | number | boolean>) => {
props.field.onChange && props.field.onChange(value);
props.setFieldValue(props.fieldName, value);
}}
/>
)}
{props.field.fieldType === FormFieldSchemaType.RadioButton && (
<RadioButtons
error={props.touched && props.error ? props.error : undefined}

View File

@@ -97,4 +97,9 @@ export default interface Field<TEntity> {
// set this to true if you want to show this field in the form even when the form is in edit mode.
doNotShowWhenEditing?: boolean | undefined;
doNotShowWhenCreating?: boolean | undefined;
//
jsonKeysForDictionary?: Array<string> | undefined;
hideOptionalLabel?: boolean | undefined;
}

View File

@@ -34,6 +34,7 @@ enum FormFieldSchemaType {
CustomComponent = "CustomComponent",
Checkbox = "Checkbox",
CategoryCheckbox = "CategoryCheckbox",
Dictionary = "Dictionary",
}
export default FormFieldSchemaType;

View File

@@ -1,10 +1,10 @@
import { FILE_URL } from "../Config";
import URL from "Common/Types/API/URL";
import Route from "Common/Types/API/Route";
import ObjectID from "Common/Types/ObjectID";
import { FileRoute } from "Common/ServiceRoute";
export default class FileUtil {
public static getFileURL(fileId: ObjectID): URL {
return URL.fromString(FILE_URL.toString())
public static getFileRoute(fileId: ObjectID): Route {
return Route.fromString(FileRoute.toString())
.addRoute("/image")
.addRoute(`/${fileId.toString()}`);
}

View File

@@ -1,11 +1,14 @@
import LogSeverity from "Common/Types/Log/LogSeverity";
import MonitorStepLogMonitor from "Common/Types/Monitor/MonitorStepLogMonitor";
import FiltersForm from "CommonUI/src/Components/Filters/FiltersForm";
import FieldType from "CommonUI/src/Components/Types/FieldType";
import Query from "CommonUI/src/Utils/BaseDatabase/Query";
import DropdownUtil from "CommonUI/src/Utils/Dropdown";
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
import React, { FunctionComponent, ReactElement } from "react";
import BasicForm from "CommonUI/src/Components/Forms/BasicForm";
import LogSeverity from "Common/Types/Log/LogSeverity";
import DropdownUtil from "CommonUI/src/Utils/Dropdown";
import FormFieldSchemaType from "CommonUI/src/Components/Forms/Types/FormFieldSchemaType";
import Button, { ButtonStyleType } from "CommonUI/src/Components/Button/Button";
import FieldLabelElement from "CommonUI/src/Components/Forms/Fields/FieldLabel";
import HorizontalRule from "CommonUI/src/Components/HorizontalRule/HorizontalRule";
import LogMonitorPreview from "../../../Monitor/LogMonitor/LogMonitorPreview";
export interface ComponentProps {
monitorStepLogMonitor: MonitorStepLogMonitor;
@@ -19,105 +22,179 @@ export interface ComponentProps {
const LogMonitorStepForm: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [monitorStepLogMonitor, setMonitorStepLogMonitor] =
React.useState<MonitorStepLogMonitor>(props.monitorStepLogMonitor);
let showAdvancedOptionsByDefault: boolean = false;
if (
monitorStepLogMonitor.attributes ||
monitorStepLogMonitor.severityTexts ||
monitorStepLogMonitor.telemetryServiceIds
) {
showAdvancedOptionsByDefault = true;
}
const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(
showAdvancedOptionsByDefault,
);
return (
<FiltersForm<MonitorStepLogMonitor>
id="logs-filter"
showFilter={true}
filterData={props.monitorStepLogMonitor}
onFilterChanged={(filterData: Query<MonitorStepLogMonitor>) => {
props.onMonitorStepLogMonitorChanged(
filterData as MonitorStepLogMonitor,
);
}}
filters={[
{
key: "body",
type: FieldType.Text,
title: "Search Log Body",
},
{
key: "lastXSecondsOfLogs",
type: FieldType.Dropdown,
filterDropdownOptions: [
{
label: "Last 5 seconds",
value: 5,
<div>
<BasicForm
id="logs-filter"
hideSubmitButton={true}
initialValues={monitorStepLogMonitor}
onChange={(values: MonitorStepLogMonitor) => {
setMonitorStepLogMonitor(values);
props.onMonitorStepLogMonitorChanged(values);
}}
fields={[
{
field: {
body: true,
},
{
label: "Last 10 seconds",
value: 10,
fieldType: FormFieldSchemaType.Text,
title: "Monitor Logs that include this text",
description:
"This monitor will filter all the logs that include this text.",
hideOptionalLabel: true,
},
{
field: {
lastXSecondsOfLogs: true,
},
{
label: "Last 30 seconds",
value: 30,
defaultValue: 60,
fieldType: FormFieldSchemaType.Dropdown,
dropdownOptions: [
{
label: "Last 5 seconds",
value: 5,
},
{
label: "Last 10 seconds",
value: 10,
},
{
label: "Last 30 seconds",
value: 30,
},
{
label: "Last 1 minute",
value: 60,
},
{
label: "Last 5 minutes",
value: 300,
},
{
label: "Last 15 minutes",
value: 900,
},
{
label: "Last 30 minutes",
value: 1800,
},
{
label: "Last 1 hour",
value: 3600,
},
{
label: "Last 6 hours",
value: 21600,
},
{
label: "Last 12 hours",
value: 43200,
},
{
label: "Last 24 hours",
value: 86400,
},
],
title: "Monitor Logs for (time)",
description:
"We will fetch all the logs that were generated in the last X time.",
hideOptionalLabel: true,
},
{
field: {
severityTexts: true,
},
{
label: "Last 1 minute",
value: 60,
dropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(LogSeverity),
fieldType: FormFieldSchemaType.MultiSelectDropdown,
title: "Log Severity",
description: "Select the severity of the logs you want to monitor.",
hideOptionalLabel: true,
showIf: () => {
return showAdvancedOptions;
},
{
label: "Last 5 minutes",
value: 300,
},
{
field: {
telemetryServiceIds: true,
},
{
label: "Last 15 minutes",
value: 900,
fieldType: FormFieldSchemaType.MultiSelectDropdown,
dropdownOptions: props.telemetryServices.map(
(telemetryService: TelemetryService) => {
return {
label: telemetryService.name!,
value: telemetryService.id?.toString() || "",
};
},
),
title: "Filter by Telemetry Service",
description: "Select the telemetry services you want to monitor.",
hideOptionalLabel: true,
showIf: () => {
return showAdvancedOptions;
},
{
label: "Last 30 minutes",
value: 1800,
},
{
field: {
attributes: true,
},
{
label: "Last 1 hour",
value: 3600,
fieldType: FormFieldSchemaType.Dictionary,
title: "Filter by Attributes",
jsonKeysForDictionary: props.attributeKeys,
description:
"You can filter the logs based on the attributes that are attached to the logs.",
hideOptionalLabel: true,
showIf: () => {
return showAdvancedOptions;
},
{
label: "Last 6 hours",
value: 21600,
},
{
label: "Last 12 hours",
value: 43200,
},
{
label: "Last 24 hours",
value: 86400,
},
],
title: "Monitor Logs for Last",
isAdvancedFilter: true,
},
{
key: "severityText",
filterDropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(LogSeverity),
type: FieldType.MultiSelectDropdown,
title: "Log Severity",
isAdvancedFilter: true,
},
{
key: "telemetryServiceIds",
type: FieldType.MultiSelectDropdown,
filterDropdownOptions: props.telemetryServices.map(
(telemetryService: TelemetryService) => {
return {
label: telemetryService.name!,
value: telemetryService.id?.toString() || "",
};
},
),
title: "Filter by Telemetry Service",
isAdvancedFilter: true,
},
{
key: "attributes",
type: FieldType.JSON,
title: "Filter by Attributes",
jsonKeys: props.attributeKeys,
isAdvancedFilter: true,
},
]}
/>
},
]}
/>
<div className="-ml-3">
<Button
buttonStyle={ButtonStyleType.SECONDARY_LINK}
title={
showAdvancedOptions
? "Hide Advanced Options"
: "Show Advanced Options"
}
onClick={() => {
return setShowAdvancedOptions(!showAdvancedOptions);
}}
/>
</div>
<div>
<HorizontalRule />
<FieldLabelElement
title={"Logs Preview"}
description={
"Here is the preview of the logs that will be monitored based on the filters you have set above."
}
hideOptionalLabel={true}
isHeading={true}
/>
<div className="mt-5 mb-5">
<LogMonitorPreview monitorStepLogMonitor={monitorStepLogMonitor} />
</div>
</div>
</div>
);
};

View File

@@ -506,13 +506,6 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
{props.monitorType === MonitorType.Logs && (
<div className="mt-5">
<FieldLabelElement
title={"Log Query"}
description={
"Please select the subset of logs you want to monitor."
}
required={true}
/>
<LogMonitorStepForm
monitorStepLogMonitor={
monitorStep.data?.logMonitor ||
@@ -625,14 +618,17 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
<div className="mt-5">
{props.monitorType !== MonitorType.IncomingRequest && (
<FieldLabelElement
title="Monitor Criteria"
isHeading={true}
description={
"Add Monitoring Criteria for this monitor. Monitor different properties."
}
required={true}
/>
<>
<HorizontalRule />
<FieldLabelElement
title="Monitor Criteria"
isHeading={true}
description={
"Add Monitoring Criteria for this monitor. Monitor different properties."
}
required={true}
/>
</>
)}
<MonitorCriteriaElement
monitorType={props.monitorType}

View File

@@ -62,7 +62,7 @@ const DashboardUserProfile: FunctionComponent<ComponentProps> = (
<HeaderIconDropdownButton
iconImageUrl={
profilePictureId
? FileUtil.getFileURL(profilePictureId)
? FileUtil.getFileRoute(profilePictureId)
: BlankProfilePic
}
name="User Profile"

View File

@@ -25,29 +25,45 @@ export interface ComponentProps {
spanIds?: Array<string> | undefined;
showFilters?: boolean | undefined;
noLogsMessage?: string | undefined;
logQuery?: Query<Log> | undefined;
limit?: number | undefined;
}
const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const query: Query<Log> = {};
type RefreshQueryFunction = () => Query<Log>;
if (props.telemetryServiceIds && props.telemetryServiceIds.length > 0) {
query.serviceId = new Includes(props.telemetryServiceIds);
}
const refreshQuery: RefreshQueryFunction = (): Query<Log> => {
let query: Query<Log> = {};
if (props.traceIds && props.traceIds.length > 0) {
query.traceId = new Includes(props.traceIds);
}
if (props.telemetryServiceIds && props.telemetryServiceIds.length > 0) {
query.serviceId = new Includes(props.telemetryServiceIds);
}
if (props.spanIds && props.spanIds.length > 0) {
query.spanId = new Includes(props.spanIds);
}
if (props.traceIds && props.traceIds.length > 0) {
query.traceId = new Includes(props.traceIds);
}
if (props.spanIds && props.spanIds.length > 0) {
query.spanId = new Includes(props.spanIds);
}
if (props.logQuery) {
query = {
...query,
...props.logQuery,
};
}
return query;
};
const [logs, setLogs] = React.useState<Array<Log>>([]);
const [error, setError] = React.useState<string>("");
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [filterOptions, setFilterOptions] = React.useState<Query<Log>>(query);
const [filterOptions, setFilterOptions] =
React.useState<Query<Log>>(refreshQuery());
const select: Select<Log> = {
body: true,
@@ -72,6 +88,15 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
});
}, [filterOptions]);
useEffect(() => {
setFilterOptions(refreshQuery());
}, [
props.telemetryServiceIds,
props.traceIds,
props.spanIds,
props.logQuery,
]);
const fetchItems: PromiseVoidFunction = async (): Promise<void> => {
setError("");
setIsLoading(true);
@@ -80,7 +105,7 @@ const DashboardLogsViewer: FunctionComponent<ComponentProps> = (
const listResult: ListResult<Log> = await AnalyticsModelAPI.getList<Log>({
modelType: Log,
query: getQuery(),
limit: LIMIT_PER_PROJECT,
limit: props.limit || LIMIT_PER_PROJECT,
skip: 0,
select: select,
sort: {

View File

@@ -0,0 +1,41 @@
import React, { FunctionComponent, ReactElement, useEffect } from "react";
import MonitorStepLogMonitor, {
MonitorStepLogMonitorUtil,
} from "Common/Types/Monitor/MonitorStepLogMonitor";
import DashboardLogsViewer from "../../Logs/LogsViewer";
import Query from "Common/Types/BaseDatabase/Query";
import Log from "Common/Models/AnalyticsModels/Log";
export interface ComponentProps {
monitorStepLogMonitor: MonitorStepLogMonitor | undefined;
}
const LogMonitorPreview: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
type RefreshQueryFunction = () => Query<Log>;
const refreshQuery: RefreshQueryFunction = (): Query<Log> => {
if (!props.monitorStepLogMonitor) {
return {};
}
return MonitorStepLogMonitorUtil.toQuery(props.monitorStepLogMonitor);
};
const [logQuery, setLogQuery] = React.useState<Query<Log>>(refreshQuery());
useEffect(() => {
setLogQuery(refreshQuery());
}, [props.monitorStepLogMonitor]);
return (
<DashboardLogsViewer
id="logs-preview"
logQuery={logQuery}
limit={10}
noLogsMessage="No logs found"
/>
);
};
export default LogMonitorPreview;

View File

@@ -16,6 +16,21 @@ import React, {
useEffect,
useState,
} from "react";
import ComponentLoader from "CommonUI/src/Components/ComponentLoader/ComponentLoader";
import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage";
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
import { JSONObject } from "Common/Types/JSON";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import ListResult from "CommonUI/src/Utils/BaseDatabase/ListResult";
import ModelAPI from "CommonUI/src/Utils/ModelAPI/ModelAPI";
import DashboardNavigation from "../../../Utils/Navigation";
import { LIMIT_PER_PROJECT } from "Common/Types/Database/LimitMax";
import SortOrder from "Common/Types/BaseDatabase/SortOrder";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import API from "CommonUI/src/Utils/API/API";
import Includes from "Common/Types/BaseDatabase/Includes";
import OneUptimeDate from "Common/Types/Date";
import TelemetryServicesElement from "../../TelemetryService/TelemetryServiceElements";
export interface ComponentProps {
monitorStatusOptions: Array<MonitorStatus>;
@@ -25,138 +40,297 @@ export interface ComponentProps {
onCallPolicyOptions: Array<OnCallDutyPolicy>;
}
export interface LogMonitorStepView {
body: string | undefined;
severityTexts: Array<string> | undefined;
attributes: JSONObject | undefined;
telemetryServices: Array<TelemetryService> | undefined;
lastXSecondsOfLogs: number | undefined;
}
const MonitorStepElement: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const [requestDetailsFields, setRequestDetailsFields] = useState<
Array<Field<MonitorStepType>>
>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | undefined>(undefined);
const [telemetryServices, setTelemetryServices] = useState<
Array<TelemetryService> | undefined
>(undefined);
// this field is used for most monitor types
let fields: Array<Field<MonitorStepType>> = [];
let logFields: Array<Field<LogMonitorStepView>> = [];
const logMonitorDetailView: LogMonitorStepView = {
body: undefined,
severityTexts: undefined,
attributes: undefined,
telemetryServices: undefined,
lastXSecondsOfLogs: undefined,
};
const fetchTelemetryServices: PromiseVoidFunction =
async (): Promise<void> => {
const telemetryServicesResult: ListResult<TelemetryService> =
await ModelAPI.getList<TelemetryService>({
modelType: TelemetryService,
query: {
projectId: DashboardNavigation.getProjectId(),
_id: new Includes(
props.monitorStep.data?.logMonitor?.telemetryServiceIds || [],
),
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
_id: true,
name: true,
serviceColor: true,
},
sort: {
name: SortOrder.Ascending,
},
});
if (telemetryServicesResult instanceof HTTPErrorResponse) {
throw telemetryServicesResult;
}
setTelemetryServices(telemetryServicesResult.data);
};
const loadComponent: PromiseVoidFunction = async (): Promise<void> => {
setIsLoading(true);
try {
if (props.monitorType === MonitorType.Logs) {
await fetchTelemetryServices();
}
} catch (err) {
setError(API.getFriendlyErrorMessage(err as Error));
}
setIsLoading(false);
};
useEffect(() => {
let fields: Array<Field<MonitorStepType>> = [];
if (props.monitorType === MonitorType.API) {
fields = [
{
key: "monitorDestination",
title: "API URL",
description: "URL of the API you want to monitor.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
{
key: "requestType",
title: "Request Type",
description: "Whats the type of the API request?",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
{
key: "requestBody",
title: "Request Body",
description: "Request Body to send, if any.",
fieldType: FieldType.JSON,
placeholder: "No data entered",
},
{
key: "requestHeaders",
title: "Request Headers",
description: "Request Headers to send, if any.",
fieldType: FieldType.DictionaryOfStrings,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Website) {
fields = [
{
key: "monitorDestination",
title: "Website URL",
description: "URL of the website you want to monitor.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Ping) {
fields = [
{
key: "monitorDestination",
title: "Ping Hostname or IP Address",
description:
"Hostname or IP Address of the resource you would like us to ping.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Port) {
fields = [
{
key: "monitorDestination",
title: "Ping Hostname or IP Address",
description:
"Hostname or IP Address of the resource you would like us to ping.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
{
key: "monitorDestinationPort",
title: "Port",
description: "Port of the resource you would like us to ping.",
fieldType: FieldType.Port,
placeholder: "No port entered",
},
];
} else if (props.monitorType === MonitorType.IP) {
fields = [
{
key: "monitorDestination",
title: "IP Address",
description: "IP Address of the resource you would like us to ping.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.CustomJavaScriptCode) {
fields = [
{
key: "customCode",
title: "JavaScript Code",
description: "JavaScript code to run.",
fieldType: FieldType.JavaScript,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.SyntheticMonitor) {
fields = [
{
key: "customCode",
title: "JavaScript Code",
description: "JavaScript code to run.",
fieldType: FieldType.JavaScript,
placeholder: "No data entered",
},
{
key: "browserTypes",
title: "Browser Types",
description: "Browser types to run the synthetic monitor on.",
fieldType: FieldType.ArrayOfText,
placeholder: "No data entered",
},
{
key: "screenSizeTypes",
title: "Screen Size Types",
description: "Screen size types to run the synthetic monitor on.",
fieldType: FieldType.ArrayOfText,
placeholder: "No data entered",
},
];
}
setRequestDetailsFields(fields);
loadComponent();
}, [props.monitorType]);
if (isLoading) {
return <ComponentLoader />;
}
if (error) {
return <ErrorMessage error={error} />;
}
if (props.monitorType === MonitorType.API) {
fields = [
{
key: "monitorDestination",
title: "API URL",
description: "URL of the API you want to monitor.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
{
key: "requestType",
title: "Request Type",
description: "Whats the type of the API request?",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
{
key: "requestBody",
title: "Request Body",
description: "Request Body to send, if any.",
fieldType: FieldType.JSON,
placeholder: "No data entered",
},
{
key: "requestHeaders",
title: "Request Headers",
description: "Request Headers to send, if any.",
fieldType: FieldType.DictionaryOfStrings,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Website) {
fields = [
{
key: "monitorDestination",
title: "Website URL",
description: "URL of the website you want to monitor.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Ping) {
fields = [
{
key: "monitorDestination",
title: "Ping Hostname or IP Address",
description:
"Hostname or IP Address of the resource you would like us to ping.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Port) {
fields = [
{
key: "monitorDestination",
title: "Ping Hostname or IP Address",
description:
"Hostname or IP Address of the resource you would like us to ping.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
{
key: "monitorDestinationPort",
title: "Port",
description: "Port of the resource you would like us to ping.",
fieldType: FieldType.Port,
placeholder: "No port entered",
},
];
} else if (props.monitorType === MonitorType.IP) {
fields = [
{
key: "monitorDestination",
title: "IP Address",
description: "IP Address of the resource you would like us to ping.",
fieldType: FieldType.Text,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.CustomJavaScriptCode) {
fields = [
{
key: "customCode",
title: "JavaScript Code",
description: "JavaScript code to run.",
fieldType: FieldType.JavaScript,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.SyntheticMonitor) {
fields = [
{
key: "customCode",
title: "JavaScript Code",
description: "JavaScript code to run.",
fieldType: FieldType.JavaScript,
placeholder: "No data entered",
},
{
key: "browserTypes",
title: "Browser Types",
description: "Browser types to run the synthetic monitor on.",
fieldType: FieldType.ArrayOfText,
placeholder: "No data entered",
},
{
key: "screenSizeTypes",
title: "Screen Size Types",
description: "Screen size types to run the synthetic monitor on.",
fieldType: FieldType.ArrayOfText,
placeholder: "No data entered",
},
];
} else if (props.monitorType === MonitorType.Logs) {
logFields = [];
if (props.monitorStep.data?.logMonitor?.body) {
logMonitorDetailView.body = props.monitorStep.data?.logMonitor?.body;
logFields.push({
key: "body",
title: "Filter Log Message",
description: "Filter by log message with this text:",
fieldType: FieldType.Text,
placeholder: "No log message entered",
});
}
if (props.monitorStep.data?.logMonitor?.lastXSecondsOfLogs) {
logMonitorDetailView.lastXSecondsOfLogs =
props.monitorStep.data?.logMonitor?.lastXSecondsOfLogs;
logFields.push({
key: "lastXSecondsOfLogs",
title: "Monitor logs for the last (time)",
description: "How many seconds of logs to monitor.",
fieldType: FieldType.Element,
placeholder: "1 minute",
getElement: (item: LogMonitorStepView): ReactElement => {
return (
<p>
{OneUptimeDate.convertSecondsToDaysHoursMinutesAndSeconds(
item.lastXSecondsOfLogs || 0,
)}
</p>
);
},
});
}
if (props.monitorStep.data?.logMonitor?.severityTexts) {
logMonitorDetailView.severityTexts =
props.monitorStep.data?.logMonitor?.severityTexts;
logFields.push({
key: "severityTexts",
title: "Log Severity",
description: "Severity of the logs to monitor.",
fieldType: FieldType.ArrayOfText,
placeholder: "No severity entered",
});
}
if (
props.monitorStep.data?.logMonitor?.attributes &&
Object.keys(props.monitorStep.data?.logMonitor?.attributes).length > 0
) {
logMonitorDetailView.attributes =
props.monitorStep.data?.logMonitor?.attributes;
logFields.push({
key: "attributes",
title: "Log Attributes",
description: "Attributes of the logs to monitor.",
fieldType: FieldType.JSON,
placeholder: "No attributes entered",
});
}
if (
props.monitorStep.data?.logMonitor?.telemetryServiceIds &&
props.monitorStep.data?.logMonitor?.telemetryServiceIds.length > 0 &&
telemetryServices &&
telemetryServices.length > 0
) {
logMonitorDetailView.telemetryServices = telemetryServices; // set the telemetry services
logFields.push({
key: "telemetryServices",
title: "Telemetry Services",
description: "Telemetry services to monitor.",
fieldType: FieldType.Element,
placeholder: "No telemetry services entered",
getElement: (): ReactElement => {
return (
<TelemetryServicesElement telemetryServices={telemetryServices} />
);
},
});
}
}
return (
<div className="mt-5">
<FieldLabelElement
title={"Request Details"}
title={"Monitor Details"}
description={
"Here are the details of the request we will send to monitor your resource status."
}
@@ -164,11 +338,20 @@ const MonitorStepElement: FunctionComponent<ComponentProps> = (
isHeading={true}
/>
<div className="mt-5">
<Detail
id={"monitor-step"}
item={props.monitorStep.data as any}
fields={requestDetailsFields}
/>
{fields && fields.length > 0 && (
<Detail<MonitorStepType>
id={"monitor-step"}
item={props.monitorStep.data!}
fields={fields}
/>
)}
{logFields && logFields.length > 0 && (
<Detail<LogMonitorStepView>
id={"monitor-logs"}
item={logMonitorDetailView}
fields={logFields}
/>
)}
</div>
<HorizontalRule />

View File

@@ -0,0 +1,43 @@
import OneUptimeDate from "Common/Types/Date";
import InfoCard from "CommonUI/src/Components/InfoCard/InfoCard";
import React, { FunctionComponent, ReactElement } from "react";
import TelemetryMonitorSummary from "./Types/TelemetryMonitorSummary";
export interface ComponentProps {
telemetryMonitorSummary?: TelemetryMonitorSummary | undefined;
}
const WebsiteMonitorSummaryView: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<div className="space-y-5">
<div className="flex space-x-3">
<InfoCard
className="w-1/2 shadow-none border-2 border-gray-100 "
title="Monitored At"
value={
props.telemetryMonitorSummary?.lastCheckedAt
? OneUptimeDate.getDateAsLocalFormattedString(
props.telemetryMonitorSummary?.lastCheckedAt,
)
: "-"
}
/>
<InfoCard
className="w-1/2 shadow-none border-2 border-gray-100 "
title="Next Check At"
value={
props.telemetryMonitorSummary?.nextCheckAt
? OneUptimeDate.getDateAsLocalFormattedString(
props.telemetryMonitorSummary?.nextCheckAt,
)
: "-"
}
/>
</div>
</div>
);
};
export default WebsiteMonitorSummaryView;

View File

@@ -10,6 +10,7 @@ import Card from "CommonUI/src/Components/Card/Card";
import { MonitorStepProbeResponse } from "Common/Models/DatabaseModels/MonitorProbe";
import Probe from "Common/Models/DatabaseModels/Probe";
import React, { FunctionComponent, ReactElement, useEffect } from "react";
import TelemetryMonitorSummary from "./Types/TelemetryMonitorSummary";
export interface ComponentProps {
probeMonitorResponses?: Array<MonitorStepProbeResponse> | undefined;
@@ -17,6 +18,7 @@ export interface ComponentProps {
serverMonitorResponse?: ServerMonitorResponse | undefined;
probes?: Array<Probe>;
monitorType: MonitorType;
telemetryMonitorSummary?: TelemetryMonitorSummary | undefined;
}
const Summary: FunctionComponent<ComponentProps> = (
@@ -81,6 +83,7 @@ const Summary: FunctionComponent<ComponentProps> = (
probeMonitorResponses={probeResponses}
incomingMonitorRequest={props.incomingMonitorRequest}
serverMonitorResponse={props.serverMonitorResponse}
telemetryMonitorSummary={props.telemetryMonitorSummary}
/>
</div>
</Card>

View File

@@ -12,12 +12,15 @@ import MonitorType, {
import ProbeMonitorResponse from "Common/Types/Probe/ProbeMonitorResponse";
import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage";
import React, { FunctionComponent, ReactElement } from "react";
import LogMonitorSummaryView from "./LogMonitorView";
import TelemetryMonitorSummary from "./Types/TelemetryMonitorSummary";
export interface ComponentProps {
monitorType: MonitorType;
probeMonitorResponses?: Array<ProbeMonitorResponse> | undefined; // this is an array because of multiple monitor steps.
incomingMonitorRequest?: IncomingMonitorRequest | undefined;
serverMonitorResponse?: ServerMonitorResponse | undefined;
telemetryMonitorSummary?: TelemetryMonitorSummary | undefined;
}
const SummaryInfo: FunctionComponent<ComponentProps> = (
@@ -140,6 +143,12 @@ const SummaryInfo: FunctionComponent<ComponentProps> = (
) : (
<></>
)}
{props.monitorType === MonitorType.Logs && (
<LogMonitorSummaryView
telemetryMonitorSummary={props.telemetryMonitorSummary}
/>
)}
</div>
);
};

View File

@@ -0,0 +1,4 @@
export default interface TelemetryMonitorSummary {
lastCheckedAt?: Date | undefined;
nextCheckAt?: Date | undefined;
}

View File

@@ -0,0 +1,31 @@
import TelemetryServiceElement from "./TelemetryServiceElement";
import TableColumnListComponent from "CommonUI/src/Components/TableColumnList/TableColumnListComponent";
import TelemetryService from "Common/Models/DatabaseModels/TelemetryService";
import React, { FunctionComponent, ReactElement } from "react";
export interface ComponentProps {
telemetryServices: Array<TelemetryService>;
onNavigateComplete?: (() => void) | undefined;
}
const TelemetryServicesElement: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
return (
<TableColumnListComponent
items={props.telemetryServices}
moreText="more services"
getEachElement={(telemetryService: TelemetryService) => {
return (
<TelemetryServiceElement
telemetryService={telemetryService}
onNavigateComplete={props.onNavigateComplete}
/>
);
}}
noItemsMessage="No services."
/>
);
};
export default TelemetryServicesElement;

View File

@@ -48,24 +48,24 @@ const DashboardSideMenu: FunctionComponent<ComponentProps> = (
},
}}
/>
{props.project?.isFeatureFlagMonitorGroupsEnabled ? (
<SideMenuSection title="Monitor Groups">
<SideMenuItem
link={{
title: "All Groups",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MONITOR_GROUPS] as Route,
),
}}
icon={IconProp.Squares}
/>
</SideMenuSection>
) : (
<></>
)}
</SideMenuSection>
{props.project?.isFeatureFlagMonitorGroupsEnabled ? (
<SideMenuSection title="Monitor Groups">
<SideMenuItem
link={{
title: "All Groups",
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.MONITOR_GROUPS] as Route,
),
}}
icon={IconProp.Squares}
/>
</SideMenuSection>
) : (
<></>
)}
<SideMenuSection title="Not Being Monitored">
<CountModelSideMenuItem<Monitor>
link={{

View File

@@ -3,7 +3,7 @@ import DisabledWarning from "../../../Components/Monitor/DisabledWarning";
import IncomingMonitorLink from "../../../Components/Monitor/IncomingRequestMonitor/IncomingMonitorLink";
import { MonitorCharts } from "../../../Components/Monitor/MonitorCharts/MonitorChart";
import ServerMonitorDocumentation from "../../../Components/Monitor/ServerMonitor/Documentation";
import Metrics from "../../../Components/Monitor/SummaryView/Summary";
import Summary from "../../../Components/Monitor/SummaryView/Summary";
import ProbeUtil from "../../../Utils/Probe";
import PageComponentProps from "../../PageComponentProps";
import InBetween from "Common/Types/BaseDatabase/InBetween";
@@ -63,6 +63,7 @@ import React, {
import useAsyncEffect from "use-async-effect";
import RouteMap, { RouteUtil } from "../../../Utils/RouteMap";
import PageMap from "../../../Utils/PageMap";
import LogMonitorPreview from "../../../Components/Monitor/LogMonitor/LogMonitorPreview";
const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID();
@@ -184,6 +185,9 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
serverMonitorResponse: true,
isNoProbeEnabledOnThisMonitor: true,
isAllProbesDisconnectedFromThisMonitor: true,
telemetryMonitorLastMonitorAt: true,
telemetryMonitorNextMonitorAt: true,
monitorSteps: true,
},
});
@@ -600,14 +604,39 @@ const MonitorView: FunctionComponent<PageComponentProps> = (): ReactElement => {
/>
</Card>
<Metrics
<Summary
monitorType={monitorType!}
probes={probes}
incomingMonitorRequest={incomingMonitorRequest}
probeMonitorResponses={probeResponses}
serverMonitorResponse={serverMonitorResponse}
telemetryMonitorSummary={{
lastCheckedAt: monitor?.telemetryMonitorLastMonitorAt,
nextCheckAt: monitor?.telemetryMonitorNextMonitorAt,
}}
/>
{monitor?.monitorType === MonitorType.Logs &&
monitor.monitorSteps &&
monitor.monitorSteps.data?.monitorStepsInstanceArray &&
monitor.monitorSteps.data?.monitorStepsInstanceArray.length > 0 && (
<div>
<Card
title={"Logs Preview"}
description={
"Preview of the logs that match the filter of this monitor."
}
>
<LogMonitorPreview
monitorStepLogMonitor={
monitor.monitorSteps.data?.monitorStepsInstanceArray[0]?.data
?.logMonitor
}
/>
</Card>
</div>
)}
{shouldFetchMonitorMetrics && getMonitorMetricsChartGroup()}
</Fragment>
);

View File

@@ -261,7 +261,8 @@ export default class CriteriaFilterUtil {
i.value === FilterType.GreaterThan ||
i.value === FilterType.LessThan ||
i.value === FilterType.LessThanOrEqualTo ||
i.value === FilterType.GreaterThanOrEqualTo
i.value === FilterType.GreaterThanOrEqualTo ||
i.value === FilterType.EqualTo
);
});
}