diff --git a/Common/Types/Log/LogSeverity.ts b/Common/Types/Log/LogSeverity.ts new file mode 100644 index 0000000000..d9c7ae22ef --- /dev/null +++ b/Common/Types/Log/LogSeverity.ts @@ -0,0 +1,11 @@ +enum LogSeverity { + Unspecified = "Unspecified", + Information = "Information", + Warning = "Warning", + Error = "Error", + Trace = "Trace", + Debug = "Debug", + Fatal = "Fatal", +} + +export default LogSeverity; diff --git a/Common/Types/Monitor/MonitorStep.ts b/Common/Types/Monitor/MonitorStep.ts index aa235b2f01..f4e6b78f68 100644 --- a/Common/Types/Monitor/MonitorStep.ts +++ b/Common/Types/Monitor/MonitorStep.ts @@ -10,6 +10,7 @@ import JSONFunctions from "../JSONFunctions"; import ObjectID from "../ObjectID"; import Port from "../Port"; import MonitorCriteria from "./MonitorCriteria"; +import MonitorStepLogMonitor, { MonitorStepLogMonitorUtil } from "./MonitorStepLogMonitor"; import MonitorType from "./MonitorType"; import BrowserType from "./SyntheticMonitors//BrowserType"; import ScreenSizeType from "./SyntheticMonitors/ScreenSizeType"; @@ -35,6 +36,9 @@ export interface MonitorStepType { // this is for synthetic monitors. screenSizeTypes?: Array | undefined; browserTypes?: Array | undefined; + + // Log monitor type. + logMonitor?: MonitorStepLogMonitor | undefined; } export default class MonitorStep extends DatabaseProperty { @@ -54,6 +58,7 @@ export default class MonitorStep extends DatabaseProperty { customCode: undefined, screenSizeTypes: undefined, browserTypes: undefined, + logMonitor: undefined, }; } @@ -77,6 +82,7 @@ export default class MonitorStep extends DatabaseProperty { customCode: undefined, screenSizeTypes: undefined, browserTypes: undefined, + logMonitor: undefined, }; return monitorStep; @@ -133,6 +139,11 @@ export default class MonitorStep extends DatabaseProperty { return this; } + public setLogMonitor(logMonitor: MonitorStepLogMonitor): MonitorStep { + this.data!.logMonitor = logMonitor; + return this; + } + public setCustomCode(customCode: string): MonitorStep { this.data!.customCode = customCode; return this; @@ -157,6 +168,7 @@ export default class MonitorStep extends DatabaseProperty { customCode: undefined, screenSizeTypes: undefined, browserTypes: undefined, + lgoMonitor: undefined, }, }; } @@ -240,6 +252,7 @@ export default class MonitorStep extends DatabaseProperty { customCode: this.data.customCode || undefined, screenSizeTypes: this.data.screenSizeTypes || undefined, browserTypes: this.data.browserTypes || undefined, + logMonitor: this.data.logMonitor ? MonitorStepLogMonitorUtil.toJSON(this.data.logMonitor) : undefined, }, }); } @@ -328,6 +341,9 @@ export default class MonitorStep extends DatabaseProperty { screenSizeTypes: (json["screenSizeTypes"] as Array) || undefined, browserTypes: (json["browserTypes"] as Array) || undefined, + logMonitor: json["logMonitor"] + ? (json["logMonitor"] as JSONObject) + : undefined, }) as any; return monitorStep; diff --git a/Common/Types/Monitor/MonitorStepLogMonitor.ts b/Common/Types/Monitor/MonitorStepLogMonitor.ts new file mode 100644 index 0000000000..63242cdfce --- /dev/null +++ b/Common/Types/Monitor/MonitorStepLogMonitor.ts @@ -0,0 +1,48 @@ +import Dictionary from "../Dictionary"; +import { JSONObject } from "../JSON"; +import LogSeverity from "../Log/LogSeverity"; +import ObjectID from "../ObjectID"; + +export default interface MonitorStepLogMonitor { + attributes: Dictionary; + body: string; + severityText: Array; + telemetryServiceId: Array; + lastXSecondsOfLogs: number; +} + +export class MonitorStepLogMonitorUtil { + + public static getDefault(): MonitorStepLogMonitor { + return { + attributes: {}, + body: "", + severityText: [], + telemetryServiceId: [], + lastXSecondsOfLogs: 60, + }; + } + + public static fromJSON(json: JSONObject): MonitorStepLogMonitor { + return { + attributes: + (json["attributes"] as Dictionary) || {}, + body: json["body"] as string, + severityText: json["severityText"] as Array, + telemetryServiceId: ObjectID.fromJSONArray( + json["telemetryServiceId"] as Array, + ), + lastXSecondsOfLogs: json["lastXSecondsOfLogs"] as number, + }; + } + + public static toJSON(monitor: MonitorStepLogMonitor): JSONObject { + return { + attributes: monitor.attributes, + body: monitor.body, + severityText: monitor.severityText, + telemetryServiceId: ObjectID.toJSONArray(monitor.telemetryServiceId), + lastXSecondsOfLogs: monitor.lastXSecondsOfLogs, + }; + } +} diff --git a/Common/Types/Monitor/MonitorType.ts b/Common/Types/Monitor/MonitorType.ts index 07f97fb0d8..0b6060c44e 100644 --- a/Common/Types/Monitor/MonitorType.ts +++ b/Common/Types/Monitor/MonitorType.ts @@ -15,6 +15,11 @@ enum MonitorType { // These two monitor types are same but we are keeping them separate for now - this is for marketing purposes SyntheticMonitor = "Synthetic Monitor", CustomJavaScriptCode = "Custom JavaScript Code", + + // Telemetry monitor types + Logs = "Logs", + Metrics = "Metrics", + Traces = "Traces", } export default MonitorType; @@ -99,6 +104,23 @@ export class MonitorTypeHelper { description: "This monitor type lets you run custom JavaScript code on a schedule.", }, + { + monitorType: MonitorType.Logs, + 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.", + }, ]; return monitorTypeProps; diff --git a/Common/Types/ObjectID.ts b/Common/Types/ObjectID.ts index 0a8bca20a7..49aa32beb6 100644 --- a/Common/Types/ObjectID.ts +++ b/Common/Types/ObjectID.ts @@ -47,6 +47,12 @@ export default class ObjectID extends DatabaseProperty { return new this(UUID.generate()); } + public static toJSONArray(ids: Array): Array { + return ids.map((id: ObjectID) => { + return id.toJSON(); + }); + } + protected static override toDatabase( value: ObjectID | FindOperator, ): string | null { @@ -76,6 +82,12 @@ export default class ObjectID extends DatabaseProperty { throw new BadDataException("Invalid JSON: " + JSON.stringify(json)); } + public static fromJSONArray(json: Array): Array { + return json.map((value: JSONObject) => { + return ObjectID.fromJSON(value); + }); + } + protected static override fromDatabase(_value: string): ObjectID | null { if (_value) { return new ObjectID(_value); diff --git a/CommonUI/src/Components/Filters/DropdownFilter.tsx b/CommonUI/src/Components/Filters/DropdownFilter.tsx index da1fe59f07..046160d98c 100644 --- a/CommonUI/src/Components/Filters/DropdownFilter.tsx +++ b/CommonUI/src/Components/Filters/DropdownFilter.tsx @@ -9,6 +9,7 @@ export interface ComponentProps { filter: Filter; onFilterChanged?: undefined | ((filterData: FilterData) => void); filterData: FilterData; + isMultiSelect?: boolean | undefined; } type DropdownFilterFunction = ( @@ -50,7 +51,7 @@ const DropdownFilter: DropdownFilterFunction = ( } }} value={dropdownValue} - isMultiSelect={false} + isMultiSelect={props.isMultiSelect || false} placeholder={`Filter by ${filter.title}`} /> ); diff --git a/CommonUI/src/Components/Filters/FiltersForm.tsx b/CommonUI/src/Components/Filters/FiltersForm.tsx index 5c824643c3..7a789ea4c1 100644 --- a/CommonUI/src/Components/Filters/FiltersForm.tsx +++ b/CommonUI/src/Components/Filters/FiltersForm.tsx @@ -2,6 +2,7 @@ import Button, { ButtonStyleType } from "../Button/Button"; import ComponentLoader from "../ComponentLoader/ComponentLoader"; import ErrorMessage from "../ErrorMessage/ErrorMessage"; import FieldLabelElement from "../Forms/Fields/FieldLabel"; +import FieldType from "../Types/FieldType"; import BooleanFilter from "./BooleanFilter"; import DateFilter from "./DateFilter"; import DropdownFilter from "./DropdownFilter"; @@ -74,6 +75,9 @@ const FiltersForm: FiltersFormFunction = ( filter={filter} filterData={props.filterData} onFilterChanged={changeFilterData} + isMultiSelect={ + filter.type === FieldType.MultiSelectDropdown + } /> void; - onAutoScrollChanged: (turnOnAutoScroll: boolean) => void; - // telemetryServices?: Array; -} - -const LogsFilters: FunctionComponent = ( - props: ComponentProps, -): ReactElement => { - const [filterOptions, setFilterOptions] = React.useState({}); - - const [turnOnAutoScroll, setTurnOnAutoScroll] = React.useState(true); - const [showMoreFilters, setShowMoreFilters] = React.useState(false); - const [isSqlQuery] = React.useState(false); - - const showAutoScrollButton: boolean = - !isSqlQuery && !showMoreFilters && !filterOptions.searchText; - const showSearchButton: boolean = Boolean( - showMoreFilters || filterOptions.searchText, - ); - - return ( -
-
-
-
-
- {!isSqlQuery && ( -
-
- - { - setFilterOptions({ - ...filterOptions, - searchText: value, - }); - }} - /> -
- - {showMoreFilters && ( -
- - | null, - ) => { - if (value === null) { - setFilterOptions({ - ...filterOptions, - logSeverity: undefined, - }); - } else { - setFilterOptions({ - ...filterOptions, - logSeverity: value.toString() as LogSeverity, - }); - } - }} - options={DropdownUtil.getDropdownOptionsFromEnum( - LogSeverity, - )} - /> -
- )} - - {showMoreFilters && ( -
-
- - { - setFilterOptions({ - ...filterOptions, - startTime: value - ? OneUptimeDate.fromString(value) - : undefined, - }); - }} - type={InputType.DATETIME_LOCAL} - /> - {filterOptions.endTime && !filterOptions.startTime && ( - - )} -
-
- - { - setFilterOptions({ - ...filterOptions, - endTime: value - ? OneUptimeDate.fromString(value) - : undefined, - }); - }} - type={InputType.DATETIME_LOCAL} - /> - {filterOptions.startTime && !filterOptions.endTime && ( - - )} -
-
- )} -
- )} - {isSqlQuery && ( -
-
- - { - setFilterOptions({ - searchText: value, - }); - }} - showLineNumbers={true} - /> -
-
- )} -
- {showAutoScrollButton && ( -
-
- {!turnOnAutoScroll && ( -
-
- )} - {isSqlQuery && ( -
-
-
-
- )} - {showSearchButton && ( -
-
-
-
- )} -
-
- -
-
-
- {!isSqlQuery && ( -
- {!showMoreFilters && ( -
- )} -
- {/* {!isSqlQuery && ( -
-
-
- {/*
-
-
-
*/} -
-
-
-
-
- ); -}; - -export default LogsFilters; diff --git a/CommonUI/src/Components/LogsViewer/LogsViewer.tsx b/CommonUI/src/Components/LogsViewer/LogsViewer.tsx index 8420bd3b26..a24fcef9be 100644 --- a/CommonUI/src/Components/LogsViewer/LogsViewer.tsx +++ b/CommonUI/src/Components/LogsViewer/LogsViewer.tsx @@ -5,7 +5,8 @@ import FiltersForm from "../Filters/FiltersForm"; import FieldType from "../Types/FieldType"; import LogItem from "./LogItem"; import { PromiseVoidFunction, VoidFunction } from "Common/Types/FunctionTypes"; -import Log, { LogSeverity } from "Model/AnalyticsModels/Log"; +import Log from "Model/AnalyticsModels/Log"; +import LogSeverity from "Common/Types/Log/LogSeverity"; import React, { FunctionComponent, ReactElement, Ref } from "react"; import Toggle from "../Toggle/Toggle"; import Card from "../Card/Card"; diff --git a/CommonUI/src/Components/Types/FieldType.ts b/CommonUI/src/Components/Types/FieldType.ts index e717cf8932..1f9ffd5a99 100644 --- a/CommonUI/src/Components/Types/FieldType.ts +++ b/CommonUI/src/Components/Types/FieldType.ts @@ -11,6 +11,7 @@ enum FieldType { Number = "Number", Password = "Password", Dropdown = "Dropdown", + MultiSelectDropdown = "MultiSelectDropdown", Text = "Text", Email = "Email", Date = "Date", diff --git a/Dashboard/src/Components/Form/Monitor/LogMonitor/LogMonitorStepFrom.tsx b/Dashboard/src/Components/Form/Monitor/LogMonitor/LogMonitorStepFrom.tsx new file mode 100644 index 0000000000..8f42e83003 --- /dev/null +++ b/Dashboard/src/Components/Form/Monitor/LogMonitor/LogMonitorStepFrom.tsx @@ -0,0 +1,124 @@ +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 "Model/Models/TelemetryService"; +import React, { FunctionComponent, ReactElement } from "react"; + +export interface ComponentProps { + monitorStepLogMonitor: MonitorStepLogMonitor; + onMonitorStepLogMonitorChanged: ( + monitorStepLogMonitor: MonitorStepLogMonitor, + ) => void; + attributeKeys: Array; + telemetryServices: Array; +} + +const LogMonitorStepForm: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + return ( + + id="logs-filter" + showFilter={true} + filterData={props.monitorStepLogMonitor} + onFilterChanged={(filterData: Query) => { + 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, + }, + { + 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: "Check Last X Seconds of Logs", + isAdvancedFilter: true, + }, + { + key: "severityText", + filterDropdownOptions: + DropdownUtil.getDropdownOptionsFromEnum(LogSeverity), + type: FieldType.MultiSelectDropdown, + title: "Log Severity", + isAdvancedFilter: true, + }, + { + key: "telemetryServiceId", + 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, + }, + ]} + /> + ); +}; + +export default LogMonitorStepForm; diff --git a/Dashboard/src/Components/Form/Monitor/MonitorStep.tsx b/Dashboard/src/Components/Form/Monitor/MonitorStep.tsx index 14b514c369..40afb39c3a 100644 --- a/Dashboard/src/Components/Form/Monitor/MonitorStep.tsx +++ b/Dashboard/src/Components/Form/Monitor/MonitorStep.tsx @@ -8,6 +8,7 @@ import Exception from "Common/Types/Exception/Exception"; import IP from "Common/Types/IP/IP"; import MonitorCriteria from "Common/Types/Monitor/MonitorCriteria"; import MonitorStep from "Common/Types/Monitor/MonitorStep"; +import MonitorStepLogMonitor, { MonitorStepLogMonitorUtil } from "Common/Types/Monitor/MonitorStepLogMonitor"; import MonitorType from "Common/Types/Monitor/MonitorType"; import BrowserType from "Common/Types/Monitor/SyntheticMonitors/BrowserType"; import Port from "Common/Types/Port"; @@ -27,7 +28,7 @@ import FieldLabelElement from "CommonUI/src/Components/Forms/Fields/FieldLabel"; import HorizontalRule from "CommonUI/src/Components/HorizontalRule/HorizontalRule"; import Input from "CommonUI/src/Components/Input/Input"; import Link from "CommonUI/src/Components/Link/Link"; -import { DOCS_URL } from "CommonUI/src/Config"; +import { APP_API_URL, DOCS_URL } from "CommonUI/src/Config"; import DropdownUtil from "CommonUI/src/Utils/Dropdown"; import React, { FunctionComponent, @@ -35,6 +36,19 @@ import React, { useEffect, useState, } from "react"; +import LogMonitorStepForm from "./LogMonitor/LogMonitorStepFrom"; +import TelemetryService from "Model/Models/TelemetryService"; +import ModelAPI, { ListResult } 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 HTTPResponse from "Common/Types/API/HTTPResponse"; +import { JSONObject } from "Common/Types/JSON"; +import ComponentLoader from "CommonUI/src/Components/ComponentLoader/ComponentLoader"; +import ErrorMessage from "CommonUI/src/Components/ErrorMessage/ErrorMessage"; +import { PromiseVoidFunction } from "Common/Types/FunctionTypes"; export interface ComponentProps { monitorStatusDropdownOptions: Array; @@ -58,12 +72,90 @@ const MonitorStepElement: FunctionComponent = ( props.initialValue || new MonitorStep(), ); + const [telemetryServices, setTelemetryServices] = useState< + Array + >([]); + const [attributeKeys, setAttributeKeys] = useState>([]); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { if (props.onChange && monitorStep) { props.onChange(monitorStep); } }, [monitorStep]); + const fetchLogAttributes: PromiseVoidFunction = async (): Promise => { + const attributeRepsonse: HTTPResponse | HTTPErrorResponse = + await API.post( + URL.fromString(APP_API_URL.toString()).addRoute( + "/telemetry/logs/get-attributes", + ), + {}, + { + ...ModelAPI.getCommonHeaders(), + }, + ); + + if (attributeRepsonse instanceof HTTPErrorResponse) { + throw attributeRepsonse; + } else { + const attributes: Array = attributeRepsonse.data[ + "attributes" + ] as Array; + setAttributeKeys(attributes); + } + }; + + const fetchTelemetryServices: PromiseVoidFunction = + async (): Promise => { + const telemetryServicesResult: ListResult = + await ModelAPI.getList({ + modelType: TelemetryService, + query: { + projectId: DashboardNavigation.getProjectId(), + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + _id: true, + name: true, + }, + sort: { + name: SortOrder.Ascending, + }, + }); + + if (telemetryServicesResult instanceof HTTPErrorResponse) { + throw telemetryServicesResult; + } + + setTelemetryServices(telemetryServicesResult.data); + }; + + const fetchTelemetryServicesAndAttributes: PromiseVoidFunction = + async (): Promise => { + setIsLoading(true); + setError(""); + try { + await fetchTelemetryServices(); + + if (props.monitorType === MonitorType.Logs) { + await fetchLogAttributes(); + } + } catch (err) { + setError(API.getFriendlyErrorMessage(err as Error)); + } + + setIsLoading(false); + }; + + useEffect(() => { + fetchTelemetryServicesAndAttributes().catch((err: Error) => { + setError(API.getFriendlyErrorMessage(err as Error)); + }); + }, [props.monitorType]); + const [errors, setErrors] = useState>({}); const [touched, setTouched] = useState>({}); @@ -158,6 +250,14 @@ const MonitorStepElement: FunctionComponent = ( props.monitorType === MonitorType.CustomJavaScriptCode || props.monitorType === MonitorType.SyntheticMonitor; + if (isLoading) { + return ; + } + + if (error) { + return ; + } + return (
{hasMonitorDestination && ( @@ -267,6 +367,29 @@ const MonitorStepElement: FunctionComponent = (
)} + {props.monitorType === MonitorType.Logs && ( +
+ + { + monitorStep.setLogMonitor(value); + setMonitorStep(MonitorStep.clone(monitorStep)); + }} + attributeKeys={attributeKeys} + telemetryServices={telemetryServices} + /> +
+ )} + {props.monitorType === MonitorType.API && (