mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
Refactor and clean up code across multiple components
- Added missing commas in migration index. - Improved formatting of permission descriptions for better readability. - Enhanced dependency array formatting in useEffect hooks in LogsViewer component. - Cleaned up JSON normalization in LogDetailsPanel for better readability. - Refactored LogSearchBar component for improved readability and structure. - Simplified filter logic in AnalyticsTooltip and LogsAnalyticsView components. - Streamlined LogScrubRuleService methods for better clarity and consistency. - Updated OtelLogsIngestService to improve readability of async calls. - Enhanced LogFilterEvaluator comments and formatting for better understanding.
This commit is contained in:
@@ -57,13 +57,13 @@ const FilterConditionElement: FunctionComponent<ComponentProps> = (
|
||||
const isAttributeField: boolean = condition.field.startsWith("attributes.");
|
||||
const selectedFieldOption: DropdownOption | undefined = isAttributeField
|
||||
? undefined
|
||||
: fieldOptions.find(
|
||||
(opt: DropdownOption) => opt.value === condition.field,
|
||||
);
|
||||
: fieldOptions.find((opt: DropdownOption) => {
|
||||
return opt.value === condition.field;
|
||||
});
|
||||
const selectedOperatorOption: DropdownOption | undefined =
|
||||
operatorOptions.find(
|
||||
(opt: DropdownOption) => opt.value === condition.operator,
|
||||
);
|
||||
operatorOptions.find((opt: DropdownOption) => {
|
||||
return opt.value === condition.operator;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md p-4 bg-gray-50 border-gray-200 border-solid border">
|
||||
@@ -83,7 +83,9 @@ const FilterConditionElement: FunctionComponent<ComponentProps> = (
|
||||
]}
|
||||
value={selectedFieldOption}
|
||||
placeholder="Select field..."
|
||||
onChange={(value: DropdownValue | Array<DropdownValue> | null) => {
|
||||
onChange={(
|
||||
value: DropdownValue | Array<DropdownValue> | null,
|
||||
) => {
|
||||
if (value === "__custom_attribute__") {
|
||||
props.onChange({
|
||||
...condition,
|
||||
@@ -128,7 +130,9 @@ const FilterConditionElement: FunctionComponent<ComponentProps> = (
|
||||
options={operatorOptions}
|
||||
value={selectedOperatorOption}
|
||||
placeholder="Select..."
|
||||
onChange={(value: DropdownValue | Array<DropdownValue> | null) => {
|
||||
onChange={(
|
||||
value: DropdownValue | Array<DropdownValue> | null,
|
||||
) => {
|
||||
props.onChange({
|
||||
...condition,
|
||||
operator: value?.toString() || "=",
|
||||
|
||||
@@ -19,9 +19,7 @@ import ObjectID from "Common/Types/ObjectID";
|
||||
import LogPipeline from "Common/Models/DatabaseModels/LogPipeline";
|
||||
import ModelAPI from "Common/UI/Utils/ModelAPI/ModelAPI";
|
||||
import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert";
|
||||
import FilterConditionElement, {
|
||||
FilterConditionData,
|
||||
} from "./FilterCondition";
|
||||
import FilterConditionElement, { FilterConditionData } from "./FilterCondition";
|
||||
|
||||
export interface ComponentProps {
|
||||
pipelineId: ObjectID;
|
||||
@@ -47,11 +45,8 @@ function parseFilterQuery(query: string): {
|
||||
}
|
||||
|
||||
// Detect connector
|
||||
const connector: LogicalConnector = query.includes(" OR ")
|
||||
? "OR"
|
||||
: "AND";
|
||||
const connectorRegex: RegExp =
|
||||
connector === "AND" ? / AND /i : / OR /i;
|
||||
const connector: LogicalConnector = query.includes(" OR ") ? "OR" : "AND";
|
||||
const connectorRegex: RegExp = connector === "AND" ? / AND /i : / OR /i;
|
||||
const parts: Array<string> = query.split(connectorRegex);
|
||||
|
||||
const conditions: Array<FilterConditionData> = [];
|
||||
@@ -104,10 +99,9 @@ function buildFilterQuery(
|
||||
connector: LogicalConnector,
|
||||
): string {
|
||||
const parts: Array<string> = conditions
|
||||
.filter(
|
||||
(c: FilterConditionData) =>
|
||||
c.field && c.operator && c.value,
|
||||
)
|
||||
.filter((c: FilterConditionData) => {
|
||||
return c.field && c.operator && c.value;
|
||||
})
|
||||
.map((c: FilterConditionData) => {
|
||||
if (c.operator === "LIKE") {
|
||||
return `${c.field} LIKE '${c.value}'`;
|
||||
@@ -115,7 +109,9 @@ function buildFilterQuery(
|
||||
if (c.operator === "IN") {
|
||||
const values: string = c.value
|
||||
.split(",")
|
||||
.map((v: string) => `'${v.trim()}'`)
|
||||
.map((v: string) => {
|
||||
return `'${v.trim()}'`;
|
||||
})
|
||||
.join(", ");
|
||||
return `${c.field} IN (${values})`;
|
||||
}
|
||||
@@ -151,30 +147,31 @@ const FilterQueryBuilder: FunctionComponent<ComponentProps> = (
|
||||
const [successMessage, setSuccessMessage] = useState<string>("");
|
||||
const [originalQuery, setOriginalQuery] = useState<string>("");
|
||||
|
||||
const loadPipeline: () => Promise<void> = useCallback(async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const pipeline: LogPipeline | null = await ModelAPI.getItem({
|
||||
modelType: LogPipeline,
|
||||
id: props.pipelineId,
|
||||
select: { filterQuery: true },
|
||||
});
|
||||
const loadPipeline: () => Promise<void> =
|
||||
useCallback(async (): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const pipeline: LogPipeline | null = await ModelAPI.getItem({
|
||||
modelType: LogPipeline,
|
||||
id: props.pipelineId,
|
||||
select: { filterQuery: true },
|
||||
});
|
||||
|
||||
if (pipeline?.filterQuery) {
|
||||
const parsed: {
|
||||
conditions: Array<FilterConditionData>;
|
||||
connector: LogicalConnector;
|
||||
} = parseFilterQuery(pipeline.filterQuery);
|
||||
setConditions(parsed.conditions);
|
||||
setConnector(parsed.connector);
|
||||
setOriginalQuery(pipeline.filterQuery);
|
||||
if (pipeline?.filterQuery) {
|
||||
const parsed: {
|
||||
conditions: Array<FilterConditionData>;
|
||||
connector: LogicalConnector;
|
||||
} = parseFilterQuery(pipeline.filterQuery);
|
||||
setConditions(parsed.conditions);
|
||||
setConnector(parsed.connector);
|
||||
setOriginalQuery(pipeline.filterQuery);
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load filter conditions.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to load filter conditions.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [props.pipelineId]);
|
||||
}, [props.pipelineId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadPipeline().catch(() => {
|
||||
@@ -217,10 +214,7 @@ const FilterQueryBuilder: FunctionComponent<ComponentProps> = (
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card
|
||||
title="Filter Conditions"
|
||||
description="Loading..."
|
||||
>
|
||||
<Card title="Filter Conditions" description="Loading...">
|
||||
<div className="p-4 text-gray-400 text-sm">
|
||||
Loading filter conditions...
|
||||
</div>
|
||||
@@ -272,15 +266,13 @@ const FilterQueryBuilder: FunctionComponent<ComponentProps> = (
|
||||
{conditions.length > 1 && (
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-gray-600 font-medium">
|
||||
Match
|
||||
</span>
|
||||
<span className="text-sm text-gray-600 font-medium">Match</span>
|
||||
<div className="w-64">
|
||||
<Dropdown
|
||||
options={connectorOptions}
|
||||
value={connectorOptions.find(
|
||||
(opt: DropdownOption) => opt.value === connector,
|
||||
)}
|
||||
value={connectorOptions.find((opt: DropdownOption) => {
|
||||
return opt.value === connector;
|
||||
})}
|
||||
onChange={(
|
||||
value: DropdownValue | Array<DropdownValue> | null,
|
||||
) => {
|
||||
@@ -296,48 +288,45 @@ const FilterQueryBuilder: FunctionComponent<ComponentProps> = (
|
||||
|
||||
{/* Condition rows */}
|
||||
<div className="space-y-3">
|
||||
{conditions.map(
|
||||
(condition: FilterConditionData, index: number) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{index > 0 && (
|
||||
<div className="flex items-center justify-center my-2">
|
||||
<div className="flex-1 border-t border-gray-200"></div>
|
||||
<span
|
||||
className={`mx-3 px-3 py-1 text-xs font-semibold rounded-full border ${
|
||||
connector === "AND"
|
||||
? "bg-blue-100 text-blue-700 border-blue-200"
|
||||
: "bg-amber-100 text-amber-700 border-amber-200"
|
||||
}`}
|
||||
>
|
||||
{connector}
|
||||
</span>
|
||||
<div className="flex-1 border-t border-gray-200"></div>
|
||||
</div>
|
||||
)}
|
||||
<FilterConditionElement
|
||||
condition={condition}
|
||||
canDelete={conditions.length > 1}
|
||||
onChange={(updated: FilterConditionData) => {
|
||||
const newConditions: Array<FilterConditionData> = [
|
||||
...conditions,
|
||||
];
|
||||
newConditions[index] = updated;
|
||||
setConditions(newConditions);
|
||||
}}
|
||||
onDelete={() => {
|
||||
const newConditions: Array<FilterConditionData> =
|
||||
conditions.filter(
|
||||
(_: FilterConditionData, i: number) =>
|
||||
i !== index,
|
||||
);
|
||||
setConditions(newConditions);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{conditions.map((condition: FilterConditionData, index: number) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{index > 0 && (
|
||||
<div className="flex items-center justify-center my-2">
|
||||
<div className="flex-1 border-t border-gray-200"></div>
|
||||
<span
|
||||
className={`mx-3 px-3 py-1 text-xs font-semibold rounded-full border ${
|
||||
connector === "AND"
|
||||
? "bg-blue-100 text-blue-700 border-blue-200"
|
||||
: "bg-amber-100 text-amber-700 border-amber-200"
|
||||
}`}
|
||||
>
|
||||
{connector}
|
||||
</span>
|
||||
<div className="flex-1 border-t border-gray-200"></div>
|
||||
</div>
|
||||
)}
|
||||
<FilterConditionElement
|
||||
condition={condition}
|
||||
canDelete={conditions.length > 1}
|
||||
onChange={(updated: FilterConditionData) => {
|
||||
const newConditions: Array<FilterConditionData> = [
|
||||
...conditions,
|
||||
];
|
||||
newConditions[index] = updated;
|
||||
setConditions(newConditions);
|
||||
}}
|
||||
onDelete={() => {
|
||||
const newConditions: Array<FilterConditionData> =
|
||||
conditions.filter((_: FilterConditionData, i: number) => {
|
||||
return i !== index;
|
||||
});
|
||||
setConditions(newConditions);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Add condition + Clear buttons */}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { FunctionComponent, ReactElement, useState } from "react";
|
||||
import Card from "Common/UI/Components/Card/Card";
|
||||
import Button, {
|
||||
ButtonSize,
|
||||
@@ -29,7 +25,11 @@ export interface ComponentProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type ProcessorType = "SeverityRemapper" | "AttributeRemapper" | "CategoryProcessor" | "";
|
||||
type ProcessorType =
|
||||
| "SeverityRemapper"
|
||||
| "AttributeRemapper"
|
||||
| "CategoryProcessor"
|
||||
| "";
|
||||
|
||||
const processorTypeOptions: Array<DropdownOption> = [
|
||||
{
|
||||
@@ -47,8 +47,7 @@ const processorTypeOptions: Array<DropdownOption> = [
|
||||
{
|
||||
value: "CategoryProcessor",
|
||||
label: "Category Processor",
|
||||
description:
|
||||
"Assign categories to logs based on filter conditions",
|
||||
description: "Assign categories to logs based on filter conditions",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -92,9 +91,9 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
case "SeverityRemapper":
|
||||
return {
|
||||
sourceKey: severitySourceKey,
|
||||
mappings: severityMappings.filter(
|
||||
(m: SeverityMapping) => m.matchValue && m.severityText,
|
||||
),
|
||||
mappings: severityMappings.filter((m: SeverityMapping) => {
|
||||
return m.matchValue && m.severityText;
|
||||
}),
|
||||
};
|
||||
case "AttributeRemapper":
|
||||
return {
|
||||
@@ -106,9 +105,9 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
case "CategoryProcessor":
|
||||
return {
|
||||
targetKey: categoryTargetKey,
|
||||
categories: categories.filter(
|
||||
(c: CategoryRule) => c.name && c.filterQuery,
|
||||
),
|
||||
categories: categories.filter((c: CategoryRule) => {
|
||||
return c.name && c.filterQuery;
|
||||
}),
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
@@ -128,10 +127,11 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
if (!severitySourceKey.trim()) {
|
||||
return "Source key is required for Severity Remapper.";
|
||||
}
|
||||
const validMappings: Array<SeverityMapping> =
|
||||
severityMappings.filter(
|
||||
(m: SeverityMapping) => m.matchValue && m.severityText,
|
||||
);
|
||||
const validMappings: Array<SeverityMapping> = severityMappings.filter(
|
||||
(m: SeverityMapping) => {
|
||||
return m.matchValue && m.severityText;
|
||||
},
|
||||
);
|
||||
if (validMappings.length === 0) {
|
||||
return "At least one severity mapping is required.";
|
||||
}
|
||||
@@ -150,7 +150,9 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
return "Target key is required.";
|
||||
}
|
||||
const validCategories: Array<CategoryRule> = categories.filter(
|
||||
(c: CategoryRule) => c.name && c.filterQuery,
|
||||
(c: CategoryRule) => {
|
||||
return c.name && c.filterQuery;
|
||||
},
|
||||
);
|
||||
if (validCategories.length === 0) {
|
||||
return "At least one category rule is required.";
|
||||
@@ -249,19 +251,16 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
options={processorTypeOptions}
|
||||
value={
|
||||
processorType
|
||||
? processorTypeOptions.find(
|
||||
(opt: DropdownOption) =>
|
||||
opt.value === processorType,
|
||||
)
|
||||
? processorTypeOptions.find((opt: DropdownOption) => {
|
||||
return opt.value === processorType;
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
placeholder="Select processor type..."
|
||||
onChange={(
|
||||
value: DropdownValue | Array<DropdownValue> | null,
|
||||
) => {
|
||||
setProcessorType(
|
||||
(value?.toString() as ProcessorType) || "",
|
||||
);
|
||||
setProcessorType((value?.toString() as ProcessorType) || "");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -274,9 +273,9 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
Severity Remapper Configuration
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Map values from a log attribute to standard severity levels.
|
||||
For example, map "warn" to WARNING, or
|
||||
"err" to ERROR.
|
||||
Map values from a log attribute to standard severity levels. For
|
||||
example, map "warn" to WARNING, or "err" to
|
||||
ERROR.
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
@@ -317,8 +316,9 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
onDelete={() => {
|
||||
setSeverityMappings(
|
||||
severityMappings.filter(
|
||||
(_: SeverityMapping, i: number) =>
|
||||
i !== index,
|
||||
(_: SeverityMapping, i: number) => {
|
||||
return i !== index;
|
||||
},
|
||||
),
|
||||
);
|
||||
}}
|
||||
@@ -414,8 +414,8 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
Category Processor Configuration
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Assign a category to logs based on filter conditions. The
|
||||
first matching rule wins.
|
||||
Assign a category to logs based on filter conditions. The first
|
||||
matching rule wins.
|
||||
</p>
|
||||
|
||||
<div className="mb-4">
|
||||
@@ -439,74 +439,71 @@ const ProcessorForm: FunctionComponent<ComponentProps> = (
|
||||
description="Define categories and the conditions that trigger them"
|
||||
/>
|
||||
<div className="mt-2 space-y-2">
|
||||
{categories.map(
|
||||
(cat: CategoryRule, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="grid grid-cols-12 gap-3 items-center p-3 bg-gray-50 rounded-md border border-gray-200"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
<Input
|
||||
type={InputType.TEXT}
|
||||
placeholder="Category name (e.g. Error)"
|
||||
value={cat.name}
|
||||
onChange={(value: string) => {
|
||||
const newCats: Array<CategoryRule> = [
|
||||
...categories,
|
||||
];
|
||||
newCats[index] = {
|
||||
...cat,
|
||||
name: value,
|
||||
};
|
||||
setCategories(newCats);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-center">
|
||||
<span className="text-gray-400 text-sm font-medium">
|
||||
when
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<Input
|
||||
type={InputType.TEXT}
|
||||
placeholder="e.g. severityText = 'ERROR'"
|
||||
value={cat.filterQuery}
|
||||
onChange={(value: string) => {
|
||||
const newCats: Array<CategoryRule> = [
|
||||
...categories,
|
||||
];
|
||||
newCats[index] = {
|
||||
...cat,
|
||||
filterQuery: value,
|
||||
};
|
||||
setCategories(newCats);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-end">
|
||||
<Button
|
||||
icon={IconProp.Trash}
|
||||
buttonStyle={
|
||||
ButtonStyleType.DANGER_OUTLINE
|
||||
}
|
||||
buttonSize={ButtonSize.Small}
|
||||
onClick={() => {
|
||||
setCategories(
|
||||
categories.filter(
|
||||
(_: CategoryRule, i: number) =>
|
||||
i !== index,
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={categories.length <= 1}
|
||||
/>
|
||||
</div>
|
||||
{categories.map((cat: CategoryRule, index: number) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="grid grid-cols-12 gap-3 items-center p-3 bg-gray-50 rounded-md border border-gray-200"
|
||||
>
|
||||
<div className="col-span-4">
|
||||
<Input
|
||||
type={InputType.TEXT}
|
||||
placeholder="Category name (e.g. Error)"
|
||||
value={cat.name}
|
||||
onChange={(value: string) => {
|
||||
const newCats: Array<CategoryRule> = [
|
||||
...categories,
|
||||
];
|
||||
newCats[index] = {
|
||||
...cat,
|
||||
name: value,
|
||||
};
|
||||
setCategories(newCats);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<div className="col-span-1 flex justify-center">
|
||||
<span className="text-gray-400 text-sm font-medium">
|
||||
when
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<Input
|
||||
type={InputType.TEXT}
|
||||
placeholder="e.g. severityText = 'ERROR'"
|
||||
value={cat.filterQuery}
|
||||
onChange={(value: string) => {
|
||||
const newCats: Array<CategoryRule> = [
|
||||
...categories,
|
||||
];
|
||||
newCats[index] = {
|
||||
...cat,
|
||||
filterQuery: value,
|
||||
};
|
||||
setCategories(newCats);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-end">
|
||||
<Button
|
||||
icon={IconProp.Trash}
|
||||
buttonStyle={ButtonStyleType.DANGER_OUTLINE}
|
||||
buttonSize={ButtonSize.Small}
|
||||
onClick={() => {
|
||||
setCategories(
|
||||
categories.filter(
|
||||
(_: CategoryRule, i: number) => {
|
||||
return i !== index;
|
||||
},
|
||||
),
|
||||
);
|
||||
}}
|
||||
disabled={categories.length <= 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
|
||||
@@ -62,8 +62,7 @@ const LogDropFilters: FunctionComponent<
|
||||
title: "Filter Query",
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
required: true,
|
||||
placeholder:
|
||||
"e.g. severityText = 'DEBUG' OR body LIKE '%health%'",
|
||||
placeholder: "e.g. severityText = 'DEBUG' OR body LIKE '%health%'",
|
||||
},
|
||||
{
|
||||
field: {
|
||||
|
||||
@@ -16,7 +16,12 @@ import LogPipeline from "Common/Models/DatabaseModels/LogPipeline";
|
||||
import LogPipelineProcessor from "Common/Models/DatabaseModels/LogPipelineProcessor";
|
||||
import FilterQueryBuilder from "../../Components/LogPipeline/FilterQueryBuilder";
|
||||
import ProcessorForm from "../../Components/LogPipeline/ProcessorForm";
|
||||
import React, { Fragment, FunctionComponent, ReactElement, useState } from "react";
|
||||
import React, {
|
||||
Fragment,
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
const LogPipelineView: FunctionComponent<PageComponentProps> = (
|
||||
_props: PageComponentProps,
|
||||
@@ -33,8 +38,7 @@ const LogPipelineView: FunctionComponent<PageComponentProps> = (
|
||||
name="Log Pipeline Details"
|
||||
cardProps={{
|
||||
title: "Pipeline Details",
|
||||
description:
|
||||
"Basic information about this pipeline.",
|
||||
description: "Basic information about this pipeline.",
|
||||
}}
|
||||
isEditable={true}
|
||||
formFields={[
|
||||
|
||||
@@ -8,7 +8,9 @@ import LogPipeline from "Common/Models/DatabaseModels/LogPipeline";
|
||||
import ProjectUtil from "Common/UI/Utils/Project";
|
||||
import React, { Fragment, FunctionComponent, ReactElement } from "react";
|
||||
|
||||
const LogPipelines: FunctionComponent<PageComponentProps> = (): ReactElement => {
|
||||
const LogPipelines: FunctionComponent<
|
||||
PageComponentProps
|
||||
> = (): ReactElement => {
|
||||
return (
|
||||
<Fragment>
|
||||
<ModelTable<LogPipeline>
|
||||
|
||||
@@ -382,15 +382,11 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.SETTINGS_LOG_PIPELINES,
|
||||
)}
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_LOG_PIPELINES)}
|
||||
element={
|
||||
<SettingsLogPipelines
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.SETTINGS_LOG_PIPELINES] as Route
|
||||
}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_LOG_PIPELINES] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -403,36 +399,26 @@ const SettingsRoutes: FunctionComponent<ComponentProps> = (
|
||||
element={
|
||||
<SettingsLogPipelineView
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.SETTINGS_LOG_PIPELINE_VIEW] as Route
|
||||
}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_LOG_PIPELINE_VIEW] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.SETTINGS_LOG_DROP_FILTERS,
|
||||
)}
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_LOG_DROP_FILTERS)}
|
||||
element={
|
||||
<SettingsLogDropFilters
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.SETTINGS_LOG_DROP_FILTERS] as Route
|
||||
}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_LOG_DROP_FILTERS] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<PageRoute
|
||||
path={RouteUtil.getLastPathForKey(
|
||||
PageMap.SETTINGS_LOG_SCRUB_RULES,
|
||||
)}
|
||||
path={RouteUtil.getLastPathForKey(PageMap.SETTINGS_LOG_SCRUB_RULES)}
|
||||
element={
|
||||
<SettingsLogScrubRules
|
||||
{...props}
|
||||
pageRoute={
|
||||
RouteMap[PageMap.SETTINGS_LOG_SCRUB_RULES] as Route
|
||||
}
|
||||
pageRoute={RouteMap[PageMap.SETTINGS_LOG_SCRUB_RULES] as Route}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -124,8 +124,7 @@ export default class LogPipelineProcessor extends BaseModel {
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Project ID",
|
||||
description:
|
||||
"ID of the project this log pipeline processor belongs to.",
|
||||
description: "ID of the project this log pipeline processor belongs to.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
@@ -154,8 +153,7 @@ export default class LogPipelineProcessor extends BaseModel {
|
||||
type: TableColumnType.Entity,
|
||||
modelType: LogPipeline,
|
||||
title: "Log Pipeline",
|
||||
description:
|
||||
"Relation to the log pipeline this processor belongs to.",
|
||||
description: "Relation to the log pipeline this processor belongs to.",
|
||||
})
|
||||
@ManyToOne(
|
||||
() => {
|
||||
@@ -192,8 +190,7 @@ export default class LogPipelineProcessor extends BaseModel {
|
||||
required: true,
|
||||
canReadOnRelationQuery: true,
|
||||
title: "Log Pipeline ID",
|
||||
description:
|
||||
"ID of the log pipeline this processor belongs to.",
|
||||
description: "ID of the log pipeline this processor belongs to.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
@@ -422,8 +419,7 @@ export default class LogPipelineProcessor extends BaseModel {
|
||||
@TableColumn({
|
||||
type: TableColumnType.ObjectID,
|
||||
title: "Created By User ID",
|
||||
description:
|
||||
"ID of the user who created this log pipeline processor.",
|
||||
description: "ID of the user who created this log pipeline processor.",
|
||||
})
|
||||
@Column({
|
||||
type: ColumnType.ObjectID,
|
||||
|
||||
@@ -442,10 +442,7 @@ router.post(
|
||||
? OneUptimeDate.fromString(body["endTime"] as string)
|
||||
: OneUptimeDate.getCurrentDate();
|
||||
|
||||
const limit: number = Math.min(
|
||||
(body["limit"] as number) || 10000,
|
||||
10000,
|
||||
);
|
||||
const limit: number = Math.min((body["limit"] as number) || 10000, 10000);
|
||||
|
||||
const format: string = (body["format"] as string) || "json";
|
||||
|
||||
@@ -471,8 +468,8 @@ router.post(
|
||||
? (body["spanIds"] as Array<string>)
|
||||
: undefined;
|
||||
|
||||
const rows: Array<JSONObject> =
|
||||
await LogAggregationService.getExportLogs({
|
||||
const rows: Array<JSONObject> = await LogAggregationService.getExportLogs(
|
||||
{
|
||||
projectId: databaseProps.tenantId,
|
||||
startTime,
|
||||
endTime,
|
||||
@@ -482,7 +479,8 @@ router.post(
|
||||
bodySearchText,
|
||||
traceIds,
|
||||
spanIds,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (format === "csv") {
|
||||
const header: string =
|
||||
@@ -491,12 +489,9 @@ router.post(
|
||||
const escapeCsv: (val: unknown) => string = (
|
||||
val: unknown,
|
||||
): string => {
|
||||
const str: string = val === null || val === undefined ? "" : String(val);
|
||||
if (
|
||||
str.includes(",") ||
|
||||
str.includes('"') ||
|
||||
str.includes("\n")
|
||||
) {
|
||||
const str: string =
|
||||
val === null || val === undefined ? "" : String(val);
|
||||
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
|
||||
@@ -1,54 +1,131 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1773402621107 implements MigrationInterface {
|
||||
public name = 'MigrationName1773402621107'
|
||||
public name = "MigrationName1773402621107";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "LogPipeline" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(50) NOT NULL, "description" character varying(500), "filterQuery" character varying(500), "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_563f923c5169ef1e28c09dfa586" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_a6bff623cedf515ae3680d8735" ON "LogPipeline" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_70fc1c15d7770bf0646001b907" ON "LogPipeline" ("isEnabled") `);
|
||||
await queryRunner.query(`CREATE TABLE "LogPipelineProcessor" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "logPipelineId" uuid NOT NULL, "name" character varying(50) NOT NULL, "processorType" character varying(100) NOT NULL, "configuration" jsonb NOT NULL DEFAULT '{}', "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, CONSTRAINT "PK_1f78f02415229abb3ff3fa0805a" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_6236eaae19a7b0ffb57b6d8b05" ON "LogPipelineProcessor" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b6281d545398353d360a05d10c" ON "LogPipelineProcessor" ("logPipelineId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_e7194996a6547d4557a26739d6" ON "LogPipelineProcessor" ("isEnabled") `);
|
||||
await queryRunner.query(`CREATE TABLE "LogDropFilter" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(50) NOT NULL, "description" character varying(500), "filterQuery" character varying(500) NOT NULL, "action" character varying(100) NOT NULL DEFAULT 'drop', "samplePercentage" integer, "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_4d5244c285955b534cbeb55b330" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_2651bf0f1b0981f3c1a15201fd" ON "LogDropFilter" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_95cd1d1be21a2d7f620698ac48" ON "LogDropFilter" ("isEnabled") `);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipeline" ADD CONSTRAINT "FK_a6bff623cedf515ae3680d87355" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipeline" ADD CONSTRAINT "FK_460794d1df27630235f2d543ba6" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipeline" ADD CONSTRAINT "FK_ffe95c5c8cbcea614d33c255d24" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipelineProcessor" ADD CONSTRAINT "FK_6236eaae19a7b0ffb57b6d8b053" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipelineProcessor" ADD CONSTRAINT "FK_b6281d545398353d360a05d10ce" FOREIGN KEY ("logPipelineId") REFERENCES "LogPipeline"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipelineProcessor" ADD CONSTRAINT "FK_b2e03f9db1555a4f5ff6bc7778c" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogDropFilter" ADD CONSTRAINT "FK_2651bf0f1b0981f3c1a15201fd7" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogDropFilter" ADD CONSTRAINT "FK_b613c2da8abed05bb4d1a94a277" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogDropFilter" ADD CONSTRAINT "FK_7702956f8707c3d3525ddb299c9" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "LogDropFilter" DROP CONSTRAINT "FK_7702956f8707c3d3525ddb299c9"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogDropFilter" DROP CONSTRAINT "FK_b613c2da8abed05bb4d1a94a277"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogDropFilter" DROP CONSTRAINT "FK_2651bf0f1b0981f3c1a15201fd7"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipelineProcessor" DROP CONSTRAINT "FK_b2e03f9db1555a4f5ff6bc7778c"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipelineProcessor" DROP CONSTRAINT "FK_b6281d545398353d360a05d10ce"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipelineProcessor" DROP CONSTRAINT "FK_6236eaae19a7b0ffb57b6d8b053"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipeline" DROP CONSTRAINT "FK_ffe95c5c8cbcea614d33c255d24"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipeline" DROP CONSTRAINT "FK_460794d1df27630235f2d543ba6"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogPipeline" DROP CONSTRAINT "FK_a6bff623cedf515ae3680d87355"`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_95cd1d1be21a2d7f620698ac48"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_2651bf0f1b0981f3c1a15201fd"`);
|
||||
await queryRunner.query(`DROP TABLE "LogDropFilter"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_e7194996a6547d4557a26739d6"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b6281d545398353d360a05d10c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_6236eaae19a7b0ffb57b6d8b05"`);
|
||||
await queryRunner.query(`DROP TABLE "LogPipelineProcessor"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_70fc1c15d7770bf0646001b907"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_a6bff623cedf515ae3680d8735"`);
|
||||
await queryRunner.query(`DROP TABLE "LogPipeline"`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "LogPipeline" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(50) NOT NULL, "description" character varying(500), "filterQuery" character varying(500), "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_563f923c5169ef1e28c09dfa586" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_a6bff623cedf515ae3680d8735" ON "LogPipeline" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_70fc1c15d7770bf0646001b907" ON "LogPipeline" ("isEnabled") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "LogPipelineProcessor" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "logPipelineId" uuid NOT NULL, "name" character varying(50) NOT NULL, "processorType" character varying(100) NOT NULL, "configuration" jsonb NOT NULL DEFAULT '{}', "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, CONSTRAINT "PK_1f78f02415229abb3ff3fa0805a" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6236eaae19a7b0ffb57b6d8b05" ON "LogPipelineProcessor" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b6281d545398353d360a05d10c" ON "LogPipelineProcessor" ("logPipelineId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_e7194996a6547d4557a26739d6" ON "LogPipelineProcessor" ("isEnabled") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "LogDropFilter" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(50) NOT NULL, "description" character varying(500), "filterQuery" character varying(500) NOT NULL, "action" character varying(100) NOT NULL DEFAULT 'drop', "samplePercentage" integer, "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_4d5244c285955b534cbeb55b330" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2651bf0f1b0981f3c1a15201fd" ON "LogDropFilter" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_95cd1d1be21a2d7f620698ac48" ON "LogDropFilter" ("isEnabled") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipeline" ADD CONSTRAINT "FK_a6bff623cedf515ae3680d87355" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipeline" ADD CONSTRAINT "FK_460794d1df27630235f2d543ba6" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipeline" ADD CONSTRAINT "FK_ffe95c5c8cbcea614d33c255d24" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipelineProcessor" ADD CONSTRAINT "FK_6236eaae19a7b0ffb57b6d8b053" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipelineProcessor" ADD CONSTRAINT "FK_b6281d545398353d360a05d10ce" FOREIGN KEY ("logPipelineId") REFERENCES "LogPipeline"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipelineProcessor" ADD CONSTRAINT "FK_b2e03f9db1555a4f5ff6bc7778c" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogDropFilter" ADD CONSTRAINT "FK_2651bf0f1b0981f3c1a15201fd7" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogDropFilter" ADD CONSTRAINT "FK_b613c2da8abed05bb4d1a94a277" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogDropFilter" ADD CONSTRAINT "FK_7702956f8707c3d3525ddb299c9" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogDropFilter" DROP CONSTRAINT "FK_7702956f8707c3d3525ddb299c9"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogDropFilter" DROP CONSTRAINT "FK_b613c2da8abed05bb4d1a94a277"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogDropFilter" DROP CONSTRAINT "FK_2651bf0f1b0981f3c1a15201fd7"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipelineProcessor" DROP CONSTRAINT "FK_b2e03f9db1555a4f5ff6bc7778c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipelineProcessor" DROP CONSTRAINT "FK_b6281d545398353d360a05d10ce"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipelineProcessor" DROP CONSTRAINT "FK_6236eaae19a7b0ffb57b6d8b053"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipeline" DROP CONSTRAINT "FK_ffe95c5c8cbcea614d33c255d24"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipeline" DROP CONSTRAINT "FK_460794d1df27630235f2d543ba6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogPipeline" DROP CONSTRAINT "FK_a6bff623cedf515ae3680d87355"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_95cd1d1be21a2d7f620698ac48"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2651bf0f1b0981f3c1a15201fd"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "LogDropFilter"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_e7194996a6547d4557a26739d6"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_b6281d545398353d360a05d10c"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_6236eaae19a7b0ffb57b6d8b05"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "LogPipelineProcessor"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_70fc1c15d7770bf0646001b907"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_a6bff623cedf515ae3680d8735"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "LogPipeline"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,79 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1773414578773 implements MigrationInterface {
|
||||
name = 'MigrationName1773414578773'
|
||||
name = "MigrationName1773414578773";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_projectId"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_createdByUserId"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_deletedByUserId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_projectId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_isEnabled"`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_0ed4595b431ba465ac9a9938d4" ON "LogScrubRule" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_88d1e2bb9908f0aada30f044f5" ON "LogScrubRule" ("isEnabled") `);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_0ed4595b431ba465ac9a9938d4d" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_cec04acd064a11bf98c2eae3819" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_88ad7031d2481dd8142e543ddbd" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_88ad7031d2481dd8142e543ddbd"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_cec04acd064a11bf98c2eae3819"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_0ed4595b431ba465ac9a9938d4d"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_88d1e2bb9908f0aada30f044f5"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_0ed4595b431ba465ac9a9938d4"`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`);
|
||||
await queryRunner.query(`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_logscrub_isEnabled" ON "LogScrubRule" ("isEnabled") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_logscrub_projectId" ON "LogScrubRule" ("projectId") `);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_projectId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_createdByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_deletedByUserId"`,
|
||||
);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_projectId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_isEnabled"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type":"Recurring","value":{"intervalType":"Day","intervalCount":{"_type":"PositiveNumber","value":1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_0ed4595b431ba465ac9a9938d4" ON "LogScrubRule" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_88d1e2bb9908f0aada30f044f5" ON "LogScrubRule" ("isEnabled") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_0ed4595b431ba465ac9a9938d4d" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_cec04acd064a11bf98c2eae3819" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_88ad7031d2481dd8142e543ddbd" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_88ad7031d2481dd8142e543ddbd"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_cec04acd064a11bf98c2eae3819"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_0ed4595b431ba465ac9a9938d4d"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_88d1e2bb9908f0aada30f044f5"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_0ed4595b431ba465ac9a9938d4"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type": "RestrictionTimes", "value": {"restictionType": "None", "dayRestrictionTimes": null, "weeklyRestrictionTimes": []}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "rotation" SET DEFAULT '{"_type": "Recurring", "value": {"intervalType": "Day", "intervalCount": {"_type": "PositiveNumber", "value": 1}}}'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_logscrub_isEnabled" ON "LogScrubRule" ("isEnabled") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_logscrub_projectId" ON "LogScrubRule" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MigrationName1773500000000 implements MigrationInterface {
|
||||
public name = 'MigrationName1773500000000'
|
||||
public name = "MigrationName1773500000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "LogScrubRule" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(50) NOT NULL, "description" character varying(500), "patternType" character varying(100) NOT NULL, "customRegex" character varying(500), "scrubAction" character varying(100) NOT NULL DEFAULT 'redact', "fieldsToScrub" character varying(100) NOT NULL DEFAULT 'both', "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_logscrub_id" PRIMARY KEY ("_id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_logscrub_projectId" ON "LogScrubRule" ("projectId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_logscrub_isEnabled" ON "LogScrubRule" ("isEnabled") `);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_deletedByUserId"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_createdByUserId"`);
|
||||
await queryRunner.query(`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_projectId"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_isEnabled"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_projectId"`);
|
||||
await queryRunner.query(`DROP TABLE "LogScrubRule"`);
|
||||
}
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "LogScrubRule" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "name" character varying(50) NOT NULL, "description" character varying(500), "patternType" character varying(100) NOT NULL, "customRegex" character varying(500), "scrubAction" character varying(100) NOT NULL DEFAULT 'redact', "fieldsToScrub" character varying(100) NOT NULL DEFAULT 'both', "isEnabled" boolean NOT NULL DEFAULT true, "sortOrder" integer NOT NULL DEFAULT '0', "createdByUserId" uuid, "deletedByUserId" uuid, CONSTRAINT "PK_logscrub_id" PRIMARY KEY ("_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_logscrub_projectId" ON "LogScrubRule" ("projectId") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_logscrub_isEnabled" ON "LogScrubRule" ("isEnabled") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_projectId" FOREIGN KEY ("projectId") REFERENCES "Project"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_createdByUserId" FOREIGN KEY ("createdByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" ADD CONSTRAINT "FK_logscrub_deletedByUserId" FOREIGN KEY ("deletedByUserId") REFERENCES "User"("_id") ON DELETE SET NULL ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_deletedByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_createdByUserId"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "LogScrubRule" DROP CONSTRAINT "FK_logscrub_projectId"`,
|
||||
);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_isEnabled"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_logscrub_projectId"`);
|
||||
await queryRunner.query(`DROP TABLE "LogScrubRule"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,5 +537,5 @@ export default [
|
||||
MigrationName1773344537755,
|
||||
MigrationName1773402621107,
|
||||
MigrationName1773500000000,
|
||||
MigrationName1773414578773
|
||||
MigrationName1773414578773,
|
||||
];
|
||||
|
||||
@@ -4027,8 +4027,7 @@ export class PermissionHelper {
|
||||
{
|
||||
permission: Permission.EditProjectLogPipeline,
|
||||
title: "Edit Log Pipeline",
|
||||
description:
|
||||
"This permission can edit Log Pipelines of this project.",
|
||||
description: "This permission can edit Log Pipelines of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
group: PermissionGroup.Telemetry,
|
||||
@@ -4036,8 +4035,7 @@ export class PermissionHelper {
|
||||
{
|
||||
permission: Permission.ReadProjectLogPipeline,
|
||||
title: "Read Log Pipeline",
|
||||
description:
|
||||
"This permission can read Log Pipelines of this project.",
|
||||
description: "This permission can read Log Pipelines of this project.",
|
||||
isAssignableToTenant: true,
|
||||
isAccessControlPermission: false,
|
||||
group: PermissionGroup.Telemetry,
|
||||
|
||||
@@ -589,7 +589,13 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [displayedLogs, focusedRowIndex, selectedLogId, showKeyboardShortcuts, handleSearchSubmit]);
|
||||
}, [
|
||||
displayedLogs,
|
||||
focusedRowIndex,
|
||||
selectedLogId,
|
||||
showKeyboardShortcuts,
|
||||
handleSearchSubmit,
|
||||
]);
|
||||
|
||||
const handlePageChange: (page: number) => void = (page: number): void => {
|
||||
if (props.onPageChange) {
|
||||
@@ -875,7 +881,9 @@ const LogsViewer: FunctionComponent<ComponentProps> = (
|
||||
logs={displayedLogs}
|
||||
serviceMap={serviceMap}
|
||||
isLoading={props.isLoading}
|
||||
focusedRowIndex={focusedRowIndex >= 0 ? focusedRowIndex : undefined}
|
||||
focusedRowIndex={
|
||||
focusedRowIndex >= 0 ? focusedRowIndex : undefined
|
||||
}
|
||||
emptyMessage={
|
||||
props.noLogsMessage ||
|
||||
getEmptyMessageWithTimeRange(props.timeRange)
|
||||
|
||||
@@ -127,8 +127,9 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
||||
}
|
||||
|
||||
try {
|
||||
const normalized: Record<string, unknown> =
|
||||
JSONFunctions.unflattenObject(props.log.attributes || {});
|
||||
const normalized: Record<string, unknown> = JSONFunctions.unflattenObject(
|
||||
props.log.attributes || {},
|
||||
);
|
||||
return JSON.stringify(normalized, null, 2);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -179,54 +180,55 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
||||
return undefined;
|
||||
}, [spanId, props, traceId]);
|
||||
|
||||
const loadContext: () => Promise<void> = useCallback(async (): Promise<void> => {
|
||||
if (!props.projectId || !serviceId || !props.log.time) {
|
||||
setContextError("Missing project or service information for context.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setContextLoading(true);
|
||||
setContextError("");
|
||||
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/telemetry/logs/context",
|
||||
),
|
||||
data: {
|
||||
logId: props.log.getColumnValue("_id")?.toString() || "",
|
||||
serviceId: serviceId,
|
||||
time: props.log.time
|
||||
? OneUptimeDate.toString(props.log.time)
|
||||
: "",
|
||||
count: 5,
|
||||
},
|
||||
headers: {
|
||||
...ModelAPI.getCommonHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
const loadContext: () => Promise<void> =
|
||||
useCallback(async (): Promise<void> => {
|
||||
if (!props.projectId || !serviceId || !props.log.time) {
|
||||
setContextError("Missing project or service information for context.");
|
||||
return;
|
||||
}
|
||||
|
||||
const before: Array<JSONObject> =
|
||||
(response.data["before"] as Array<JSONObject>) || [];
|
||||
const after: Array<JSONObject> =
|
||||
(response.data["after"] as Array<JSONObject>) || [];
|
||||
try {
|
||||
setContextLoading(true);
|
||||
setContextError("");
|
||||
|
||||
setContextBefore(before.map(parseContextRow));
|
||||
setContextAfter(after.map(parseContextRow));
|
||||
setContextLoaded(true);
|
||||
} catch (err) {
|
||||
setContextError(
|
||||
`Failed to load log context. ${API.getFriendlyErrorMessage(err as Error)}`,
|
||||
);
|
||||
} finally {
|
||||
setContextLoading(false);
|
||||
}
|
||||
}, [props.projectId, serviceId, props.log]);
|
||||
const response: HTTPResponse<JSONObject> | HTTPErrorResponse =
|
||||
await API.post({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
"/telemetry/logs/context",
|
||||
),
|
||||
data: {
|
||||
logId: props.log.getColumnValue("_id")?.toString() || "",
|
||||
serviceId: serviceId,
|
||||
time: props.log.time
|
||||
? OneUptimeDate.toString(props.log.time)
|
||||
: "",
|
||||
count: 5,
|
||||
},
|
||||
headers: {
|
||||
...ModelAPI.getCommonHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
const before: Array<JSONObject> =
|
||||
(response.data["before"] as Array<JSONObject>) || [];
|
||||
const after: Array<JSONObject> =
|
||||
(response.data["after"] as Array<JSONObject>) || [];
|
||||
|
||||
setContextBefore(before.map(parseContextRow));
|
||||
setContextAfter(after.map(parseContextRow));
|
||||
setContextLoaded(true);
|
||||
} catch (err) {
|
||||
setContextError(
|
||||
`Failed to load log context. ${API.getFriendlyErrorMessage(err as Error)}`,
|
||||
);
|
||||
} finally {
|
||||
setContextLoading(false);
|
||||
}
|
||||
}, [props.projectId, serviceId, props.log]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "context" && !contextLoaded && !contextLoading) {
|
||||
@@ -532,11 +534,8 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
||||
{renderContextLogRow(
|
||||
{
|
||||
id: props.log.getColumnValue("_id")?.toString() || "current",
|
||||
time: props.log.time
|
||||
? props.log.time.toString()
|
||||
: "",
|
||||
severity:
|
||||
props.log.severityText?.toString() || "Unspecified",
|
||||
time: props.log.time ? props.log.time.toString() : "",
|
||||
severity: props.log.severityText?.toString() || "Unspecified",
|
||||
body: props.log.body || "",
|
||||
serviceId: serviceId,
|
||||
},
|
||||
|
||||
@@ -29,315 +29,313 @@ export interface LogSearchBarRef {
|
||||
|
||||
const LogSearchBar: React.ForwardRefExoticComponent<
|
||||
LogSearchBarProps & React.RefAttributes<LogSearchBarRef>
|
||||
> = forwardRef<LogSearchBarRef, LogSearchBarProps>(function LogSearchBar(
|
||||
props: LogSearchBarProps,
|
||||
ref: React.Ref<LogSearchBarRef>,
|
||||
): ReactElement {
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
|
||||
const [showHelp, setShowHelp] = useState<boolean>(false);
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] =
|
||||
useState<number>(-1);
|
||||
const inputRef: React.RefObject<HTMLInputElement> = useRef<HTMLInputElement>(
|
||||
null!,
|
||||
);
|
||||
const containerRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
||||
null!,
|
||||
);
|
||||
> = forwardRef<LogSearchBarRef, LogSearchBarProps>(
|
||||
(props: LogSearchBarProps, ref: React.Ref<LogSearchBarRef>): ReactElement => {
|
||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
|
||||
const [showHelp, setShowHelp] = useState<boolean>(false);
|
||||
const [selectedSuggestionIndex, setSelectedSuggestionIndex] =
|
||||
useState<number>(-1);
|
||||
const inputRef: React.RefObject<HTMLInputElement> =
|
||||
useRef<HTMLInputElement>(null!);
|
||||
const containerRef: React.RefObject<HTMLDivElement> =
|
||||
useRef<HTMLDivElement>(null!);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
focus: (): void => {
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
}, []);
|
||||
|
||||
const currentWord: string = extractCurrentWord(props.value);
|
||||
const currentWord: string = extractCurrentWord(props.value);
|
||||
|
||||
// Strip leading "@" — treat it as a trigger character for suggestions
|
||||
const hasAtPrefix: boolean = currentWord.startsWith("@");
|
||||
const normalizedWord: string = hasAtPrefix
|
||||
? currentWord.substring(1)
|
||||
: currentWord;
|
||||
// Strip leading "@" — treat it as a trigger character for suggestions
|
||||
const hasAtPrefix: boolean = currentWord.startsWith("@");
|
||||
const normalizedWord: string = hasAtPrefix
|
||||
? currentWord.substring(1)
|
||||
: currentWord;
|
||||
|
||||
// Determine if we're in "field:value" mode or "field name" mode
|
||||
const colonIndex: number = normalizedWord.indexOf(":");
|
||||
const isValueMode: boolean = colonIndex > 0;
|
||||
const fieldPrefix: string = isValueMode
|
||||
? normalizedWord.substring(0, colonIndex).toLowerCase()
|
||||
: "";
|
||||
const partialValue: string = isValueMode
|
||||
? normalizedWord.substring(colonIndex + 1)
|
||||
: "";
|
||||
// Determine if we're in "field:value" mode or "field name" mode
|
||||
const colonIndex: number = normalizedWord.indexOf(":");
|
||||
const isValueMode: boolean = colonIndex > 0;
|
||||
const fieldPrefix: string = isValueMode
|
||||
? normalizedWord.substring(0, colonIndex).toLowerCase()
|
||||
: "";
|
||||
const partialValue: string = isValueMode
|
||||
? normalizedWord.substring(colonIndex + 1)
|
||||
: "";
|
||||
|
||||
const filteredSuggestions: Array<string> = isValueMode
|
||||
? getValueSuggestions(
|
||||
fieldPrefix,
|
||||
partialValue,
|
||||
props.valueSuggestions || {},
|
||||
)
|
||||
: (props.suggestions || []).filter((s: string): boolean => {
|
||||
if (!normalizedWord && !hasAtPrefix) {
|
||||
return false;
|
||||
}
|
||||
// When just "@" is typed, show all suggestions
|
||||
if (hasAtPrefix && normalizedWord.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Match against the suggestion name, stripping any leading "@" from the suggestion too
|
||||
const normalizedSuggestion: string = s.startsWith("@")
|
||||
? s.substring(1).toLowerCase()
|
||||
: s.toLowerCase();
|
||||
return normalizedSuggestion.startsWith(normalizedWord.toLowerCase());
|
||||
});
|
||||
|
||||
const shouldShowSuggestions: boolean =
|
||||
showSuggestions &&
|
||||
isFocused &&
|
||||
filteredSuggestions.length > 0 &&
|
||||
(isValueMode ? true : currentWord.length > 0);
|
||||
|
||||
// Show help when focused, input is empty, and no suggestions visible
|
||||
const shouldShowHelp: boolean =
|
||||
showHelp && isFocused && props.value.length === 0 && !shouldShowSuggestions;
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}, [currentWord]);
|
||||
|
||||
const handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void =
|
||||
useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === "Enter") {
|
||||
if (
|
||||
shouldShowSuggestions &&
|
||||
selectedSuggestionIndex >= 0 &&
|
||||
selectedSuggestionIndex < filteredSuggestions.length
|
||||
) {
|
||||
applySuggestion(filteredSuggestions[selectedSuggestionIndex]!);
|
||||
e.preventDefault();
|
||||
return;
|
||||
const filteredSuggestions: Array<string> = isValueMode
|
||||
? getValueSuggestions(
|
||||
fieldPrefix,
|
||||
partialValue,
|
||||
props.valueSuggestions || {},
|
||||
)
|
||||
: (props.suggestions || []).filter((s: string): boolean => {
|
||||
if (!normalizedWord && !hasAtPrefix) {
|
||||
return false;
|
||||
}
|
||||
// When just "@" is typed, show all suggestions
|
||||
if (hasAtPrefix && normalizedWord.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Match against the suggestion name, stripping any leading "@" from the suggestion too
|
||||
const normalizedSuggestion: string = s.startsWith("@")
|
||||
? s.substring(1).toLowerCase()
|
||||
: s.toLowerCase();
|
||||
return normalizedSuggestion.startsWith(normalizedWord.toLowerCase());
|
||||
});
|
||||
|
||||
// If in value mode with a typed value, try to match and apply as chip
|
||||
if (
|
||||
isValueMode &&
|
||||
partialValue.length > 0 &&
|
||||
props.onFieldValueSelect
|
||||
) {
|
||||
// First try exact case-insensitive match from the available values
|
||||
const resolvedField: string =
|
||||
FIELD_ALIAS_MAP[fieldPrefix] || fieldPrefix;
|
||||
const availableValues: Array<string> =
|
||||
(props.valueSuggestions || {})[resolvedField] || [];
|
||||
const lowerPartial: string = partialValue.toLowerCase();
|
||||
const exactMatch: string | undefined = availableValues.find(
|
||||
(v: string): boolean => {
|
||||
return v.toLowerCase() === lowerPartial;
|
||||
},
|
||||
);
|
||||
const shouldShowSuggestions: boolean =
|
||||
showSuggestions &&
|
||||
isFocused &&
|
||||
filteredSuggestions.length > 0 &&
|
||||
(isValueMode ? true : currentWord.length > 0);
|
||||
|
||||
// Use exact match, or if there's exactly one prefix match, use that
|
||||
const resolvedMatch: string | undefined =
|
||||
exactMatch ||
|
||||
(filteredSuggestions.length === 1
|
||||
? filteredSuggestions[0]
|
||||
: undefined);
|
||||
// Show help when focused, input is empty, and no suggestions visible
|
||||
const shouldShowHelp: boolean =
|
||||
showHelp &&
|
||||
isFocused &&
|
||||
props.value.length === 0 &&
|
||||
!shouldShowSuggestions;
|
||||
|
||||
if (resolvedMatch) {
|
||||
props.onFieldValueSelect(fieldPrefix, resolvedMatch);
|
||||
// Remove the field:value term from text
|
||||
const parts: Array<string> = props.value.split(/\s+/);
|
||||
parts.pop();
|
||||
const remaining: string = parts.join(" ");
|
||||
props.onChange(remaining ? remaining + " " : "");
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
useEffect(() => {
|
||||
setSelectedSuggestionIndex(-1);
|
||||
}, [currentWord]);
|
||||
|
||||
const handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void =
|
||||
useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === "Enter") {
|
||||
if (
|
||||
shouldShowSuggestions &&
|
||||
selectedSuggestionIndex >= 0 &&
|
||||
selectedSuggestionIndex < filteredSuggestions.length
|
||||
) {
|
||||
applySuggestion(filteredSuggestions[selectedSuggestionIndex]!);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// If in value mode with a typed value, try to match and apply as chip
|
||||
if (
|
||||
isValueMode &&
|
||||
partialValue.length > 0 &&
|
||||
props.onFieldValueSelect
|
||||
) {
|
||||
// First try exact case-insensitive match from the available values
|
||||
const resolvedField: string =
|
||||
FIELD_ALIAS_MAP[fieldPrefix] || fieldPrefix;
|
||||
const availableValues: Array<string> =
|
||||
(props.valueSuggestions || {})[resolvedField] || [];
|
||||
const lowerPartial: string = partialValue.toLowerCase();
|
||||
const exactMatch: string | undefined = availableValues.find(
|
||||
(v: string): boolean => {
|
||||
return v.toLowerCase() === lowerPartial;
|
||||
},
|
||||
);
|
||||
|
||||
// Use exact match, or if there's exactly one prefix match, use that
|
||||
const resolvedMatch: string | undefined =
|
||||
exactMatch ||
|
||||
(filteredSuggestions.length === 1
|
||||
? filteredSuggestions[0]
|
||||
: undefined);
|
||||
|
||||
if (resolvedMatch) {
|
||||
props.onFieldValueSelect(fieldPrefix, resolvedMatch);
|
||||
// Remove the field:value term from text
|
||||
const parts: Array<string> = props.value.split(/\s+/);
|
||||
parts.pop();
|
||||
const remaining: string = parts.join(" ");
|
||||
props.onChange(remaining ? remaining + " " : "");
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
props.onSubmit();
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
return;
|
||||
}
|
||||
|
||||
props.onSubmit();
|
||||
if (e.key === "Escape") {
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldShowSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelectedSuggestionIndex((prev: number): number => {
|
||||
return Math.min(prev + 1, filteredSuggestions.length - 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelectedSuggestionIndex((prev: number): number => {
|
||||
return Math.max(prev - 1, 0);
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
shouldShowSuggestions,
|
||||
selectedSuggestionIndex,
|
||||
filteredSuggestions,
|
||||
isValueMode,
|
||||
fieldPrefix,
|
||||
partialValue,
|
||||
props,
|
||||
],
|
||||
);
|
||||
|
||||
const applySuggestion: (suggestion: string) => void = useCallback(
|
||||
(suggestion: string): void => {
|
||||
if (isValueMode) {
|
||||
// Value mode: apply as a chip via onFieldValueSelect
|
||||
if (props.onFieldValueSelect) {
|
||||
props.onFieldValueSelect(fieldPrefix, suggestion);
|
||||
}
|
||||
|
||||
// Remove the current field:value term from the search text
|
||||
const parts: Array<string> = props.value.split(/\s+/);
|
||||
parts.pop(); // remove the field:partialValue
|
||||
const remaining: string = parts.join(" ");
|
||||
props.onChange(remaining ? remaining + " " : "");
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "Escape") {
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldShowSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setSelectedSuggestionIndex((prev: number): number => {
|
||||
return Math.min(prev + 1, filteredSuggestions.length - 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setSelectedSuggestionIndex((prev: number): number => {
|
||||
return Math.max(prev - 1, 0);
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
shouldShowSuggestions,
|
||||
selectedSuggestionIndex,
|
||||
filteredSuggestions,
|
||||
isValueMode,
|
||||
fieldPrefix,
|
||||
partialValue,
|
||||
props,
|
||||
],
|
||||
);
|
||||
|
||||
const applySuggestion: (suggestion: string) => void = useCallback(
|
||||
(suggestion: string): void => {
|
||||
if (isValueMode) {
|
||||
// Value mode: apply as a chip via onFieldValueSelect
|
||||
if (props.onFieldValueSelect) {
|
||||
props.onFieldValueSelect(fieldPrefix, suggestion);
|
||||
}
|
||||
|
||||
// Remove the current field:value term from the search text
|
||||
// Field name mode: append colon
|
||||
const parts: Array<string> = props.value.split(/\s+/);
|
||||
parts.pop(); // remove the field:partialValue
|
||||
const remaining: string = parts.join(" ");
|
||||
props.onChange(remaining ? remaining + " " : "");
|
||||
|
||||
if (parts.length > 0) {
|
||||
parts[parts.length - 1] = suggestion + ":";
|
||||
}
|
||||
|
||||
props.onChange(parts.join(" "));
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
},
|
||||
[props, isValueMode, fieldPrefix],
|
||||
);
|
||||
|
||||
// Field name mode: append colon
|
||||
const parts: Array<string> = props.value.split(/\s+/);
|
||||
|
||||
if (parts.length > 0) {
|
||||
parts[parts.length - 1] = suggestion + ":";
|
||||
}
|
||||
|
||||
props.onChange(parts.join(" "));
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
[props, isValueMode, fieldPrefix],
|
||||
);
|
||||
|
||||
const handleExampleClick: (example: string) => void = useCallback(
|
||||
(example: string): void => {
|
||||
props.onChange(example);
|
||||
setShowHelp(false);
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
[props],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside: (e: MouseEvent) => void = (
|
||||
e: MouseEvent,
|
||||
): void => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setShowSuggestions(false);
|
||||
const handleExampleClick: (example: string) => void = useCallback(
|
||||
(example: string): void => {
|
||||
props.onChange(example);
|
||||
setShowHelp(false);
|
||||
}
|
||||
};
|
||||
inputRef.current?.focus();
|
||||
},
|
||||
[props],
|
||||
);
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const handleClickOutside: (e: MouseEvent) => void = (
|
||||
e: MouseEvent,
|
||||
): void => {
|
||||
if (
|
||||
containerRef.current &&
|
||||
!containerRef.current.contains(e.target as Node)
|
||||
) {
|
||||
setShowSuggestions(false);
|
||||
setShowHelp(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-lg border bg-white px-3 py-2 transition-colors ${
|
||||
isFocused
|
||||
? "border-indigo-400 ring-2 ring-indigo-100"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
icon={IconProp.Search}
|
||||
className="h-4 w-4 flex-none text-gray-400"
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={props.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.onChange(e.target.value);
|
||||
setShowSuggestions(true);
|
||||
setShowHelp(false);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
setShowSuggestions(true);
|
||||
if (props.value.length === 0) {
|
||||
setShowHelp(true);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
props.placeholder ||
|
||||
'Search logs... (e.g. severity:error service:api "connection refused")'
|
||||
}
|
||||
className="flex-1 bg-transparent font-mono text-sm text-gray-900 placeholder-gray-400 outline-none"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{props.value.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex-none rounded-full p-1 text-gray-400 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
props.onChange("");
|
||||
setShowHelp(true);
|
||||
setShowSuggestions(false);
|
||||
inputRef.current?.focus();
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-lg border bg-white px-3 py-2 transition-colors ${
|
||||
isFocused
|
||||
? "border-indigo-400 ring-2 ring-indigo-100"
|
||||
: "border-gray-200 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
icon={IconProp.Search}
|
||||
className="h-4 w-4 flex-none text-gray-400"
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={props.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
props.onChange(e.target.value);
|
||||
setShowSuggestions(true);
|
||||
setShowHelp(false);
|
||||
}}
|
||||
title="Clear search"
|
||||
>
|
||||
<Icon icon={IconProp.Close} className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
onFocus={() => {
|
||||
setIsFocused(true);
|
||||
setShowSuggestions(true);
|
||||
if (props.value.length === 0) {
|
||||
setShowHelp(true);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setIsFocused(false);
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
props.placeholder ||
|
||||
'Search logs... (e.g. severity:error service:api "connection refused")'
|
||||
}
|
||||
className="flex-1 bg-transparent font-mono text-sm text-gray-900 placeholder-gray-400 outline-none"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{props.value.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="flex-none rounded-full p-1 text-gray-400 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
props.onChange("");
|
||||
setShowHelp(true);
|
||||
setShowSuggestions(false);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
title="Clear search"
|
||||
>
|
||||
<Icon icon={IconProp.Close} className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowSuggestions && (
|
||||
<LogSearchSuggestions
|
||||
suggestions={filteredSuggestions}
|
||||
selectedIndex={selectedSuggestionIndex}
|
||||
onSelect={applySuggestion}
|
||||
fieldContext={isValueMode ? fieldPrefix : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowHelp && (
|
||||
<LogSearchHelp onExampleClick={handleExampleClick} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowSuggestions && (
|
||||
<LogSearchSuggestions
|
||||
suggestions={filteredSuggestions}
|
||||
selectedIndex={selectedSuggestionIndex}
|
||||
onSelect={applySuggestion}
|
||||
fieldContext={isValueMode ? fieldPrefix : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowHelp && <LogSearchHelp onExampleClick={handleExampleClick} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function extractCurrentWord(value: string): string {
|
||||
const parts: Array<string> = value.split(/\s+/);
|
||||
|
||||
@@ -195,11 +195,9 @@ const AnalyticsTooltip: FunctionComponent<AnalyticsTooltipProps> = (
|
||||
|
||||
const entries: Array<{ key: string; value: number; color: string }> =
|
||||
props.payload
|
||||
.filter(
|
||||
(entry: { value: number }): boolean => {
|
||||
return entry.value > 0;
|
||||
},
|
||||
)
|
||||
.filter((entry: { value: number }): boolean => {
|
||||
return entry.value > 0;
|
||||
})
|
||||
.map(
|
||||
(entry: {
|
||||
dataKey: string;
|
||||
@@ -253,27 +251,25 @@ const AnalyticsTooltip: FunctionComponent<AnalyticsTooltipProps> = (
|
||||
{formatTime(props.label)}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{entries.map(
|
||||
(entry: { key: string; value: number; color: string }) => {
|
||||
return (
|
||||
<div
|
||||
key={entry.key}
|
||||
className="flex items-center justify-between gap-8"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block h-2.5 w-2.5 rounded-[3px]"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-xs text-gray-600">{entry.key}</span>
|
||||
</div>
|
||||
<span className="font-mono text-xs font-semibold tabular-nums text-gray-800">
|
||||
{entry.value.toLocaleString()}
|
||||
</span>
|
||||
{entries.map((entry: { key: string; value: number; color: string }) => {
|
||||
return (
|
||||
<div
|
||||
key={entry.key}
|
||||
className="flex items-center justify-between gap-8"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="inline-block h-2.5 w-2.5 rounded-[3px]"
|
||||
style={{ backgroundColor: entry.color }}
|
||||
/>
|
||||
<span className="text-xs text-gray-600">{entry.key}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<span className="font-mono text-xs font-semibold tabular-nums text-gray-800">
|
||||
{entry.value.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{entries.length > 1 && (
|
||||
<div className="mt-2 flex items-center justify-between border-t border-gray-100 pt-2">
|
||||
@@ -477,15 +473,13 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
|
||||
backgroundPosition: "right 6px center",
|
||||
}}
|
||||
>
|
||||
{options.map(
|
||||
(opt: { value: string | number; label: string }) => {
|
||||
return (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
{options.map((opt: { value: string | number; label: string }) => {
|
||||
return (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
@@ -494,70 +488,89 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
|
||||
const renderQueryBuilder: () => ReactElement = (): ReactElement => {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-4 border-b border-gray-100 px-5 py-3">
|
||||
{renderSelectControl("Chart", chartType, (val: string) => {
|
||||
setChartType(val as AnalyticsChartType);
|
||||
}, [
|
||||
{ value: "timeseries", label: "Timeseries" },
|
||||
{ value: "toplist", label: "Top List" },
|
||||
{ value: "table", label: "Table" },
|
||||
])}
|
||||
{renderSelectControl(
|
||||
"Chart",
|
||||
chartType,
|
||||
(val: string) => {
|
||||
setChartType(val as AnalyticsChartType);
|
||||
},
|
||||
[
|
||||
{ value: "timeseries", label: "Timeseries" },
|
||||
{ value: "toplist", label: "Top List" },
|
||||
{ value: "table", label: "Table" },
|
||||
],
|
||||
)}
|
||||
|
||||
<div className="h-4 w-px bg-gray-200" />
|
||||
|
||||
{renderSelectControl("Measure", aggregation, (val: string) => {
|
||||
setAggregation(val as AnalyticsAggregation);
|
||||
}, [
|
||||
{ value: "count", label: "Count" },
|
||||
{ value: "unique", label: "Unique Count" },
|
||||
])}
|
||||
{renderSelectControl(
|
||||
"Measure",
|
||||
aggregation,
|
||||
(val: string) => {
|
||||
setAggregation(val as AnalyticsAggregation);
|
||||
},
|
||||
[
|
||||
{ value: "count", label: "Count" },
|
||||
{ value: "unique", label: "Unique Count" },
|
||||
],
|
||||
)}
|
||||
|
||||
{aggregation === "unique" && (
|
||||
<>
|
||||
{renderSelectControl("of", aggregationField, (val: string) => {
|
||||
setAggregationField(val);
|
||||
}, [
|
||||
{ value: "", label: "Select field..." },
|
||||
...allDimensionOptions,
|
||||
])}
|
||||
{renderSelectControl(
|
||||
"of",
|
||||
aggregationField,
|
||||
(val: string) => {
|
||||
setAggregationField(val);
|
||||
},
|
||||
[{ value: "", label: "Select field..." }, ...allDimensionOptions],
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-4 w-px bg-gray-200" />
|
||||
|
||||
{renderSelectControl("Group by", groupByFields[0] || "", (val: string) => {
|
||||
setGroupByFields((prev: Array<string>) => {
|
||||
const next: Array<string> = [...prev];
|
||||
next[0] = val;
|
||||
return next.filter((f: string) => {
|
||||
return f.length > 0;
|
||||
});
|
||||
});
|
||||
}, [
|
||||
{ value: "", label: "None" },
|
||||
...allDimensionOptions,
|
||||
])}
|
||||
|
||||
{groupByFields[0] && groupByFields[0].length > 0 && (
|
||||
renderSelectControl("then by", groupByFields[1] || "", (val: string) => {
|
||||
{renderSelectControl(
|
||||
"Group by",
|
||||
groupByFields[0] || "",
|
||||
(val: string) => {
|
||||
setGroupByFields((prev: Array<string>) => {
|
||||
const next: Array<string> = [prev[0] || ""];
|
||||
|
||||
if (val.length > 0) {
|
||||
next.push(val);
|
||||
}
|
||||
|
||||
const next: Array<string> = [...prev];
|
||||
next[0] = val;
|
||||
return next.filter((f: string) => {
|
||||
return f.length > 0;
|
||||
});
|
||||
});
|
||||
}, [
|
||||
{ value: "", label: "None" },
|
||||
...allDimensionOptions.filter((opt: { value: string }) => {
|
||||
return opt.value !== groupByFields[0];
|
||||
}),
|
||||
])
|
||||
},
|
||||
[{ value: "", label: "None" }, ...allDimensionOptions],
|
||||
)}
|
||||
|
||||
{groupByFields[0] &&
|
||||
groupByFields[0].length > 0 &&
|
||||
renderSelectControl(
|
||||
"then by",
|
||||
groupByFields[1] || "",
|
||||
(val: string) => {
|
||||
setGroupByFields((prev: Array<string>) => {
|
||||
const next: Array<string> = [prev[0] || ""];
|
||||
|
||||
if (val.length > 0) {
|
||||
next.push(val);
|
||||
}
|
||||
|
||||
return next.filter((f: string) => {
|
||||
return f.length > 0;
|
||||
});
|
||||
});
|
||||
},
|
||||
[
|
||||
{ value: "", label: "None" },
|
||||
...allDimensionOptions.filter((opt: { value: string }) => {
|
||||
return opt.value !== groupByFields[0];
|
||||
}),
|
||||
],
|
||||
)}
|
||||
|
||||
{(chartType === "toplist" || chartType === "table") && (
|
||||
<>
|
||||
<div className="h-4 w-px bg-gray-200" />
|
||||
@@ -590,8 +603,7 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
|
||||
<span
|
||||
className="inline-block h-2.5 w-2.5 rounded-[3px]"
|
||||
style={{
|
||||
backgroundColor:
|
||||
CHART_COLORS[index % CHART_COLORS.length],
|
||||
backgroundColor: CHART_COLORS[index % CHART_COLORS.length],
|
||||
}}
|
||||
/>
|
||||
<span className="text-[11px] text-gray-500">{key}</span>
|
||||
@@ -769,9 +781,7 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
|
||||
{topListData.map((item: AnalyticsTopItem, index: number) => {
|
||||
const percentage: number = (item.count / maxCount) * 100;
|
||||
const sharePercent: number =
|
||||
totalCount > 0
|
||||
? Math.round((item.count / totalCount) * 100)
|
||||
: 0;
|
||||
totalCount > 0 ? Math.round((item.count / totalCount) * 100) : 0;
|
||||
const color: string =
|
||||
CHART_COLORS[index % CHART_COLORS.length] || CHART_COLORS[0]!;
|
||||
const mutedColor: string =
|
||||
@@ -866,8 +876,7 @@ const LogsAnalyticsView: FunctionComponent<LogsAnalyticsViewProps> = (
|
||||
{tableData.map((row: AnalyticsTableRow, index: number) => {
|
||||
const barWidth: number = (row.count / maxCount) * 100;
|
||||
const color: string =
|
||||
CHART_COLORS[index % CHART_COLORS.length] ||
|
||||
CHART_COLORS[0]!;
|
||||
CHART_COLORS[index % CHART_COLORS.length] || CHART_COLORS[0]!;
|
||||
const mutedColor: string =
|
||||
CHART_COLORS_MUTED[index % CHART_COLORS_MUTED.length] ||
|
||||
CHART_COLORS_MUTED[0]!;
|
||||
|
||||
@@ -13,36 +13,38 @@ export interface LogsFilterCardProps {
|
||||
|
||||
const LogsFilterCard: React.ForwardRefExoticComponent<
|
||||
LogsFilterCardProps & React.RefAttributes<LogSearchBarRef>
|
||||
> = forwardRef<LogSearchBarRef, LogsFilterCardProps>(function LogsFilterCard(
|
||||
props: LogsFilterCardProps,
|
||||
ref: React.Ref<LogSearchBarRef>,
|
||||
): ReactElement {
|
||||
const searchBarSuggestions: Array<string> = [
|
||||
"severity",
|
||||
"service",
|
||||
"trace",
|
||||
"span",
|
||||
...props.logAttributes.map((attr: string) => {
|
||||
return `@${attr}`;
|
||||
}),
|
||||
];
|
||||
> = forwardRef<LogSearchBarRef, LogsFilterCardProps>(
|
||||
(
|
||||
props: LogsFilterCardProps,
|
||||
ref: React.Ref<LogSearchBarRef>,
|
||||
): ReactElement => {
|
||||
const searchBarSuggestions: Array<string> = [
|
||||
"severity",
|
||||
"service",
|
||||
"trace",
|
||||
"span",
|
||||
...props.logAttributes.map((attr: string) => {
|
||||
return `@${attr}`;
|
||||
}),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<LogSearchBar
|
||||
ref={ref}
|
||||
value={props.searchQuery}
|
||||
onChange={props.onSearchQueryChange}
|
||||
onSubmit={props.onSearchSubmit}
|
||||
suggestions={searchBarSuggestions}
|
||||
valueSuggestions={props.valueSuggestions}
|
||||
onFieldValueSelect={props.onFieldValueSelect}
|
||||
/>
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
<LogSearchBar
|
||||
ref={ref}
|
||||
value={props.searchQuery}
|
||||
onChange={props.onSearchQueryChange}
|
||||
onSubmit={props.onSearchSubmit}
|
||||
suggestions={searchBarSuggestions}
|
||||
valueSuggestions={props.valueSuggestions}
|
||||
onFieldValueSelect={props.onFieldValueSelect}
|
||||
/>
|
||||
</div>
|
||||
<div>{props.toolbar}</div>
|
||||
</div>
|
||||
<div>{props.toolbar}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default LogsFilterCard;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import React, { FunctionComponent, ReactElement, useRef, useState } from "react";
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
ReactElement,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import LiveLogsToggle from "./LiveLogsToggle";
|
||||
import LogTimeRangePicker from "./LogTimeRangePicker";
|
||||
import ColumnSelector from "./ColumnSelector";
|
||||
|
||||
@@ -130,8 +130,7 @@ export class LogPipelineService {
|
||||
logRow: JSONObject,
|
||||
processor: LogPipelineProcessor,
|
||||
): JSONObject {
|
||||
const config: JSONObject =
|
||||
(processor.configuration as JSONObject) || {};
|
||||
const config: JSONObject = (processor.configuration as JSONObject) || {};
|
||||
|
||||
switch (processor.processorType) {
|
||||
case LogPipelineProcessorType.AttributeRemapper:
|
||||
|
||||
@@ -152,11 +152,7 @@ export class LogScrubRuleService {
|
||||
if (atIndex > 0) {
|
||||
const dotIndex: number = value.lastIndexOf(".");
|
||||
if (dotIndex > atIndex) {
|
||||
return (
|
||||
value[0] +
|
||||
"***@***" +
|
||||
value.substring(dotIndex)
|
||||
);
|
||||
return value[0] + "***@***" + value.substring(dotIndex);
|
||||
}
|
||||
}
|
||||
return "***@***.***";
|
||||
@@ -178,9 +174,7 @@ export class LogScrubRuleService {
|
||||
// Show last 4 digits only
|
||||
const phoneDigits: string = value.replace(/[^0-9]/g, "");
|
||||
if (phoneDigits.length >= 4) {
|
||||
return (
|
||||
"***-***-" + phoneDigits.substring(phoneDigits.length - 4)
|
||||
);
|
||||
return "***-***-" + phoneDigits.substring(phoneDigits.length - 4);
|
||||
}
|
||||
return "***-***-****";
|
||||
}
|
||||
@@ -194,7 +188,9 @@ export class LogScrubRuleService {
|
||||
return "***";
|
||||
}
|
||||
return (
|
||||
value[0] + "*".repeat(Math.max(value.length - 2, 3)) + value[value.length - 1]!
|
||||
value[0] +
|
||||
"*".repeat(Math.max(value.length - 2, 3)) +
|
||||
value[value.length - 1]!
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,7 +206,8 @@ export class LogScrubRuleService {
|
||||
// Reset lastIndex for global regex
|
||||
regex.lastIndex = 0;
|
||||
|
||||
const action: string = (rule.scrubAction as string) || LogScrubAction.Redact;
|
||||
const action: string =
|
||||
(rule.scrubAction as string) || LogScrubAction.Redact;
|
||||
const patternType: string = (rule.patternType as string) || "";
|
||||
|
||||
result = result.replace(regex, (match: string) => {
|
||||
@@ -230,12 +227,13 @@ export class LogScrubRuleService {
|
||||
}
|
||||
|
||||
for (const { rule } of compiledRules) {
|
||||
const fieldsToScrub: string =
|
||||
(rule.fieldsToScrub as string) || "both";
|
||||
const fieldsToScrub: string = (rule.fieldsToScrub as string) || "both";
|
||||
|
||||
// Filter compiled rules to just this one for per-rule scrubbing
|
||||
const singleRule: Array<CompiledRule> = compiledRules.filter(
|
||||
(cr: CompiledRule) => cr.rule === rule,
|
||||
(cr: CompiledRule) => {
|
||||
return cr.rule === rule;
|
||||
},
|
||||
);
|
||||
|
||||
// Scrub body
|
||||
@@ -243,10 +241,7 @@ export class LogScrubRuleService {
|
||||
(fieldsToScrub === "body" || fieldsToScrub === "both") &&
|
||||
typeof logRow["body"] === "string"
|
||||
) {
|
||||
logRow["body"] = this.scrubString(
|
||||
logRow["body"] as string,
|
||||
singleRule,
|
||||
);
|
||||
logRow["body"] = this.scrubString(logRow["body"] as string, singleRule);
|
||||
}
|
||||
|
||||
// Scrub attributes
|
||||
|
||||
@@ -107,8 +107,7 @@ export default class OtelLogsIngestService extends OtelIngestBaseService {
|
||||
loadedPipelines = await LogPipelineService.loadPipelines(projectId);
|
||||
loadedDropFilters =
|
||||
await LogDropFilterService.loadDropFilters(projectId);
|
||||
loadedScrubRules =
|
||||
await LogScrubRuleService.loadScrubRules(projectId);
|
||||
loadedScrubRules = await LogScrubRuleService.loadScrubRules(projectId);
|
||||
} catch (loadError) {
|
||||
logger.error("Error loading pipelines/drop filters/scrub rules:");
|
||||
logger.error(loadError);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { JSONObject } from "Common/Types/JSON";
|
||||
|
||||
// Simple filter evaluator for log rows used by pipelines and drop filters.
|
||||
// Supports: =, !=, LIKE, IN, AND, OR, NOT, parentheses
|
||||
// Field paths: severityText, body, serviceId, attributes.<key>
|
||||
/*
|
||||
* Simple filter evaluator for log rows used by pipelines and drop filters.
|
||||
* Supports: =, !=, LIKE, IN, AND, OR, NOT, parentheses
|
||||
* Field paths: severityText, body, serviceId, attributes.<key>
|
||||
*/
|
||||
|
||||
interface Token {
|
||||
type:
|
||||
@@ -46,7 +48,7 @@ function tokenize(query: string): Array<Token> {
|
||||
|
||||
while (i < len) {
|
||||
// Skip whitespace
|
||||
if (/\s/.test(query[i]!)) {
|
||||
if ((/\s/).test(query[i]!)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -120,7 +122,7 @@ function tokenize(query: string): Array<Token> {
|
||||
|
||||
// Field name or unquoted value
|
||||
let word: string = "";
|
||||
while (i < len && !/[\s()=!]/.test(query[i]!)) {
|
||||
while (i < len && !(/[\s()=!]/).test(query[i]!)) {
|
||||
word += query[i];
|
||||
i++;
|
||||
}
|
||||
@@ -182,7 +184,10 @@ class Parser {
|
||||
|
||||
private parseOr(): FilterExpression {
|
||||
let left: FilterExpression = this.parseAnd();
|
||||
while (this.pos < this.tokens.length && this.tokens[this.pos]!.type === "or") {
|
||||
while (
|
||||
this.pos < this.tokens.length &&
|
||||
this.tokens[this.pos]!.type === "or"
|
||||
) {
|
||||
this.pos++;
|
||||
const right: FilterExpression = this.parseAnd();
|
||||
left = { type: "or", left, right } as OrExpr;
|
||||
@@ -192,7 +197,10 @@ class Parser {
|
||||
|
||||
private parseAnd(): FilterExpression {
|
||||
let left: FilterExpression = this.parseUnary();
|
||||
while (this.pos < this.tokens.length && this.tokens[this.pos]!.type === "and") {
|
||||
while (
|
||||
this.pos < this.tokens.length &&
|
||||
this.tokens[this.pos]!.type === "and"
|
||||
) {
|
||||
this.pos++;
|
||||
const right: FilterExpression = this.parseUnary();
|
||||
left = { type: "and", left, right } as AndExpr;
|
||||
@@ -349,7 +357,10 @@ function evaluateExpr(logRow: JSONObject, expr: FilterExpression): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function evaluateFilter(logRow: JSONObject, filterQuery: string): boolean {
|
||||
export function evaluateFilter(
|
||||
logRow: JSONObject,
|
||||
filterQuery: string,
|
||||
): boolean {
|
||||
if (!filterQuery || filterQuery.trim().length === 0) {
|
||||
return true; // empty filter matches everything
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user