mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-08 17:08:58 +08:00
Feat: fe: logs pipelines timestamp parsing processor (#4106)
* chore: add processor config for time parsing processor * chore: add select input and processor fields with enumerated options * feat: set timestamp layout to default value when layout_type is changed * chore: minor cleanup * chore: some more cleanup * chore: some more cleanup * chore: get jest passing * chore: normalize ts in pipelines previews input and output * chore: some cleanup * fix: set correct field id for timestamp format input
This commit is contained in:
parent
d6f0559adc
commit
fc5f0fbf9e
@ -0,0 +1,78 @@
|
|||||||
|
import { Form, Input, Select } from 'antd';
|
||||||
|
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { formValidationRules } from '../config';
|
||||||
|
import { processorFields, ProcessorFormField } from './config';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
FormWrapper,
|
||||||
|
PipelineIndexIcon,
|
||||||
|
StyledSelect,
|
||||||
|
} from './styles';
|
||||||
|
|
||||||
|
function ProcessorFieldInput({
|
||||||
|
fieldData,
|
||||||
|
}: ProcessorFieldInputProps): JSX.Element | null {
|
||||||
|
const { t } = useTranslation('pipeline');
|
||||||
|
|
||||||
|
// Watch form values so we can evaluate shouldRender on
|
||||||
|
// conditional fields when form values are updated.
|
||||||
|
const form = Form.useFormInstance();
|
||||||
|
Form.useWatch(fieldData?.dependencies || [], form);
|
||||||
|
|
||||||
|
if (fieldData.shouldRender && !fieldData.shouldRender(form)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<PipelineIndexIcon size="small">
|
||||||
|
{Number(fieldData.id) + 1}
|
||||||
|
</PipelineIndexIcon>
|
||||||
|
<FormWrapper>
|
||||||
|
<Form.Item
|
||||||
|
required={false}
|
||||||
|
label={<ModalFooterTitle>{fieldData.fieldName}</ModalFooterTitle>}
|
||||||
|
key={fieldData.id}
|
||||||
|
name={fieldData.name}
|
||||||
|
initialValue={fieldData.initialValue}
|
||||||
|
rules={fieldData.rules ? fieldData.rules : formValidationRules}
|
||||||
|
dependencies={fieldData.dependencies || []}
|
||||||
|
>
|
||||||
|
{fieldData?.options ? (
|
||||||
|
<StyledSelect>
|
||||||
|
{fieldData.options.map(({ value, label }) => (
|
||||||
|
<Select.Option key={value + label} value={value}>
|
||||||
|
{label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</StyledSelect>
|
||||||
|
) : (
|
||||||
|
<Input placeholder={t(fieldData.placeholder)} />
|
||||||
|
)}
|
||||||
|
</Form.Item>
|
||||||
|
</FormWrapper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessorFieldInputProps {
|
||||||
|
fieldData: ProcessorFormField;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessorForm({ processorType }: ProcessorFormProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
|
||||||
|
<ProcessorFieldInput key={fieldData.id} fieldData={fieldData} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProcessorFormProps {
|
||||||
|
processorType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessorForm;
|
@ -1,5 +1,7 @@
|
|||||||
|
import { FormInstance } from 'antd';
|
||||||
import { Rule, RuleRender } from 'antd/es/form';
|
import { Rule, RuleRender } from 'antd/es/form';
|
||||||
import { NamePath } from 'antd/es/form/interface';
|
import { NamePath } from 'antd/es/form/interface';
|
||||||
|
import { ProcessorData } from 'types/api/pipeline/def';
|
||||||
|
|
||||||
type ProcessorType = {
|
type ProcessorType = {
|
||||||
key: string;
|
key: string;
|
||||||
@ -14,6 +16,7 @@ export const processorTypes: Array<ProcessorType> = [
|
|||||||
{ key: 'regex_parser', value: 'regex_parser', label: 'Regex' },
|
{ key: 'regex_parser', value: 'regex_parser', label: 'Regex' },
|
||||||
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
|
{ key: 'json_parser', value: 'json_parser', label: 'Json Parser' },
|
||||||
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
|
{ key: 'trace_parser', value: 'trace_parser', label: 'Trace Parser' },
|
||||||
|
{ key: 'time_parser', value: 'time_parser', label: 'Timestamp Parser' },
|
||||||
{ key: 'add', value: 'add', label: 'Add' },
|
{ key: 'add', value: 'add', label: 'Add' },
|
||||||
{ key: 'remove', value: 'remove', label: 'Remove' },
|
{ key: 'remove', value: 'remove', label: 'Remove' },
|
||||||
// { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion
|
// { key: 'retain', value: 'retain', label: 'Retain' }, @Chintan - Commented as per Nitya's suggestion
|
||||||
@ -23,6 +26,11 @@ export const processorTypes: Array<ProcessorType> = [
|
|||||||
|
|
||||||
export const DEFAULT_PROCESSOR_TYPE = processorTypes[0].value;
|
export const DEFAULT_PROCESSOR_TYPE = processorTypes[0].value;
|
||||||
|
|
||||||
|
export type ProcessorFieldOption = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ProcessorFormField = {
|
export type ProcessorFormField = {
|
||||||
id: number;
|
id: number;
|
||||||
fieldName: string;
|
fieldName: string;
|
||||||
@ -31,6 +39,12 @@ export type ProcessorFormField = {
|
|||||||
rules?: Array<Rule>;
|
rules?: Array<Rule>;
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
dependencies?: Array<string | NamePath>;
|
dependencies?: Array<string | NamePath>;
|
||||||
|
options?: Array<ProcessorFieldOption>;
|
||||||
|
shouldRender?: (form: FormInstance) => boolean;
|
||||||
|
onFormValuesChanged?: (
|
||||||
|
changedValues: ProcessorData,
|
||||||
|
form: FormInstance,
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const traceParserFieldValidator: RuleRender = (form) => ({
|
const traceParserFieldValidator: RuleRender = (form) => ({
|
||||||
@ -206,6 +220,103 @@ export const processorFields: { [key: string]: Array<ProcessorFormField> } = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
time_parser: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
fieldName: 'Name of Timestamp Parsing Processor',
|
||||||
|
placeholder: 'processor_name_placeholder',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
fieldName: 'Parse Timestamp Value From',
|
||||||
|
placeholder: 'processor_parsefrom_placeholder',
|
||||||
|
name: 'parse_from',
|
||||||
|
initialValue: 'attributes.timestamp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
fieldName: 'Timestamp Format Type',
|
||||||
|
placeholder: '',
|
||||||
|
name: 'layout_type',
|
||||||
|
initialValue: 'strptime',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'Unix Epoch',
|
||||||
|
value: 'epoch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'strptime Format',
|
||||||
|
value: 'strptime',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onFormValuesChanged: (
|
||||||
|
changedValues: ProcessorData,
|
||||||
|
form: FormInstance,
|
||||||
|
): void => {
|
||||||
|
if (changedValues?.layout_type) {
|
||||||
|
const newLayoutValue =
|
||||||
|
changedValues.layout_type === 'strptime' ? '%Y-%m-%dT%H:%M:%S.%f%z' : 's';
|
||||||
|
|
||||||
|
form.setFieldValue('layout', newLayoutValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
fieldName: 'Epoch Format',
|
||||||
|
placeholder: '',
|
||||||
|
name: 'layout',
|
||||||
|
dependencies: ['layout_type'],
|
||||||
|
shouldRender: (form: FormInstance): boolean => {
|
||||||
|
const layoutType = form.getFieldValue('layout_type');
|
||||||
|
return layoutType === 'epoch';
|
||||||
|
},
|
||||||
|
initialValue: 's',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'seconds',
|
||||||
|
value: 's',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'milliseconds',
|
||||||
|
value: 'ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'microseconds',
|
||||||
|
value: 'us',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'nanoseconds',
|
||||||
|
value: 'ns',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'seconds.milliseconds (eg: 1136214245.123)',
|
||||||
|
value: 's.ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'seconds.microseconds (eg: 1136214245.123456)',
|
||||||
|
value: 's.us',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'seconds.nanoseconds (eg: 1136214245.123456789)',
|
||||||
|
value: 's.ns',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
fieldName: 'Timestamp Format',
|
||||||
|
placeholder: 'strptime directives based format. Eg: %Y-%m-%dT%H:%M:%S.%f%z',
|
||||||
|
name: 'layout',
|
||||||
|
dependencies: ['layout_type'],
|
||||||
|
shouldRender: (form: FormInstance): boolean => {
|
||||||
|
const layoutType = form.getFieldValue('layout_type');
|
||||||
|
return layoutType === 'strptime';
|
||||||
|
},
|
||||||
|
initialValue: '%Y-%m-%dT%H:%M:%S.%f%z',
|
||||||
|
},
|
||||||
|
],
|
||||||
retain: [
|
retain: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -11,9 +11,9 @@ import { v4 } from 'uuid';
|
|||||||
|
|
||||||
import { ModalButtonWrapper, ModalTitle } from '../styles';
|
import { ModalButtonWrapper, ModalTitle } from '../styles';
|
||||||
import { getEditedDataSource, getRecordIndex } from '../utils';
|
import { getEditedDataSource, getRecordIndex } from '../utils';
|
||||||
import { DEFAULT_PROCESSOR_TYPE } from './config';
|
import { DEFAULT_PROCESSOR_TYPE, processorFields } from './config';
|
||||||
import TypeSelect from './FormFields/TypeSelect';
|
import TypeSelect from './FormFields/TypeSelect';
|
||||||
import { renderProcessorForm } from './utils';
|
import ProcessorForm from './ProcessorForm';
|
||||||
|
|
||||||
function AddNewProcessor({
|
function AddNewProcessor({
|
||||||
isActionType,
|
isActionType,
|
||||||
@ -141,6 +141,17 @@ function AddNewProcessor({
|
|||||||
|
|
||||||
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
|
const isOpen = useMemo(() => isEdit || isAdd, [isAdd, isEdit]);
|
||||||
|
|
||||||
|
const onFormValuesChanged = useCallback(
|
||||||
|
(changedValues: ProcessorData): void => {
|
||||||
|
processorFields[processorType].forEach((field) => {
|
||||||
|
if (field.onFormValuesChanged) {
|
||||||
|
field.onFormValuesChanged(changedValues, form);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[form, processorType],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
|
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
|
||||||
@ -157,9 +168,10 @@ function AddNewProcessor({
|
|||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
form={form}
|
form={form}
|
||||||
|
onValuesChange={onFormValuesChanged}
|
||||||
>
|
>
|
||||||
<TypeSelect value={processorType} onChange={handleProcessorType} />
|
<TypeSelect value={processorType} onChange={handleProcessorType} />
|
||||||
{renderProcessorForm(processorType)}
|
<ProcessorForm processorType={processorType} />
|
||||||
<Divider plain />
|
<Divider plain />
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<ModalButtonWrapper>
|
<ModalButtonWrapper>
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { processorFields, ProcessorFormField } from './config';
|
|
||||||
import NameInput from './FormFields/NameInput';
|
|
||||||
|
|
||||||
export const renderProcessorForm = (
|
|
||||||
processorType: string,
|
|
||||||
): Array<JSX.Element> =>
|
|
||||||
processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
|
|
||||||
<NameInput key={fieldData.id} fieldData={fieldData} />
|
|
||||||
));
|
|
@ -27,7 +27,8 @@ const usePipelinePreview = ({
|
|||||||
// ILog allows both number and string while the API needs a number
|
// ILog allows both number and string while the API needs a number
|
||||||
const simulationInput = inputLogs.map((l) => ({
|
const simulationInput = inputLogs.map((l) => ({
|
||||||
...l,
|
...l,
|
||||||
timestamp: new Date(l.timestamp).getTime(),
|
// log timestamps in query service API are unix nanos
|
||||||
|
timestamp: new Date(l.timestamp).getTime() * 10 ** 6,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const response = useQuery<PipelineSimulationResponse, AxiosError>({
|
const response = useQuery<PipelineSimulationResponse, AxiosError>({
|
||||||
@ -42,9 +43,15 @@ const usePipelinePreview = ({
|
|||||||
|
|
||||||
const { isFetching, isError, data, error } = response;
|
const { isFetching, isError, data, error } = response;
|
||||||
|
|
||||||
|
const outputLogs = (data?.logs || []).map((l: ILog) => ({
|
||||||
|
...l,
|
||||||
|
// log timestamps in query service API are unix nanos
|
||||||
|
timestamp: (l.timestamp as number) / 10 ** 6,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading: isFetching,
|
isLoading: isFetching,
|
||||||
outputLogs: data?.logs || [],
|
outputLogs,
|
||||||
isError,
|
isError,
|
||||||
errorMsg: error?.response?.data?.error || '',
|
errorMsg: error?.response?.data?.error || '',
|
||||||
};
|
};
|
||||||
|
@ -27,6 +27,10 @@ export interface ProcessorData {
|
|||||||
trace_flags?: {
|
trace_flags?: {
|
||||||
parse_from: string;
|
parse_from: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// time parser fields
|
||||||
|
layout_type?: string;
|
||||||
|
layout?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PipelineData {
|
export interface PipelineData {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user