diff --git a/pkg/server/server.go b/pkg/server/server.go index c542851e8..af0266411 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -40,6 +40,7 @@ type PluginConfig struct { AlertingRuleNamespaceLabelKey string `json:"alertingRuleNamespaceLabelKey,omitempty" yaml:"alertingRuleNamespaceLabelKey,omitempty"` Timeout time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` LogsLimit int `json:"logsLimit,omitempty" yaml:"logsLimit,omitempty"` + Schema string `json:"schema,omitempty" yaml:"schema,omitempty"` } func (pluginConfig *PluginConfig) MarshalJSON() ([]byte, error) { diff --git a/web/cypress/integration/logs-dev-page.cy.ts b/web/cypress/integration/logs-dev-page.cy.ts index 1789ca624..a1e808c7e 100644 --- a/web/cypress/integration/logs-dev-page.cy.ts +++ b/web/cypress/integration/logs-dev-page.cy.ts @@ -160,9 +160,7 @@ describe('Logs Dev Page', () => { cy.wait('@queryRangeStreams').then(({ request }) => { const url = new URL(request.url); const query = url.searchParams.get('query'); - expect(query).to.equal( - '{ log_type="application", kubernetes_namespace_name="my-namespace" } | json', - ); + expect(query).to.equal('{ kubernetes_namespace_name="my-namespace" } | json'); }); }); @@ -186,9 +184,7 @@ describe('Logs Dev Page', () => { cy.wait('@queryRangeStreams').then(({ request }) => { const url = new URL(request.url); const query = url.searchParams.get('query'); - expect(query).to.equal( - '{ log_type="application", kubernetes_namespace_name="my-namespace" } | json', - ); + expect(query).to.equal('{ kubernetes_namespace_name="my-namespace" } | json'); }); cy.getByTestId(TestIds.NamespaceToggle).click(); @@ -197,9 +193,7 @@ describe('Logs Dev Page', () => { cy.wait('@queryRangeStreams').then(({ request }) => { const url = new URL(request.url); const query = url.searchParams.get('query'); - expect(query).to.equal( - '{ log_type="application", kubernetes_namespace_name="my-namespace-two" } | json', - ); + expect(query).to.equal('{ kubernetes_namespace_name="my-namespace-two" } | json'); }); }); @@ -231,9 +225,7 @@ describe('Logs Dev Page', () => { cy.wait('@queryRangeStreams').then(({ request }) => { const url = new URL(request.url); const query = url.searchParams.get('query'); - expect(query).to.equal( - '{ log_type="application", kubernetes_namespace_name="my-namespace" } | json', - ); + expect(query).to.equal('{ kubernetes_namespace_name="my-namespace" } | json'); }); cy.getByTestId(TestIds.NamespaceToggle).click(); @@ -246,9 +238,7 @@ describe('Logs Dev Page', () => { '/api/proxy/plugin/logging-view-plugin/backend/api/logs/v1/infrastructure/loki/api/v1/query_range', ); const query = url.searchParams.get('query'); - expect(query).to.equal( - '{ log_type="infrastructure", kubernetes_namespace_name="openshift-cluster-version" } | json', - ); + expect(query).to.equal('{ kubernetes_namespace_name="openshift-cluster-version" } | json'); }); }); diff --git a/web/cypress/integration/logs-page.cy.ts b/web/cypress/integration/logs-page.cy.ts index cffb8dcb9..423f2e143 100644 --- a/web/cypress/integration/logs-page.cy.ts +++ b/web/cypress/integration/logs-page.cy.ts @@ -410,7 +410,7 @@ describe('Logs Page', () => { .invoke('val') .should( 'equal', - '{ log_type="application", kubernetes_namespace_name="gitops" } |= `line filter` | json | level=~"error|err|eror|info|inf|information|notice"', + '{ kubernetes_namespace_name="gitops" } |= `line filter` | json | level=~"error|err|eror|info|inf|information|notice"', ); }); @@ -682,7 +682,7 @@ describe('Logs Page', () => { .invoke('val') .should( 'equal', - '{ log_type="application", kubernetes_container_name="operator", kubernetes_pod_name="my-pod-2" } | json', + '{ kubernetes_container_name="operator", kubernetes_pod_name="my-pod-2" } | json', ); }); diff --git a/web/locales/en/plugin__logging-view-plugin.json b/web/locales/en/plugin__logging-view-plugin.json index 8a87f9958..f7c7c1c60 100644 --- a/web/locales/en/plugin__logging-view-plugin.json +++ b/web/locales/en/plugin__logging-view-plugin.json @@ -24,7 +24,7 @@ "No datapoints found": "No datapoints found", "No data": "No data", "Invalid data": "Invalid data", - "Invalid log stream selector. Please select a namespace, pod or container as filter, or add a log stream selector like: ": "Invalid log stream selector. Please select a namespace, pod or container as filter, or add a log stream selector like: ", + "Invalid log stream selector. Please select a namespace, pod or container as filter.": "Invalid log stream selector. Please select a namespace, pod or container as filter.", "Show Resources": "Show Resources", "Hide Resources": "Hide Resources", "Show Stats": "Show Stats", diff --git a/web/src/__tests__/attribute-filters.spec.ts b/web/src/__tests__/attribute-filters.spec.ts index 8ae9a95cf..bd892ac81 100644 --- a/web/src/__tests__/attribute-filters.spec.ts +++ b/web/src/__tests__/attribute-filters.spec.ts @@ -7,48 +7,58 @@ import { getSeverityFilterPipelineStage, queryFromFilters, } from '../attribute-filters'; -import { AttributeList } from '../components/filters/filter.types'; +import { AttributeList, Filters } from '../components/filters/filter.types'; +import { LabelMatcher, PipelineStage } from '../logql-query'; +import { Schema } from '../logs.types'; +import { getStreamLabelsFromSchema, ResourceLabel } from '../parse-resources'; -export const availableAttributes: AttributeList = [ - { - name: 'Content', - id: 'content', - valueType: 'text', - }, - { - name: 'Namespaces', - label: 'kubernetes_namespace_name', - id: 'namespace', - options: () => - Promise.resolve([ - { option: 'Namespace 1', value: 'namespace-1' }, - { option: 'Namespace 2', value: 'namespace-2' }, - ]), - valueType: 'checkbox-select', - }, - { - name: 'Pods', - label: 'kubernetes_pod_name', - id: 'pod', - options: () => - Promise.resolve([ - { option: 'Pod 1', value: 'pod-1' }, - { option: 'Pod 2', value: 'pod-2' }, - ]), - valueType: 'checkbox-select', - }, - { - name: 'Containers', - label: 'kubernetes_container_name', - id: 'container', - options: () => - Promise.resolve([ - { option: 'Container 1', value: 'container-1' }, - { value: 'container-2', option: 'Container 2' }, - ]), - valueType: 'checkbox-select', - }, -]; +const availableAttributes = (schema: Schema): AttributeList => { + const labelMatchers = getStreamLabelsFromSchema(schema); + const namespaceLabel = labelMatchers[ResourceLabel.Namespace]; + const podLabel = labelMatchers[ResourceLabel.Pod]; + const containerLabel = labelMatchers[ResourceLabel.Container]; + + return [ + { + name: 'Content', + id: 'content', + valueType: 'text', + }, + { + name: 'Namespaces', + label: namespaceLabel, + id: 'namespace', + options: () => + Promise.resolve([ + { option: 'Namespace 1', value: 'namespace-1' }, + { option: 'Namespace 2', value: 'namespace-2' }, + ]), + valueType: 'checkbox-select', + }, + { + name: 'Pods', + label: podLabel, + id: 'pod', + options: () => + Promise.resolve([ + { option: 'Pod 1', value: 'pod-1' }, + { option: 'Pod 2', value: 'pod-2' }, + ]), + valueType: 'checkbox-select', + }, + { + name: 'Containers', + label: containerLabel, + id: 'container', + options: () => + Promise.resolve([ + { option: 'Container 1', value: 'container-1' }, + { value: 'container-2', option: 'Container 2' }, + ]), + valueType: 'checkbox-select', + }, + ]; +}; describe('Attribute filters', () => { it('should return matchers from filters', () => { @@ -93,10 +103,45 @@ describe('Attribute filters', () => { }, ], }, - ].forEach(({ filters, expected }) => { - const matchers = getMatchersFromFilters(filters); - expect(matchers).toEqual(expected); - }); + { + filters: { + namespace: new Set(['my-namespace', 'other-namespace']), + pod: new Set(['pod-1', 'pod-2']), + container: new Set(['container-1', 'container-2']), + }, + schema: Schema.otel, + expected: [ + { + label: 'k8s_namespace_name', + operator: '=~', + value: '"my-namespace|other-namespace"', + }, + { + label: 'k8s_pod_name', + operator: '=~', + value: '"pod-1|pod-2"', + }, + { + label: 'k8s_container_name', + operator: '=~', + value: '"container-1|container-2"', + }, + ], + }, + ].forEach( + ({ + filters, + expected, + schema, + }: { + filters: Filters; + expected: LabelMatcher[]; + schema?: Schema; + }) => { + const matchers = getMatchersFromFilters({ filters, schema: schema ?? Schema.viaq }); + expect(matchers).toEqual(expected); + }, + ); }); it('should get content pipeline stage from filters', () => { @@ -163,14 +208,33 @@ describe('Attribute filters', () => { value: '""', }, }, + { + namespace: 'otel-namespace', + schema: Schema.otel, + expected: { + label: 'k8s_namespace_name', + operator: '=', + value: '"otel-namespace"', + }, + }, { namespace: undefined, expected: undefined, }, - ].forEach(({ namespace, expected }) => { - const selectors = getNamespaceMatcher(namespace); - expect(selectors).toEqual(expected); - }); + ].forEach( + ({ + namespace, + expected, + schema, + }: { + namespace: string | undefined; + expected: LabelMatcher | undefined; + schema?: Schema; + }) => { + const selectors = getNamespaceMatcher({ namespace, schema: schema ?? Schema.viaq }); + expect(selectors).toEqual(expected); + }, + ); }); test('getSeverityFilterPipeline', () => { @@ -212,6 +276,17 @@ describe('Attribute filters', () => { 'level="unknown" or level="" or level=~"error|err|eror|info|inf|information|notice"', }, }, + { + filters: { + severity: new Set(['error', 'info', 'unknown']), + }, + schema: Schema.otel, + expected: { + operator: '|', + value: + 'severity_text="unknown" or severity_text="" or severity_text=~"error|err|eror|info|inf|information|notice"', + }, + }, { filters: { severity: new Set(['unknown']), @@ -221,10 +296,20 @@ describe('Attribute filters', () => { value: 'level="unknown" or level=""', }, }, - ].forEach(({ filters, expected }) => { - const pipeline = getSeverityFilterPipelineStage(filters); - expect(pipeline).toEqual(expected); - }); + ].forEach( + ({ + filters, + expected, + schema, + }: { + filters: Filters | undefined; + expected: PipelineStage | undefined; + schema?: Schema; + }) => { + const pipeline = getSeverityFilterPipelineStage({ filters, schema: schema ?? Schema.viaq }); + expect(pipeline).toEqual(expected); + }, + ); }); test('filtersFromQuery', () => { @@ -319,6 +404,18 @@ describe('Attribute filters', () => { severity: new Set(['error', 'unknown']), }, }, + { + query: + '{ k8s_pod_name=~"a-pod|b-pod", k8s_namespace_name=~"ns-1|ns-2", label="test", k8s_container_name="container-1" } |=`some line content` | other="filter" | severity_text="err|eror" or severity_text="unknown" or severity_text=""', + schema: Schema.otel, + expectedFilters: { + pod: new Set(['a-pod', 'b-pod']), + namespace: new Set(['ns-1', 'ns-2']), + container: new Set(['container-1']), + content: new Set([`some line content`]), + severity: new Set(['error', 'unknown']), + }, + }, { query: '{ kubernetes_pod_name=~"a-pod|b-pod", kubernetes_namespace_name=~"ns-1|ns-2", label="test", kubernetes_container_name="container-1" } |=`some line content` | other="filter" | level', @@ -327,7 +424,6 @@ describe('Attribute filters', () => { namespace: new Set(['ns-1', 'ns-2']), container: new Set(['container-1']), content: new Set([`some line content`]), - severity: new Set(), }, }, { @@ -338,7 +434,6 @@ describe('Attribute filters', () => { namespace: new Set(['ns-1', 'ns-2']), container: new Set(['container-1']), content: new Set([`"some line content"`]), - severity: new Set(), }, }, { @@ -349,16 +444,37 @@ describe('Attribute filters', () => { namespace: new Set(['ns-1', 'ns-2']), container: new Set(['container-1']), content: new Set([`"some-line-content"`]), - severity: new Set(), }, }, - ].forEach(({ query, expectedFilters }) => { - const filters = filtersFromQuery({ + { + query: + '{ k8s_pod_name=~"a-pod|b-pod", k8s_namespace_name=~"ns-1|ns-2", label="test", k8s_container_name="container-1" } |=`"some-line-content"` | other="filter" | level', + expectedFilters: { + pod: new Set(['a-pod', 'b-pod']), + namespace: new Set(['ns-1', 'ns-2']), + container: new Set(['container-1']), + content: new Set([`"some-line-content"`]), + }, + schema: Schema.otel, + }, + ].forEach( + ({ query, - attributes: availableAttributes, - }); - expect(filters).toEqual(expectedFilters); - }); + expectedFilters, + schema, + }: { + query: string | undefined; + expectedFilters: Filters; + schema?: Schema; + }) => { + const filters = filtersFromQuery({ + query, + attributes: availableAttributes(schema ?? Schema.viaq), + schema: schema ?? Schema.viaq, + }); + expect(filters).toEqual(expectedFilters); + }, + ); }); test('query from filters', () => { @@ -518,14 +634,41 @@ describe('Attribute filters', () => { expectedQuery: '{ kubernetes_namespace_name=~"namespace-3|namespace-4", label="test" } | other="filter"', }, - ].forEach(({ initialQuery, filters, expectedQuery }) => { - expect( - queryFromFilters({ - existingQuery: initialQuery, - filters, - attributes: availableAttributes, - }), - ).toEqual(expectedQuery); - }); + // Add otel labels keeping the viaq labels + { + initialQuery: + '{ kubernetes_pod_name=~"a-pod|b-pod", kubernetes_namespace_name=~"ns-1|ns-2", label="test", kubernetes_container_name="container-1" } |="some line content" | other="filter" | level=~"err|error|eror" or level="unknown" or level=""', + filters: { + namespace: new Set(['namespace-3', 'namespace-4']), + content: new Set(), + severity: new Set(), + pod: new Set(), + }, + schema: Schema.otel, + expectedQuery: + '{ kubernetes_pod_name=~"a-pod|b-pod", kubernetes_namespace_name=~"ns-1|ns-2", label="test", kubernetes_container_name="container-1", k8s_namespace_name=~"namespace-3|namespace-4" } | other="filter" | level=~"err|error|eror" or level="unknown" or level=""', + }, + ].forEach( + ({ + initialQuery, + filters, + expectedQuery, + schema, + }: { + initialQuery: string; + filters?: Filters; + expectedQuery: string; + schema?: Schema; + }) => { + expect( + queryFromFilters({ + existingQuery: initialQuery, + filters, + attributes: availableAttributes(schema ?? Schema.viaq), + schema: schema ?? Schema.viaq, + }), + ).toEqual(expectedQuery); + }, + ); }); }); diff --git a/web/src/__tests__/loki-client-spec.ts b/web/src/__tests__/loki-client-spec.ts index b59033b78..d6b63bfb1 100644 --- a/web/src/__tests__/loki-client-spec.ts +++ b/web/src/__tests__/loki-client-spec.ts @@ -1,10 +1,16 @@ +import { SchemaConfig } from '../logs.types'; import { getFetchConfig } from '../loki-client'; describe('Loki Client', () => { it('should generate a valid config', () => { [ { - config: { config: undefined, tenant: 'application', logsLimit: 100 }, + config: { + config: undefined, + tenant: 'application', + logsLimit: 100, + schema: SchemaConfig.viaq, + }, expectedFetchConfig: { endpoint: '/api/proxy/plugin/logging-view-plugin/backend/api/logs/v1/application', requestInit: { timeout: undefined }, @@ -12,7 +18,7 @@ describe('Loki Client', () => { }, { config: { - config: { useTenantInHeader: true, logsLimit: 100 }, + config: { useTenantInHeader: true, logsLimit: 100, schema: SchemaConfig.viaq }, tenant: 'application', }, expectedFetchConfig: { @@ -22,7 +28,7 @@ describe('Loki Client', () => { }, { config: { - config: { useTenantInHeader: false, logsLimit: 100 }, + config: { useTenantInHeader: false, logsLimit: 100, schema: SchemaConfig.viaq }, tenant: 'infrastructure', }, expectedFetchConfig: { @@ -32,7 +38,12 @@ describe('Loki Client', () => { }, { config: { - config: { useTenantInHeader: false, logsLimit: 100, timeout: 2 }, + config: { + useTenantInHeader: false, + logsLimit: 100, + timeout: 2, + schema: SchemaConfig.viaq, + }, tenant: 'infrastructure', }, expectedFetchConfig: { diff --git a/web/src/attribute-filters.tsx b/web/src/attribute-filters.tsx index 0445cec59..a87477ed4 100644 --- a/web/src/attribute-filters.tsx +++ b/web/src/attribute-filters.tsx @@ -1,11 +1,12 @@ import { K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; -import { getInitialTenantFromNamespace, notEmptyString, notUndefined } from './value-utils'; import { cancellableFetch } from './cancellable-fetch'; import { AttributeList, Filters, Option } from './components/filters/filter.types'; -import { LogQLQuery, LabelMatcher, PipelineStage } from './logql-query'; -import { Severity, severityAbbreviations, severityFromString } from './severity'; +import { LabelMatcher, LogQLQuery, PipelineStage } from './logql-query'; +import { Config, Schema } from './logs.types'; import { executeLabelValue } from './loki-client'; -import { Config } from './logs.types'; +import { getStreamLabelsFromSchema, ResourceLabel } from './parse-resources'; +import { Severity, severityAbbreviations, severityFromString } from './severity'; +import { getInitialTenantFromNamespace, notEmptyString, notUndefined } from './value-utils'; const RESOURCES_ENDPOINT = '/api/kubernetes/api/v1'; @@ -128,189 +129,256 @@ const resourceDataSource = return listItems.flatMap(mapper).filter(({ value }) => notEmptyString(value)); }; +const getAttributeLabels = (schema: Schema) => { + const labels = getStreamLabelsFromSchema(schema); + const namespaceLabel = labels[ResourceLabel.Namespace]; + const podLabel = labels[ResourceLabel.Pod]; + const containerLabel = labels[ResourceLabel.Container]; + return { namespaceLabel, podLabel, containerLabel }; +}; + // The logs-page and the logs-dev-page both need a default set of attributes to pass // to queryFromFilters and filtersFromQuery which only need id and label -export const initialAvailableAttributes: AttributeList = [ - { - name: 'Namespaces', - label: 'kubernetes_namespace_name', - id: 'namespace', - valueType: 'checkbox-select', - }, - { - name: 'Pods', - label: 'kubernetes_pod_name', - id: 'pod', - valueType: 'checkbox-select', - }, - { - name: 'Containers', - label: 'kubernetes_container_name', - id: 'container', - valueType: 'checkbox-select', - }, -]; - -export const availableAttributes = (tenant: string, config: Config): AttributeList => [ - { - name: 'Content', - id: 'content', - valueType: 'text', - }, - { - name: 'Namespaces', - label: 'kubernetes_namespace_name', - id: 'namespace', - options: resourceDataSource({ resource: 'namespaces' }), - valueType: 'checkbox-select', - }, - { - name: 'Pods', - label: 'kubernetes_pod_name', - id: 'pod', - options: getPodAttributeOptions(tenant, config), - valueType: 'checkbox-select', - }, - { - name: 'Containers', - label: 'kubernetes_container_name', - id: 'container', - options: resourceDataSource({ - resource: 'pods', - mapper: (resource) => - resource?.spec?.containers.map((container) => ({ - option: `${resource?.metadata?.name} / ${container.name}`, - value: `${resource?.metadata?.name} / ${container.name}`, - })) ?? [], - }), - expandSelection: (selections) => { - const podSelections = new Set(); - const containerSelections = new Set(); - - for (const container of selections.values()) { - if (container.includes(' / ')) { - const [pod, containerName] = container.split(' / '); - podSelections.add(pod); - containerSelections.add(containerName); - } - } +export const initialAvailableAttributes = (schema: Schema): AttributeList => { + const labels = getStreamLabelsFromSchema(schema); + const namespaceLabel = labels[ResourceLabel.Namespace]; + const podLabel = labels[ResourceLabel.Pod]; + const containerLabel = labels[ResourceLabel.Container]; + + return [ + { + name: 'Namespaces', + label: namespaceLabel, + id: 'namespace', + valueType: 'checkbox-select', + }, + { + name: 'Pods', + label: podLabel, + id: 'pod', + valueType: 'checkbox-select', + }, + { + name: 'Containers', + label: containerLabel, + id: 'container', + valueType: 'checkbox-select', + }, + ]; +}; - return new Map([ - ['pod', podSelections], - ['container', containerSelections], - ]); +export const availableAttributes = ({ + tenant, + config, + schema, +}: { + tenant: string; + config: Config; + schema: Schema; +}): AttributeList => { + const { namespaceLabel, podLabel, containerLabel } = getAttributeLabels(schema); + + return [ + { + name: 'Content', + id: 'content', + valueType: 'text', }, - isItemSelected: (value, filters) => { - const parts = value.split(' / '); - if (parts.length !== 2) { - return false; - } + { + name: 'Namespaces', + label: namespaceLabel, + id: 'namespace', + options: resourceDataSource({ resource: 'namespaces' }), + valueType: 'checkbox-select', + }, + { + name: 'Pods', + label: podLabel, + id: 'pod', + options: getPodAttributeOptions(tenant, config, schema), + valueType: 'checkbox-select', + }, + { + name: 'Containers', + label: containerLabel, + id: 'container', + options: resourceDataSource({ + resource: 'pods', + mapper: (resource) => + resource?.spec?.containers.map((container) => ({ + option: `${resource?.metadata?.name} / ${container.name}`, + value: `${resource?.metadata?.name} / ${container.name}`, + })) ?? [], + }), + expandSelection: (selections) => { + const podSelections = new Set(); + const containerSelections = new Set(); + + for (const container of selections.values()) { + if (container.includes(' / ')) { + const [pod, containerName] = container.split(' / '); + podSelections.add(pod); + containerSelections.add(containerName); + } + } - const [pod, container] = parts; + return new Map([ + ['pod', podSelections], + ['container', containerSelections], + ]); + }, + isItemSelected: (value, filters) => { + const parts = value.split(' / '); + if (parts.length !== 2) { + return false; + } - if ( - (!filters.pod || filters.pod.size === 0) && - filters.container && - filters.container.size > 0 - ) { - return filters.container.has(container); - } + const [pod, container] = parts; - if ( - !filters.pod || - filters.pod.size === 0 || - !filters.container || - filters.container.size === 0 - ) { - return false; - } + if ( + (!filters.pod || filters.pod.size === 0) && + filters.container && + filters.container.size > 0 + ) { + return filters.container.has(container); + } + + if ( + !filters.pod || + filters.pod.size === 0 || + !filters.container || + filters.container.size === 0 + ) { + return false; + } - return filters.pod.has(pod) && filters.container.has(container); + return filters.pod.has(pod) && filters.container.has(container); + }, + valueType: 'checkbox-select', }, - valueType: 'checkbox-select', - }, -]; - -export const availableDevConsoleAttributes = (tenant: string, config: Config): AttributeList => [ - { - name: 'Content', - id: 'content', - valueType: 'text', - }, - { - name: 'Namespaces', - label: 'kubernetes_namespace_name', - id: 'namespace', - options: projectsDataSource(), - valueType: 'checkbox-select', - }, - { - name: 'Pods', - label: 'kubernetes_pod_name', - id: 'pod', - options: lokiLabelValuesDataSource({ - config, - tenant, - labelName: 'kubernetes_pod_name', - }), - valueType: 'checkbox-select', - }, - { - name: 'Containers', - label: 'kubernetes_container_name', - id: 'container', - options: lokiLabelValuesDataSource({ - config, - tenant, - labelName: 'kubernetes_container_name', - }), - valueType: 'checkbox-select', - }, -]; + ]; +}; + +export const availableDevConsoleAttributes = ( + tenant: string, + config: Config, + schema: Schema, +): AttributeList => { + const { namespaceLabel, podLabel, containerLabel } = getAttributeLabels(schema); + + return [ + { + name: 'Content', + id: 'content', + valueType: 'text', + }, + { + name: 'Namespaces', + label: namespaceLabel, + id: 'namespace', + options: projectsDataSource(), + valueType: 'checkbox-select', + }, + { + name: 'Pods', + label: podLabel, + id: 'pod', + options: lokiLabelValuesDataSource({ + config, + tenant, + labelName: podLabel, + }), + valueType: 'checkbox-select', + }, + { + name: 'Containers', + label: containerLabel, + id: 'container', + options: lokiLabelValuesDataSource({ + config, + tenant, + labelName: containerLabel, + }), + valueType: 'checkbox-select', + }, + ]; +}; export const availablePodAttributes = ( namespace: string, podId: string, config: Config, -): AttributeList => [ - { - name: 'Content', - id: 'content', - valueType: 'text', - }, - { - name: 'Containers', - label: 'kubernetes_container_name', - id: 'container', - options: lokiLabelValuesDataSource({ - config, - query: `{ kubernetes_pod_name="${podId}" }`, - labelName: 'kubernetes_container_name', - tenant: getInitialTenantFromNamespace(namespace), - }), - valueType: 'checkbox-select', - }, -]; + schema: Schema, +): AttributeList => { + const streamLabels = getStreamLabelsFromSchema(schema); + const namespaceLabel = streamLabels[ResourceLabel.Namespace]; + const podLabel = streamLabels[ResourceLabel.Pod]; + const containerLabel = streamLabels[ResourceLabel.Container]; + + return [ + { + name: 'Content', + id: 'content', + valueType: 'text', + }, + { + name: 'Pods', + label: podLabel, + id: 'pod', + options: lokiLabelValuesDataSource({ + config, + query: `{ ${namespaceLabel}="${namespace}" }`, + labelName: podLabel, + tenant: getInitialTenantFromNamespace(namespace), + }), + valueType: 'checkbox-select', + }, + { + name: 'Containers', + label: containerLabel, + id: 'container', + options: lokiLabelValuesDataSource({ + config, + query: `{ ${podLabel}="${podId}" }`, + labelName: containerLabel, + tenant: getInitialTenantFromNamespace(namespace), + }), + valueType: 'checkbox-select', + }, + ]; +}; export const queryFromFilters = ({ existingQuery, filters, attributes, tenant, + schema, + addJSONParser, }: { existingQuery: string; filters?: Filters; attributes: AttributeList; tenant?: string; + schema: Schema; + addJSONParser?: boolean; }): string => { const query = new LogQLQuery(existingQuery); + const streamLabels = getStreamLabelsFromSchema(schema); + const tenantLabel = streamLabels[ResourceLabel.LogType]; + const severityLabel = streamLabels[ResourceLabel.Severity]; + if (!filters) { return query.toString(); } if (tenant) { - query.addSelectorMatcher({ label: 'log_type', operator: '=', value: `"${tenant}"` }); + query.addSelectorMatcher({ + label: tenantLabel, + operator: '=', + value: `"${tenant}"`, + }); } const contentPipelineStage = getContentPipelineStage(filters); @@ -325,19 +393,21 @@ export const queryFromFilters = ({ query.removePipelineStage({ operator: '|=' }); } - const severityPipelineStage = getSeverityFilterPipelineStage(filters); + const severityPipelineStage = getSeverityFilterPipelineStage({ filters, schema }); if (severityPipelineStage) { - query.removePipelineStage({}, { matchLabel: 'level' }).addPipelineStage(severityPipelineStage, { - placement: 'end', - }); + query + .removePipelineStage({}, { matchLabel: `${severityLabel}` }) + .addPipelineStage(severityPipelineStage, { + placement: 'end', + }); } if (filters?.severity === undefined || filters.severity.size === 0) { - query.removePipelineStage({}, { matchLabel: 'level' }); + query.removePipelineStage({}, { matchLabel: `${severityLabel}` }); } - query.addSelectorMatcher(getMatchersFromFilters(filters)); + query.addSelectorMatcher(getMatchersFromFilters({ filters, schema })); attributes.forEach(({ id, label }) => { if (label) { @@ -348,6 +418,15 @@ export const queryFromFilters = ({ } }); + // Remove the tenant label matcher if the query has other selectors + if (tenant && query.streamSelector.filter((a) => a.label !== tenantLabel).length > 0) { + query.removeSelectorMatcher({ label: tenantLabel }); + } + + if (schema === Schema.viaq && !!addJSONParser) { + query.addPipelineStage({ operator: '| json' }, { placement: 'start' }); + } + return query.toString(); }; @@ -357,13 +436,18 @@ const removeBacktick = (value?: string) => (value ? value.replace(/`/g, '') : '' export const filtersFromQuery = ({ query, attributes, + schema, }: { query?: string; attributes: AttributeList; + schema: Schema.viaq | Schema.otel; }): Filters => { const filters: Filters = {}; const logQLQuery = new LogQLQuery(query ?? ''); + const streamLabels = getStreamLabelsFromSchema(schema); + const severityLabel = streamLabels[ResourceLabel.Severity]; + for (const { label, id } of attributes) { if (label && label.length > 0) { for (const selector of logQLQuery.streamSelector) { @@ -377,7 +461,7 @@ export const filtersFromQuery = ({ for (const pipelineStage of logQLQuery.pipeline) { if ( pipelineStage.operator === '|' && - pipelineStage.labelsInFilter?.every(({ label }) => label === 'level') && + pipelineStage.labelsInFilter?.every(({ label }) => label === `${severityLabel}`) && !filters.severity ) { const severityValues: Array = pipelineStage.labelsInFilter @@ -385,7 +469,9 @@ export const filtersFromQuery = ({ .map(removeQuotes) .map(severityFromString) .filter(notUndefined); - filters.severity = new Set(severityValues); + if (severityValues.length > 0) { + filters.severity = new Set(severityValues); + } } else if (pipelineStage.operator === '|=' && !filters.content) { filters.content = new Set([removeBacktick(pipelineStage.value)]); } @@ -394,13 +480,22 @@ export const filtersFromQuery = ({ return filters; }; -export const getNamespaceMatcher = (namespace?: string): LabelMatcher | undefined => { +export const getNamespaceMatcher = ({ + namespace, + schema, +}: { + namespace?: string; + schema: Schema; +}): LabelMatcher | undefined => { if (namespace === undefined) { return undefined; } + const streamLabels = getStreamLabelsFromSchema(schema); + const namespaceLabel = streamLabels[ResourceLabel.Namespace]; + return { - label: 'kubernetes_namespace_name', + label: namespaceLabel, operator: '=', value: `"${namespace}"`, }; @@ -412,10 +507,18 @@ const isK8sValueARegex = (value: string) => { return testRegex.test(value); }; -export const queryWithNamespace = ({ query, namespace }: { query: string; namespace?: string }) => { +export const queryWithNamespace = ({ + query, + namespace, + schema, +}: { + query: string; + namespace?: string; + schema: Schema; +}) => { const logQLQuery = new LogQLQuery(query ?? ''); - logQLQuery.addSelectorMatcher(getNamespaceMatcher(namespace)); + logQLQuery.addSelectorMatcher(getNamespaceMatcher({ namespace, schema })); return logQLQuery.toString(); }; @@ -446,25 +549,35 @@ export const getK8sMatcherFromSet = ( }; }; -export const getMatchersFromFilters = (filters?: Filters): Array => { +export const getMatchersFromFilters = ({ + filters, + schema, +}: { + filters?: Filters; + schema: Schema; +}): Array => { if (!filters) { return []; } const matchers: Array = []; + const labels = getStreamLabelsFromSchema(schema); + const namespaceLabel = labels[ResourceLabel.Namespace]; + const podLabel = labels[ResourceLabel.Pod]; + const containerLabel = labels[ResourceLabel.Container]; for (const key of Object.keys(filters)) { const value = filters[key]; if (value) { switch (key) { case 'namespace': - matchers.push(getK8sMatcherFromSet('kubernetes_namespace_name', value)); + matchers.push(getK8sMatcherFromSet(namespaceLabel, value)); break; case 'pod': - matchers.push(getK8sMatcherFromSet('kubernetes_pod_name', value)); + matchers.push(getK8sMatcherFromSet(podLabel, value)); break; case 'container': - matchers.push(getK8sMatcherFromSet('kubernetes_container_name', value)); + matchers.push(getK8sMatcherFromSet(containerLabel, value)); break; } } @@ -493,18 +606,29 @@ export const getContentPipelineStage = (filters?: Filters): PipelineStage | unde return { operator: '|=', value: `\`${textValue}\`` }; }; -export const getSeverityFilterPipelineStage = (filters?: Filters): PipelineStage | undefined => { +export const getSeverityFilterPipelineStage = ({ + filters, + schema, +}: { + filters?: Filters; + schema: Schema; +}): PipelineStage | undefined => { if (!filters) { return undefined; } const severity = filters.severity; + const labels = getStreamLabelsFromSchema(schema); + const severityLabel = labels.Severity; + if (!severity) { return undefined; } - const unknownFilter = severity.has('unknown') ? 'level="unknown" or level=""' : ''; + const unknownFilter = severity.has('unknown') + ? `${severityLabel}="unknown" or ${severityLabel}=""` + : ''; const severityFilters = Array.from(severity).flatMap((group: string | undefined) => { if (group === 'unknown' || group === undefined) { @@ -514,20 +638,27 @@ export const getSeverityFilterPipelineStage = (filters?: Filters): PipelineStage return severityAbbreviations[group as Severity]; }); - const levelsfilter = severityFilters.length > 0 ? `level=~"${severityFilters.join('|')}"` : ''; + const levelsfilter = + severityFilters.length > 0 ? `${severityLabel}=~"${severityFilters.join('|')}"` : ''; const allFilters = [unknownFilter, levelsfilter].filter(notEmptyString); return allFilters.length > 0 ? { operator: '|', value: allFilters.join(' or ') } : undefined; }; -const getPodAttributeOptions = (tenant: string, config: Config): (() => Promise) => { +const getPodAttributeOptions = ( + tenant: string, + config: Config, + schema: Schema, +): (() => Promise) => { + const { podLabel } = getAttributeLabels(schema); + return () => Promise.allSettled>([ lokiLabelValuesDataSource({ config, tenant, - labelName: 'kubernetes_pod_name', + labelName: podLabel, })(), resourceDataSource({ resource: 'pods' })(), ]) diff --git a/web/src/backend-client.ts b/web/src/backend-client.ts index eeceeed9d..f9cbb9c23 100644 --- a/web/src/backend-client.ts +++ b/web/src/backend-client.ts @@ -1,11 +1,17 @@ import { cancellableFetch } from './cancellable-fetch'; -import { Config } from './logs.types'; +import { Config, SchemaConfig } from './logs.types'; const BACKEND_ENDPOINT = '/api/plugins/logging-view-plugin'; let singletonRequest: Promise | null = null; let throttleTimeout: NodeJS.Timeout | null = null; +export const defaultConfig: Config = { + isStreamingEnabledInDefaultPage: false, + logsLimit: 100, + schema: SchemaConfig.viaq, +}; + export const getConfig = async (): Promise => { if (singletonRequest) { if (throttleTimeout) { @@ -19,9 +25,18 @@ export const getConfig = async (): Promise => { return singletonRequest; } - const { request } = cancellableFetch(`${BACKEND_ENDPOINT}/config`); + try { + const { request } = cancellableFetch(`${BACKEND_ENDPOINT}/config`); - singletonRequest = request(); + singletonRequest = request(); + const config = await singletonRequest; - return singletonRequest; + return config; + } catch (e) { + // eslint-disable-next-line no-console + console.error('Error fetching logging plugin configuration', e); + } finally { + // eslint-disable-next-line no-unsafe-finally + return Promise.resolve(defaultConfig); + } }; diff --git a/web/src/cancellable-fetch.ts b/web/src/cancellable-fetch.ts index a4554d5df..b95668147 100644 --- a/web/src/cancellable-fetch.ts +++ b/web/src/cancellable-fetch.ts @@ -46,8 +46,8 @@ export const cancellableFetch = ( const abortController = new AbortController(); const abort = () => abortController.abort(); - const fetchPromise = () => - fetch(url, { + const fetchPromise = async () => { + const response = await fetch(url, { ...init, headers: { ...init?.headers, @@ -55,14 +55,15 @@ export const cancellableFetch = ( ...(init?.method === 'POST' ? { 'X-CSRFToken': getCSRFToken() } : {}), }, signal: abortController.signal, - }).then(async (response) => { - if (!response.ok) { - const text = await response.text(); - throw new FetchError(text, response.status); - } - return response.json(); }); + if (!response.ok) { + const text = await response.text(); + throw new FetchError(text, response.status); + } + return response.json(); + }; + const timeout = init?.timeout ?? 30 * 1000; if (timeout <= 0) { diff --git a/web/src/components/alerts/logs-alerts-metrics.tsx b/web/src/components/alerts/logs-alerts-metrics.tsx index 4bbc4ac20..768bc8a0c 100644 --- a/web/src/components/alerts/logs-alerts-metrics.tsx +++ b/web/src/components/alerts/logs-alerts-metrics.tsx @@ -1,7 +1,9 @@ import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; +import { LogsConfigProvider, useLogsConfig } from '../../hooks/LogsConfigProvider'; import { useLogs } from '../../hooks/useLogs'; import { Rule, TimeRange } from '../../logs.types'; +import { getSchema } from '../../value-utils'; import { LogsMetrics } from '../logs-metrics'; import { TimeRangeDropdown } from '../time-range-dropdown'; @@ -13,14 +15,15 @@ const LOKI_TENANT_LABEL_KEY = 'tenantId'; const LogsAlertMetrics: React.FC = ({ rule }) => { const { t } = useTranslation('plugin__logging-view-plugin'); - const { getLogs, logsData, logsError, isLoadingLogsData, config } = useLogs(); + const { getLogs, logsData, logsError, isLoadingLogsData } = useLogs(); + const { config } = useLogsConfig(); const tenant = rule?.labels?.[config.alertingRuleTenantLabelKey ?? LOKI_TENANT_LABEL_KEY]; const [timeRange, setTimeRange] = React.useState(); useEffect(() => { if (rule?.query && tenant) { - getLogs({ query: rule.query, timeRange, tenant }); + getLogs({ query: rule.query, timeRange, tenant, schema: getSchema(config.schema) }); } }, [rule?.query, timeRange]); @@ -47,4 +50,12 @@ const LogsAlertMetrics: React.FC = ({ rule }) => { ); }; -export default LogsAlertMetrics; +const LogsAlertMetricsWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +export default LogsAlertMetricsWrapper; diff --git a/web/src/components/filters/attribute-filter.tsx b/web/src/components/filters/attribute-filter.tsx index 294fc00d1..4d9e34dd0 100644 --- a/web/src/components/filters/attribute-filter.tsx +++ b/web/src/components/filters/attribute-filter.tsx @@ -11,7 +11,7 @@ import { ToolbarGroup, } from '@patternfly/react-core'; import { FilterIcon } from '@patternfly/react-icons'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useDebounce } from '../../hooks/useDebounce'; import { TestIds } from '../../test-ids'; @@ -58,6 +58,12 @@ export const AttributeFilter: React.FC = ({ } }, [textAttribute, filters]); + useEffect(() => { + if (!selectedAttributeId) { + setSelectedAttributeId(attributeList[0]?.id); + } + }, [attributeList]); + const handleAttributeSelect = ( _: React.MouseEvent | undefined, value: string | number | undefined, diff --git a/web/src/components/filters/attribute-value-data.tsx b/web/src/components/filters/attribute-value-data.tsx index 9f35661f6..3b0ce8f8a 100644 --- a/web/src/components/filters/attribute-value-data.tsx +++ b/web/src/components/filters/attribute-value-data.tsx @@ -19,9 +19,9 @@ export const useAttributeValueData = (attribute: Attribute): UseAttributeValueDa setAttributeError(undefined); if (attribute.options) { if (Array.isArray(attribute.options)) { + setAttributeLoading(false); setAttributeOptions(attribute.options); } else { - setAttributeLoading(true); attribute .options(searchQuery) .then((asyncOptions) => { diff --git a/web/src/components/logs-histogram.tsx b/web/src/components/logs-histogram.tsx index 046b1ae1c..b82bcad36 100644 --- a/web/src/components/logs-histogram.tsx +++ b/web/src/components/logs-histogram.tsx @@ -16,6 +16,7 @@ import { isMatrixResult, MetricValue, QueryRangeResponse, + Schema, TimeRange, TimeRangeNumber, } from '../logs.types'; @@ -59,14 +60,21 @@ interface LogHistogramProps { interval?: number; isLoading?: boolean; error?: unknown; + schema?: Schema; } const resultHasAbreviation = ( result: Record, abbreviation: Array, -): boolean => !!result.level && abbreviation.includes(result.level); + schema: Schema | undefined, +): boolean => { + if (schema == Schema.otel) { + return !!result.severity_text && abbreviation.includes(result.severity_text); + } + return !!result.level && abbreviation.includes(result.level); +}; -const aggregateMetricsLogData = (response?: QueryRangeResponse): HistogramData => { +const aggregateMetricsLogData = (response?: QueryRangeResponse, schema?: Schema): HistogramData => { const histogramData: HistogramData = { critical: [], error: [], @@ -84,7 +92,7 @@ const aggregateMetricsLogData = (response?: QueryRangeResponse): HistogramData = for (const logData of data.result) { let logDataIngroup = false; for (const [group, abbreviations] of Object.entries(severityAbbreviations)) { - if (resultHasAbreviation(logData.metric, abbreviations)) { + if (resultHasAbreviation(logData.metric, abbreviations, schema)) { histogramData[group as Severity].push(...logData.values); logDataIngroup = true; break; @@ -240,6 +248,7 @@ export const LogsHistogram: React.FC = ({ error, onChangeTimeRange, interval, + schema, }) => { const { t } = useTranslation('plugin__logging-view-plugin'); @@ -271,7 +280,7 @@ export const LogsHistogram: React.FC = ({ return { ticks: [], charts: null, availableGroups: [] }; } - const data = aggregateMetricsLogData(histogramData); + const data = aggregateMetricsLogData(histogramData, schema); const chartsData = getChartsData(data, intervalValue); const tickCount = tickCountFromTimeRange(timeRangeValue, intervalValue); diff --git a/web/src/components/logs-query-input.tsx b/web/src/components/logs-query-input.tsx index fb22a8c32..a7f2d915f 100644 --- a/web/src/components/logs-query-input.tsx +++ b/web/src/components/logs-query-input.tsx @@ -59,9 +59,9 @@ export const LogsQueryInput: React.FC = ({ variant="danger" title={ !isValid - ? `${t( - 'Invalid log stream selector. Please select a namespace, pod or container as filter, or add a log stream selector like: ', - )} { log_type =~ ".+" } | json` + ? t( + 'Invalid log stream selector. Please select a namespace, pod or container as filter.', + ) : invalidQueryErrorMessage } aria-live="polite" diff --git a/web/src/components/logs-toolbar.tsx b/web/src/components/logs-toolbar.tsx index 2baa029a8..973b61681 100644 --- a/web/src/components/logs-toolbar.tsx +++ b/web/src/components/logs-toolbar.tsx @@ -16,6 +16,8 @@ import { } from '@patternfly/react-core'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import { useLogsConfig } from '../hooks/LogsConfigProvider'; +import { Schema, SchemaConfig } from '../logs.types'; import { Severity, severityFromString } from '../severity'; import { TestIds } from '../test-ids'; import { notUndefined } from '../value-utils'; @@ -23,13 +25,14 @@ import { ExecuteQueryButton } from './execute-query-button'; import { ExecuteVolumeButton } from './execute-volume-button'; import { AttributeFilter } from './filters/attribute-filter'; import { AttributeList, Filters } from './filters/filter.types'; +import { isOption } from './filters/filters-from-params'; import { LogsQueryInput } from './logs-query-input'; import './logs-toolbar.css'; +import { SchemaDropdown } from './schema-dropdown'; import { Spacer } from './spacer'; import { TenantDropdown } from './tenant-dropdown'; import { ToggleButton } from './toggle-button'; import { TogglePlay } from './toggle-play'; -import { isOption } from './filters/filters-from-params'; interface LogsToolbarProps { query: string; @@ -39,6 +42,7 @@ interface LogsToolbarProps { invalidQueryErrorMessage?: string | null; tenant?: string; onTenantSelect?: (tenant: string) => void; + onSchemaSelect?: (schema: Schema) => void; enableStreaming?: boolean; isStreaming?: boolean; severityFilter?: Set; @@ -54,6 +58,7 @@ interface LogsToolbarProps { filters?: Filters; attributeList?: AttributeList; onDownloadCSV?: () => void; + schema: Schema; } const availableSeverityFilters: Array = [ @@ -74,6 +79,7 @@ export const LogsToolbar: React.FC = ({ invalidQueryErrorMessage, tenant = 'application', onTenantSelect, + onSchemaSelect, onStreamingToggle, onShowResourcesToggle, onDownloadCSV, @@ -87,11 +93,22 @@ export const LogsToolbar: React.FC = ({ filters, onFiltersChange, attributeList, + schema, }) => { const { t } = useTranslation('plugin__logging-view-plugin'); const [isSeverityExpanded, setIsSeverityExpanded] = React.useState(false); const [isQueryShown, setIsQueryShown] = React.useState(false); + const [isSchemaShown, setIsSchemaShown] = React.useState(false); + + const { config } = useLogsConfig(); + + React.useEffect(() => { + if (config?.schema === SchemaConfig.select) { + setIsSchemaShown(true); + } + }, [config?.schema]); + const severityFilter: Set = filters?.severity ? new Set(Array.from(filters?.severity).map(severityFromString).filter(notUndefined)) : new Set(); @@ -164,6 +181,7 @@ export const LogsToolbar: React.FC = ({ onFiltersChange={onFiltersChange} /> )} + = ({ )} + {isSchemaShown && ( + + + + )} + = ({ untoggledText={t('Show Resources')} toggledText={t('Hide Resources')} /> - - = ({ /> - {onDownloadCSV && ( - - - - )} - - - - - {!isQueryShown && ( - <> + {onDownloadCSV && ( - + - {invalidQueryErrorMessage && ( + )} + + + + + + {!isQueryShown && ( + <> - + - )} - - )} + {invalidQueryErrorMessage && ( + + + + )} + + )} - + - - + + + {enableStreaming && ( diff --git a/web/src/components/schema-dropdown.tsx b/web/src/components/schema-dropdown.tsx new file mode 100644 index 000000000..d78712fe6 --- /dev/null +++ b/web/src/components/schema-dropdown.tsx @@ -0,0 +1,63 @@ +import { + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, +} from '@patternfly/react-core'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Schema } from '../logs.types'; +import { TestIds } from '../test-ids'; + +type SchemaDropdownProps = { + onSchemaSelected: ((schema: Schema) => void) | undefined; + schema: Schema; +}; + +export const SchemaDropdown: React.FC = ({ onSchemaSelected, schema }) => { + const { t } = useTranslation('plugin__logging-view-plugin'); + + const [isOpen, setIsOpen] = React.useState(false); + + const onToggle = () => setIsOpen(!isOpen); + const onSelect = ( + _: React.MouseEvent | undefined, + value: string | number | undefined, + ) => { + if (value != schema) { + onSchemaSelected?.(value as Schema); + } + setIsOpen(false); + }; + + const toggle = (toggleRef: React.Ref) => ( + + {schema} + + ); + + return ( + + ); +}; diff --git a/web/src/getAlertingRules.ts b/web/src/getAlertingRules.ts index 0b1cbd193..cf9766dc9 100644 --- a/web/src/getAlertingRules.ts +++ b/web/src/getAlertingRules.ts @@ -1,3 +1,4 @@ +import { getConfig } from './backend-client'; import { RulesResponse } from './logs.types'; import { getRules } from './loki-client'; import { namespaceBelongsToInfrastructureTenant } from './value-utils'; @@ -22,13 +23,15 @@ export const getAlertingRules = async (tenants: Array, namespace?: strin return null; } + const config = await getConfig(); + const rulesResponses = await Promise.allSettled( tenants.map((tenant) => { if (abortControllers.has(tenant)) { abortControllers.get(tenant)?.(); } - const { abort, request } = getRules({ tenant, namespace }); + const { abort, request } = getRules({ tenant, namespace, config }); abortControllers.set(tenant, abort); return request(); diff --git a/web/src/hooks/LogsConfigProvider.tsx b/web/src/hooks/LogsConfigProvider.tsx new file mode 100644 index 000000000..d2f285336 --- /dev/null +++ b/web/src/hooks/LogsConfigProvider.tsx @@ -0,0 +1,53 @@ +import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import { defaultConfig, getConfig } from '../backend-client'; +import { Config } from '../logs.types'; + +interface LogsContextType { + config: Config; + fetchConfig: () => Promise; +} + +export const LogsContext = createContext(undefined); + +export const LogsConfigProvider: React.FC<{ children?: React.ReactNode | undefined }> = ({ + children, +}) => { + const [config, setConfig] = useState(defaultConfig); + const [configLoaded, setConfigLoaded] = useState(false); + + const fetchConfig = useCallback(async () => { + try { + if (!configLoaded) { + const configData = await getConfig(); + const mergedConfig = { ...defaultConfig, ...configData }; + setConfigLoaded(true); + setConfig(mergedConfig); + + return mergedConfig; + } + + return config; + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching logging plugin configuration', error); + setConfig(defaultConfig); + return defaultConfig; + } + }, [config]); + + return {children}; +}; + +export const useLogsConfig = (): LogsContextType => { + const context = useContext(LogsContext); + + if (context === undefined) { + throw new Error('useLogsConfig must be used within a LogsConfigProvider'); + } + + useEffect(() => { + context.fetchConfig(); + }, []); + + return context; +}; diff --git a/web/src/hooks/useLogs.ts b/web/src/hooks/useLogs.ts index 720a5194d..3f197de8a 100644 --- a/web/src/hooks/useLogs.ts +++ b/web/src/hooks/useLogs.ts @@ -1,14 +1,14 @@ import { WSFactory } from '@openshift-console/dynamic-plugin-sdk/lib/utils/k8s/ws-factory'; import React from 'react'; -import { getConfig } from '../backend-client'; import { Config, Direction, isMatrixResult, isStreamsResult, QueryRangeResponse, - VolumeRangeResponse, + Schema, TimeRange, + VolumeRangeResponse, } from '../logs.types'; import { connectToTailSocket, @@ -20,15 +20,11 @@ import { intervalFromTimeRange, numericTimeRange, timeRangeFromDuration } from ' import { millisecondsFromDuration } from '../value-utils'; import { LogQLQuery } from '../logql-query'; +import { LogsContext } from './LogsConfigProvider'; const DEFAULT_TIME_SPAN = '1h'; const STREAMING_MAX_LOGS_LIMIT = 1e3; -const defaultConfig: Config = { - isStreamingEnabledInDefaultPage: false, - logsLimit: 100, -}; - const isAbortError = (error: unknown): boolean => error instanceof Error && error.name === 'AbortError'; @@ -46,8 +42,6 @@ type State = { showVolumeGraph?: boolean; hasMoreLogsData?: boolean; isStreaming: boolean; - config: Config; - configLoaded: boolean; }; type Action = @@ -66,7 +60,7 @@ type Action = } | { type: 'logsResponse'; - payload: { logsData: QueryRangeResponse }; + payload: { logsData: QueryRangeResponse; config: Config }; } | { type: 'startStreaming'; @@ -80,7 +74,7 @@ type Action = } | { type: 'moreLogsResponse'; - payload: { logsData: QueryRangeResponse }; + payload: { logsData: QueryRangeResponse; config: Config }; } | { type: 'logsError'; @@ -90,10 +84,6 @@ type Action = type: 'histogramError'; payload: { error: unknown }; } - | { - type: 'setConfig'; - payload: { config: Config }; - } | { type: 'volumeRequest'; } @@ -227,14 +217,14 @@ const reducer = (state: State, action: Action): State => { isLoadingLogsData: false, showVolumeGraph: false, logsData: action.payload.logsData, - hasMoreLogsData: hasMoreLogs(action.payload.logsData, state.config.logsLimit), + hasMoreLogsData: hasMoreLogs(action.payload.logsData, action.payload.config.logsLimit), }; case 'moreLogsResponse': return { ...state, isLoadingMoreLogsData: false, logsData: appendData(state.logsData, action.payload.logsData), - hasMoreLogsData: hasMoreLogs(action.payload.logsData, state.config.logsLimit), + hasMoreLogsData: hasMoreLogs(action.payload.logsData, action.payload.config.logsLimit), }; case 'logsError': return { @@ -243,12 +233,6 @@ const reducer = (state: State, action: Action): State => { isLoadingMoreLogsData: false, logsError: action.payload.error, }; - case 'setConfig': - return { - ...state, - configLoaded: true, - config: action.payload.config, - }; default: return state; @@ -266,7 +250,6 @@ export const useLogs = ( }, ) => { const currentQuery = React.useRef(); - const currentConfig = React.useRef(defaultConfig); const currentTenant = React.useRef(initialTenant); const currentTimeRange = React.useRef(initialTimeRange); const currentTime = React.useRef(Date.now()); @@ -280,6 +263,13 @@ export const useLogs = ( const histogramAbort = React.useRef<() => void | undefined>(); const volumeAbort = React.useRef<() => void | undefined>(); const ws = React.useRef(); + const logsContext = React.useContext(LogsContext); + + if (logsContext === undefined) { + throw new Error('useLogs must be used within a LogsProvider'); + } + + const { fetchConfig } = logsContext; const [ { @@ -296,8 +286,6 @@ export const useLogs = ( showVolumeGraph, hasMoreLogsData, isStreaming, - config, - configLoaded, }, dispatch, ] = React.useReducer(reducer, { @@ -307,37 +295,20 @@ export const useLogs = ( isLoadingMoreLogsData: false, hasMoreLogsData: false, isStreaming: false, - config: defaultConfig, - configLoaded: false, }); - const fetchConfig = React.useCallback(async () => { - if (!configLoaded) { - try { - const configData = await getConfig(); - const mergedConfig = { ...defaultConfig, ...configData }; - dispatch({ type: 'setConfig', payload: { config: mergedConfig } }); - - currentConfig.current = mergedConfig; - } catch (e) { - // eslint-disable-next-line no-console - console.warn('Error fetching configuration', e); - } - } - - return currentConfig.current; - }, [configLoaded]); - const getMoreLogs = async ({ lastTimestamp, query, namespace, direction, + schema, }: { lastTimestamp: number; query: string; namespace?: string; direction?: Direction; + schema: Schema; }) => { if (query.length === 0) { dispatch({ type: 'logsError', payload: { error: new Error('Query is empty') } }); @@ -365,16 +336,17 @@ export const useLogs = ( logsAbort.current(); } - await fetchConfig(); + const config = await fetchConfig(); const { request, abort } = executeQueryRange({ query, start, end, - config: currentConfig.current, + config, tenant: currentTenant.current, namespace, direction: currentDirection.current, + schema, }); logsAbort.current = abort; @@ -383,7 +355,7 @@ export const useLogs = ( dispatch({ type: 'moreLogsResponse', - payload: { logsData: queryResponse }, + payload: { logsData: queryResponse, config }, }); } catch (error) { if (!isAbortError(error)) { @@ -398,12 +370,14 @@ export const useLogs = ( timeRange, namespace, direction, + schema, }: { query: string; tenant?: string; timeRange?: TimeRange; namespace?: string; direction?: Direction; + schema: Schema; }) => { if (query.length === 0) { dispatch({ type: 'logsError', payload: { error: new Error('Query is empty') } }); @@ -431,23 +405,24 @@ export const useLogs = ( logsAbort.current(); } - await fetchConfig(); + const config = await fetchConfig(); const { request, abort } = executeQueryRange({ query, start, end, - config: currentConfig.current, + config, tenant: currentTenant.current, namespace, direction: currentDirection.current, + schema, }); logsAbort.current = abort; const queryResponse = await request(); - dispatch({ type: 'logsResponse', payload: { logsData: queryResponse } }); + dispatch({ type: 'logsResponse', payload: { logsData: queryResponse, config } }); } catch (error) { if (!isAbortError(error)) { dispatch({ type: 'logsError', payload: { error } }); @@ -467,10 +442,12 @@ export const useLogs = ( query, tenant, namespace, + schema, }: { query: string; tenant?: string; namespace?: string; + schema: Schema; }) => { currentQuery.current = query; currentTenant.current = tenant ?? currentTenant.current; @@ -486,6 +463,7 @@ export const useLogs = ( query, tenant: currentTenant.current, namespace, + schema, }); ws.current.onerror((error) => { @@ -517,10 +495,12 @@ export const useLogs = ( query, tenant, namespace, + schema, }: { query: string; tenant?: string; namespace?: string; + schema: Schema; }) => { currentQuery.current = query; currentTenant.current = tenant ?? currentTenant.current; @@ -528,7 +508,7 @@ export const useLogs = ( if (isStreaming) { pauseTailLog(); } else { - startTailLog({ query, tenant, namespace }); + startTailLog({ query, tenant, namespace, schema }); } }; @@ -537,11 +517,13 @@ export const useLogs = ( tenant, timeRange, namespace, + schema, }: { query: string; tenant?: string; timeRange?: TimeRange; namespace?: string; + schema: Schema; }) => { if (query.length === 0) { dispatch({ type: 'volumeError', payload: { error: new Error('Query is empty') } }); @@ -568,7 +550,7 @@ export const useLogs = ( volumeAbort.current(); } - await fetchConfig(); + const config = await fetchConfig(); // Volume API only accepts labels, so have to extract them from the query. // Only grabs the data within the { } @@ -584,9 +566,10 @@ export const useLogs = ( query, start, end, - config: currentConfig.current, + config, tenant: currentTenant.current, namespace, + schema, }); volumeAbort.current = abort; @@ -606,11 +589,13 @@ export const useLogs = ( tenant, timeRange, namespace, + schema, }: { query: string; tenant?: string; timeRange?: TimeRange; namespace?: string; + schema: Schema; }) => { if (query.length === 0) { dispatch({ type: 'histogramError', payload: { error: new Error('Query is empty') } }); @@ -641,16 +626,17 @@ export const useLogs = ( const { start, end } = numericTimeRange(currentTimeRange.current); - await fetchConfig(); + const config = await fetchConfig(); const { request, abort } = executeHistogramQuery({ query, start, end, interval: intervalFromTimeRange(currentTimeRange.current), - config: currentConfig.current, + config, tenant: currentTenant.current, namespace, + schema, }); histogramAbort.current = abort; @@ -687,6 +673,5 @@ export const useLogs = ( getHistogram, histogramError, toggleStreaming, - config, }; }; diff --git a/web/src/hooks/useURLState.ts b/web/src/hooks/useURLState.ts index 7ac5b5b02..43b98b5e8 100644 --- a/web/src/hooks/useURLState.ts +++ b/web/src/hooks/useURLState.ts @@ -1,15 +1,27 @@ -import React from 'react'; -import { useNavigate, useLocation } from 'react-router-dom-v5-compat'; -import { filtersFromQuery } from '../attribute-filters'; +import React, { DependencyList } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom-v5-compat'; +import { filtersFromQuery, queryFromFilters } from '../attribute-filters'; import { AttributeList, Filters } from '../components/filters/filter.types'; -import { Direction, TimeRange } from '../logs.types'; +import { Config, Direction, Schema, TimeRange } from '../logs.types'; +import { ResourceLabel, ResourceToStreamLabels } from '../parse-resources'; import { intervalFromTimeRange } from '../time-range'; +import { getSchema } from '../value-utils'; +import { useLogsConfig } from './LogsConfigProvider'; import { useQueryParams } from './useQueryParams'; interface UseURLStateHook { - defaultQuery?: string; defaultTenant?: string; - attributes: AttributeList; + getDefaultQuery?({ tenant, schema }: { tenant: string; schema: Schema }): string; + getAttributes?: ({ + tenant, + config, + schema, + }: { + tenant: string; + config: Config; + schema: Schema; + }) => AttributeList | undefined; + attributesDependencies?: DependencyList; } const QUERY_PARAM_KEY = 'q'; @@ -17,30 +29,50 @@ const TIME_RANGE_START = 'start'; const TIME_RANGE_END = 'end'; const DIRECTION = 'direction'; const TENANT_PARAM_KEY = 'tenant'; +const SCHEMA_PARAM_KEY = 'schema'; const SHOW_RESOURCES_PARAM_KEY = 'showResources'; const SHOW_STATS_PARAM_KEY = 'showStats'; -const DEFAULT_TENANT = 'application'; +export const DEFAULT_TENANT = 'application'; const DEFAULT_SHOW_RESOURCES = '0'; const DEFAULT_SHOW_STATS = '0'; -export const defaultQueryFromTenant = (tenant: string = DEFAULT_TENANT) => - `{ log_type="${tenant}" } | json`; + +export const defaultQueryFromTenant = ({ + tenant = DEFAULT_TENANT, + schema, +}: { + tenant?: string; + schema: Schema; +}) => { + const logType = ResourceToStreamLabels[ResourceLabel.LogType]; + if (schema === Schema.otel) { + return `{ ${logType.otel}="${tenant}" } `; + } + return `{ ${logType.viaq}="${tenant}" } | json`; +}; const getDirectionValue = (value?: string | null): Direction => value !== null ? (value === 'forward' ? 'forward' : 'backward') : 'backward'; export const useURLState = ({ - defaultQuery, defaultTenant = DEFAULT_TENANT, - attributes, + getDefaultQuery, + getAttributes, + attributesDependencies, }: UseURLStateHook) => { const queryParams = useQueryParams(); const navigate = useNavigate(); const location = useLocation(); + const { config } = useLogsConfig(); const initialTenant = queryParams.get(TENANT_PARAM_KEY) ?? defaultTenant; + const initialSchema: Schema = getSchema(queryParams.get(SCHEMA_PARAM_KEY) ?? config?.schema); + const initialQuery = - queryParams.get(QUERY_PARAM_KEY) ?? defaultQuery ?? defaultQueryFromTenant(initialTenant); + queryParams.get(QUERY_PARAM_KEY) ?? + getDefaultQuery?.({ tenant: initialTenant, schema: initialSchema }) ?? + defaultQueryFromTenant({ tenant: initialTenant, schema: initialSchema }); + const initialTimeRangeStart = queryParams.get(TIME_RANGE_START); const initialTimeRangeEnd = queryParams.get(TIME_RANGE_END); const initialDirection = queryParams.get(DIRECTION); @@ -51,9 +83,15 @@ export const useURLState = ({ const [query, setQuery] = React.useState(initialQuery); const [tenant, setTenant] = React.useState(initialTenant); + const [schema, setSchema] = React.useState(initialSchema); + const attributes = React.useMemo( + () => (getAttributes ? getAttributes({ tenant, config, schema }) ?? [] : []), + [tenant, config, schema, ...(attributesDependencies || [])], + ); const [filters, setFilters] = React.useState( - filtersFromQuery({ query: initialQuery, attributes }), + filtersFromQuery({ query: initialQuery, attributes, schema }), ); + const [areResourcesShown, setAreResourcesShown] = React.useState(initialResorcesShown); const [areStatsShown, setAreStatsShown] = React.useState(intitalStatsShown); const [direction, setDirection] = React.useState(getDirectionValue(initialDirection)); @@ -71,6 +109,28 @@ export const useURLState = ({ navigate(`${location.pathname}?${queryParams.toString()}`); }; + const setSchemaInURL = (selectedSchema: Schema) => { + if (selectedSchema) { + queryParams.set(SCHEMA_PARAM_KEY, selectedSchema as string); + + // re create query based on current filters and new schema + const newQuery = queryFromFilters({ + existingQuery: '', + filters, + attributes, + tenant, + schema: selectedSchema, + addJSONParser: true, + }); + queryParams.set(QUERY_PARAM_KEY, newQuery); + + navigate(`${location.pathname}?${queryParams.toString()}`); + } else { + queryParams.delete(SCHEMA_PARAM_KEY); + navigate(`${location.pathname}?${queryParams.toString()}`); + } + }; + const setShowResourcesInURL = (showResources: boolean) => { queryParams.set(SHOW_RESOURCES_PARAM_KEY, showResources ? '1' : '0'); navigate(`${location.pathname}?${queryParams.toString()}`); @@ -103,6 +163,7 @@ export const useURLState = ({ }; React.useEffect(() => { + const schemaValue = getSchema(queryParams.get(SCHEMA_PARAM_KEY) ?? config?.schema); const queryValue = queryParams.get(QUERY_PARAM_KEY) ?? initialQuery; const tenantValue = queryParams.get(TENANT_PARAM_KEY) ?? DEFAULT_TENANT; const showResourcesValue = queryParams.get(SHOW_RESOURCES_PARAM_KEY) ?? DEFAULT_SHOW_RESOURCES; @@ -113,10 +174,13 @@ export const useURLState = ({ setQuery(queryValue.trim()); setTenant(tenantValue); + setSchema(schemaValue); setDirection(getDirectionValue(directionValue)); setAreResourcesShown(showResourcesValue === '1'); setAreStatsShown(showStatsValue === '1'); - setFilters(filtersFromQuery({ query: queryValue, attributes })); + setFilters( + filtersFromQuery({ query: queryValue, attributes: attributes, schema: schemaValue }), + ); setTimeRange((prevTimeRange) => { if (!timeRangeStartValue || !timeRangeEndValue) { return undefined; @@ -134,13 +198,16 @@ export const useURLState = ({ end: timeRangeEndValue, }; }); - }, [queryParams]); + }, [queryParams, attributes]); return { + initialQuery, query, setQueryInURL, tenant, setTenantInURL, + schema, + setSchemaInURL, areResourcesShown, setShowResourcesInURL, areStatsShown, @@ -150,6 +217,7 @@ export const useURLState = ({ timeRange, setTimeRangeInURL, setDirectionInURL, + attributes, direction, interval: timeRange ? intervalFromTimeRange(timeRange) : undefined, }; diff --git a/web/src/logs.types.ts b/web/src/logs.types.ts index 7062ce71c..abf2d99f7 100644 --- a/web/src/logs.types.ts +++ b/web/src/logs.types.ts @@ -1,3 +1,15 @@ +export enum SchemaConfig { + viaq = 'viaq', + otel = 'otel', + select = 'select', // allows dropdown to appear to select either viaq of otel +} + +export enum Schema { + viaq = 'viaq', + otel = 'otel', +} +export const DEFAULT_SCHEMA = Schema.viaq; + export type Config = { useTenantInHeader?: boolean; isStreamingEnabledInDefaultPage?: boolean; @@ -5,6 +17,7 @@ export type Config = { alertingRuleNamespaceLabelKey?: string; timeout?: number; logsLimit: number; + schema: SchemaConfig; }; export type MetricValue = Array; diff --git a/web/src/loki-client.ts b/web/src/loki-client.ts index 6a9112773..8508386c7 100644 --- a/web/src/loki-client.ts +++ b/web/src/loki-client.ts @@ -5,12 +5,14 @@ import { Config, Direction, LabelValueResponse, + MatrixResult, QueryRangeResponse, - VolumeRangeResponse, RulesResponse, - MatrixResult, + Schema, + VolumeRangeResponse, } from './logs.types'; -import { durationFromTimestamp } from './value-utils'; +import { getStreamLabelsFromSchema, ResourceLabel } from './parse-resources'; +import { durationFromTimestamp, getSchema } from './value-utils'; const LOKI_ENDPOINT = '/api/proxy/plugin/logging-view-plugin/backend'; @@ -22,6 +24,7 @@ type QueryRangeParams = { namespace?: string; tenant: string; direction?: Direction; + schema: Schema; }; type VolumeRangeParams = { @@ -32,6 +35,7 @@ type VolumeRangeParams = { namespace?: string; tenant: string; targetLabels?: string; + schema: Schema; }; type HistogramQuerParams = { @@ -42,6 +46,7 @@ type HistogramQuerParams = { config?: Config; namespace?: string; tenant: string; + schema: Schema; }; type LokiTailQueryParams = { @@ -52,6 +57,7 @@ type LokiTailQueryParams = { config?: Config; namespace?: string; tenant: string; + schema: Schema; }; const MAX_RANGE_REQUEST = 60 * 60 * 6 * 1000; // 6 hours @@ -115,10 +121,12 @@ export const executeQueryRange = ({ tenant, namespace, direction, + schema, }: QueryRangeParams): CancellableFetch => { const extendedQuery = queryWithNamespace({ query, namespace, + schema, }); const params: Record = { @@ -147,10 +155,12 @@ export const executeVolumeRange = ({ config, tenant, namespace, + schema, }: VolumeRangeParams): CancellableFetch => { const extendedQuery = queryWithNamespace({ query, namespace, + schema, }); const params: Record = { @@ -191,15 +201,20 @@ export const executeHistogramQuery = ({ config, tenant, namespace, + schema, }: HistogramQuerParams): CancellableFetch> => { const intervalString = durationFromTimestamp(interval); + const labels = getStreamLabelsFromSchema(schema); + const labelSeverity = labels[ResourceLabel.Severity]; const extendedQuery = queryWithNamespace({ query, namespace, + schema, }); - const histogramQuery = `sum by (level) (count_over_time(${extendedQuery} [${intervalString}]))`; + // eslint-disable-next-line max-len + const histogramQuery = `sum by (${labelSeverity}) (count_over_time(${extendedQuery} [${intervalString}]))`; const params = { query: histogramQuery, @@ -271,10 +286,17 @@ export const executeHistogramQuery = ({ ); }; -export const connectToTailSocket = ({ query, config, tenant, namespace }: LokiTailQueryParams) => { +export const connectToTailSocket = ({ + query, + config, + tenant, + namespace, + schema, +}: LokiTailQueryParams) => { const extendedQuery = queryWithNamespace({ query, namespace, + schema, }); const params = { @@ -310,8 +332,10 @@ export const getRules = ({ let url = `${endpoint}/prometheus/api/v1/rules`; - const alertingRulesNamespaceLabelKey = - config?.alertingRuleNamespaceLabelKey || 'kubernetes_namespace_name'; + const labelMatchers = getStreamLabelsFromSchema(getSchema(config?.schema)); + const namespaceLabel = labelMatchers[ResourceLabel.Namespace]; + + const alertingRulesNamespaceLabelKey = config?.alertingRuleNamespaceLabelKey || namespaceLabel; if (namespace) { url = `${url}?${alertingRulesNamespaceLabelKey}=${namespace}`; diff --git a/web/src/pages/logs-detail-page.tsx b/web/src/pages/logs-detail-page.tsx index d0ba4d0bb..d89604b09 100644 --- a/web/src/pages/logs-detail-page.tsx +++ b/web/src/pages/logs-detail-page.tsx @@ -13,21 +13,23 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; import { availablePodAttributes, filtersFromQuery, queryFromFilters } from '../attribute-filters'; -import { AttributeList, Filters } from '../components/filters/filter.types'; +import { CenteredContainer } from '../components/centered-container'; +import { Filters } from '../components/filters/filter.types'; import { LogsHistogram } from '../components/logs-histogram'; +import { LogsMetrics } from '../components/logs-metrics'; import { LogsTable } from '../components/logs-table'; import { LogsToolbar } from '../components/logs-toolbar'; import { RefreshIntervalDropdown } from '../components/refresh-interval-dropdown'; import { TimeRangeDropdown } from '../components/time-range-dropdown'; import { ToggleHistogramButton } from '../components/toggle-histogram-button'; +import { downloadCSV } from '../download-csv'; +import { LogsConfigProvider } from '../hooks/LogsConfigProvider'; import { useLogs } from '../hooks/useLogs'; import { useURLState } from '../hooks/useURLState'; -import { Direction, isMatrixResult } from '../logs.types'; +import { Direction, isMatrixResult, Schema } from '../logs.types'; +import { getStreamLabelsFromSchema, ResourceLabel } from '../parse-resources'; import { TestIds } from '../test-ids'; import { getInitialTenantFromNamespace } from '../value-utils'; -import { CenteredContainer } from '../components/centered-container'; -import { LogsMetrics } from '../components/logs-metrics'; -import { downloadCSV } from '../download-csv'; /* This comment creates an entry in the translations catalogue for console extensions @@ -51,7 +53,6 @@ const LogsDetailPage: React.FC = ({ useParams<{ name: string; ns: string }>(); const namespace = namespaceFromParams || namespaceFromProps; const podname = podnameFromParams || podNameFromProps; - const defaultQuery = `{ kubernetes_pod_name = "${podname}" } | json`; const [isHistogramVisible, setIsHistogramVisible] = React.useState(false); const { @@ -73,17 +74,14 @@ const LogsDetailPage: React.FC = ({ histogramData, isLoadingHistogramData, histogramError, - config, } = useLogs(); - const attributesForPod: AttributeList = React.useMemo( - () => (namespace && podname ? availablePodAttributes(namespace, podname, config) : []), - [podname, config], - ); - const { + initialQuery, query, setQueryInURL, + schema, + setSchemaInURL, areResourcesShown, setShowResourcesInURL, areStatsShown, @@ -95,20 +93,32 @@ const LogsDetailPage: React.FC = ({ timeRange, direction, setDirectionInURL, + attributes, } = useURLState({ - defaultQuery, - attributes: attributesForPod, + getDefaultQuery: ({ schema: s }) => { + const labelMatchers = getStreamLabelsFromSchema(s); + const podLabel = labelMatchers[ResourceLabel.Pod]; + + return `{ ${podLabel} = "${podname}" }${s == Schema.viaq ? ' | json' : ''}`; + }, + getAttributes: ({ config: c, schema: s }) => { + if (namespace && podname) { + return availablePodAttributes(namespace, podname, c, s); + } + }, + attributesDependencies: [namespace, podname], }); + const initialTenant = getInitialTenantFromNamespace(namespace); const tenant = React.useRef(initialTenant); const handleToggleStreaming = () => { - toggleStreaming({ query }); + toggleStreaming({ query, schema }); }; const handleLoadMoreData = (lastTimestamp: number) => { if (!isLoadingMoreLogsData) { - getMoreLogs({ lastTimestamp, query, namespace, direction }); + getMoreLogs({ lastTimestamp, query, namespace, direction, schema }); } }; @@ -117,27 +127,28 @@ const LogsDetailPage: React.FC = ({ }; const runQuery = () => { - getLogs({ query, tenant: tenant.current, namespace, timeRange, direction }); + getLogs({ query, tenant: tenant.current, namespace, timeRange, direction, schema }); if (isHistogramVisible) { - getHistogram({ query, tenant: tenant.current, namespace, timeRange }); + getHistogram({ query, tenant: tenant.current, namespace, timeRange, schema }); } }; const runVolume = () => { - getVolume({ query, tenant: tenant.current, namespace, timeRange }); + getVolume({ query, tenant: tenant.current, namespace, timeRange, schema }); }; const handleFiltersChange = (selectedFilters?: Filters) => { setFilters(selectedFilters); if (!selectedFilters || Object.keys(selectedFilters).length === 0) { - setQueryInURL(defaultQuery); + setQueryInURL(initialQuery); } else { const updatedQuery = queryFromFilters({ existingQuery: query, filters: selectedFilters, - attributes: attributesForPod, + attributes, + schema, }); setQueryInURL(updatedQuery); } @@ -148,7 +159,8 @@ const LogsDetailPage: React.FC = ({ const updatedFilters = filtersFromQuery({ query: queryFromInput, - attributes: attributesForPod, + attributes, + schema, }); setFilters(updatedFilters); @@ -226,10 +238,12 @@ const LogsDetailPage: React.FC = ({ enableStreaming enableTenantDropdown={false} isDisabled={isQueryEmpty} - attributeList={attributesForPod} + attributeList={attributes} filters={filters} onFiltersChange={handleFiltersChange} onDownloadCSV={() => downloadCSV(logsData)} + schema={schema} + onSchemaSelect={setSchemaInURL} /> {isLoadingLogsData ? ( @@ -280,4 +294,12 @@ const LogsDetailPage: React.FC = ({ ); }; -export default LogsDetailPage; +const LogsDetailPageWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +export default LogsDetailPageWrapper; diff --git a/web/src/pages/logs-dev-page.tsx b/web/src/pages/logs-dev-page.tsx index 8b2d46a70..75ce48f05 100644 --- a/web/src/pages/logs-dev-page.tsx +++ b/web/src/pages/logs-dev-page.tsx @@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom-v5-compat'; import { availableDevConsoleAttributes, - initialAvailableAttributes, filtersFromQuery, + initialAvailableAttributes, queryFromFilters, } from '../attribute-filters'; import { CenteredContainer } from '../components/centered-container'; @@ -18,12 +18,13 @@ import { LogsToolbar } from '../components/logs-toolbar'; import { RefreshIntervalDropdown } from '../components/refresh-interval-dropdown'; import { TimeRangeDropdown } from '../components/time-range-dropdown'; import { ToggleHistogramButton } from '../components/toggle-histogram-button'; +import { downloadCSV } from '../download-csv'; +import { LogsConfigProvider, useLogsConfig } from '../hooks/LogsConfigProvider'; import { useLogs } from '../hooks/useLogs'; import { defaultQueryFromTenant, useURLState } from '../hooks/useURLState'; import { Direction, isMatrixResult } from '../logs.types'; import { TestIds } from '../test-ids'; import { getInitialTenantFromNamespace } from '../value-utils'; -import { downloadCSV } from '../download-csv'; /* This comment creates an entry in the translations catalogue for console extensions @@ -43,21 +44,7 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => const [isHistogramVisible, setIsHistogramVisible] = React.useState(false); let tenant = getInitialTenantFromNamespace(namespace); - const { - query, - setQueryInURL, - areResourcesShown, - setShowResourcesInURL, - areStatsShown, - setShowStatsInURL, - filters, - setFilters, - setTimeRangeInURL, - timeRange, - interval, - direction, - setDirectionInURL, - } = useURLState({ attributes: initialAvailableAttributes, defaultTenant: tenant }); + const { config } = useLogsConfig(); const { histogramData, @@ -78,16 +65,39 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => hasMoreLogsData, getHistogram, toggleStreaming, - config, } = useLogs(); + const { + query, + setQueryInURL, + schema, + setSchemaInURL, + areResourcesShown, + setShowResourcesInURL, + areStatsShown, + setShowStatsInURL, + filters, + setFilters, + setTimeRangeInURL, + timeRange, + interval, + direction, + setDirectionInURL, + attributes, + } = useURLState({ + defaultTenant: tenant, + getAttributes: ({ config: c, schema: s }) => + availableDevConsoleAttributes(getInitialTenantFromNamespace(namespace), c, s), + attributesDependencies: [namespace], + }); + const handleToggleStreaming = () => { - toggleStreaming({ query }); + toggleStreaming({ query, schema }); }; const handleLoadMoreData = (lastTimestamp: number) => { if (!isLoadingMoreLogsData) { - getMoreLogs({ lastTimestamp, query, namespace, direction }); + getMoreLogs({ lastTimestamp, query, namespace, direction, schema }); } }; @@ -101,15 +111,16 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => timeRange, direction, tenant, + schema, }); if (isHistogramVisible) { - getHistogram({ query: queryToUse ?? query, timeRange, tenant }); + getHistogram({ query: queryToUse ?? query, timeRange, tenant, schema }); } }; const runVolume = () => { - getVolume({ query, tenant, namespace, timeRange }); + getVolume({ query, tenant, namespace, timeRange, schema }); }; const handleFiltersChange = (selectedFilters?: Filters) => { @@ -122,7 +133,8 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => const updatedFilters = filtersFromQuery({ query: queryFromInput, - attributes: initialAvailableAttributes, + attributes: initialAvailableAttributes(schema), + schema: schema, }); setFilters(updatedFilters); @@ -137,10 +149,11 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => if (hasNoSelectedfilters) { const updatedQuery = queryFromFilters({ - existingQuery: defaultQueryFromTenant(selectedTenant), + existingQuery: defaultQueryFromTenant({ tenant: selectedTenant, schema }), filters: { namespace: new Set(namespace ? [namespace] : []) }, - attributes: initialAvailableAttributes, + attributes: initialAvailableAttributes(schema), tenant: selectedTenant, + schema, }); setQueryInURL(updatedQuery); @@ -150,8 +163,9 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => const updatedQuery = queryFromFilters({ existingQuery: query, filters: selectedFilters, - attributes: initialAvailableAttributes, + attributes: initialAvailableAttributes(schema), tenant: selectedTenant, + schema, }); setQueryInURL(updatedQuery); @@ -160,14 +174,6 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => } }; - const attributeList = React.useMemo( - () => - namespace - ? availableDevConsoleAttributes(getInitialTenantFromNamespace(namespace), config) - : [], - [namespace, config], - ); - React.useEffect(() => { tenant = getInitialTenantFromNamespace(namespace); @@ -259,10 +265,12 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => onShowStatsToggle={setShowStatsInURL} enableTenantDropdown={false} isDisabled={isRunQueryDisabled} - attributeList={attributeList} + attributeList={attributes} filters={filters} onFiltersChange={handleFiltersChange} onDownloadCSV={() => downloadCSV(logsData)} + schema={schema} + onSchemaSelect={setSchemaInURL} /> {isLoadingLogsData ? ( @@ -313,4 +321,12 @@ const LogsDevPage: React.FC = ({ ns: namespaceFromProps }) => ); }; -export default LogsDevPage; +const LogsDevPageWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +export default LogsDevPageWrapper; diff --git a/web/src/pages/logs-page.tsx b/web/src/pages/logs-page.tsx index 534c2813e..0b26d2162 100644 --- a/web/src/pages/logs-page.tsx +++ b/web/src/pages/logs-page.tsx @@ -13,10 +13,11 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { availableAttributes, - initialAvailableAttributes, filtersFromQuery, + initialAvailableAttributes, queryFromFilters, } from '../attribute-filters'; +import { CenteredContainer } from '../components/centered-container'; import { Filters } from '../components/filters/filter.types'; import { LogsHistogram } from '../components/logs-histogram'; import { LogsMetrics } from '../components/logs-metrics'; @@ -25,34 +26,19 @@ import { LogsToolbar } from '../components/logs-toolbar'; import { RefreshIntervalDropdown } from '../components/refresh-interval-dropdown'; import { TimeRangeDropdown } from '../components/time-range-dropdown'; import { ToggleHistogramButton } from '../components/toggle-histogram-button'; +import { downloadCSV } from '../download-csv'; +import { LogsConfigProvider, useLogsConfig } from '../hooks/LogsConfigProvider'; import { useLogs } from '../hooks/useLogs'; import { defaultQueryFromTenant, useURLState } from '../hooks/useURLState'; import { Direction, isMatrixResult } from '../logs.types'; import { TestIds } from '../test-ids'; -import { CenteredContainer } from '../components/centered-container'; -import { downloadCSV } from '../download-csv'; const LogsPage: React.FC = () => { const { t } = useTranslation('plugin__logging-view-plugin'); const [isHistogramVisible, setIsHistogramVisible] = React.useState(false); - const { - query, - setQueryInURL, - tenant, - setTenantInURL, - areResourcesShown, - setShowResourcesInURL, - areStatsShown, - setShowStatsInURL, - filters, - setFilters, - setTimeRangeInURL, - timeRange, - interval, - direction, - setDirectionInURL, - } = useURLState({ attributes: initialAvailableAttributes }); + + const { config } = useLogsConfig(); const { histogramData, @@ -73,16 +59,38 @@ const LogsPage: React.FC = () => { hasMoreLogsData, getHistogram, toggleStreaming, - config, } = useLogs(); + const { + query, + setQueryInURL, + tenant, + setTenantInURL, + schema, + setSchemaInURL, + areResourcesShown, + setShowResourcesInURL, + areStatsShown, + setShowStatsInURL, + filters, + setFilters, + setTimeRangeInURL, + timeRange, + interval, + direction, + setDirectionInURL, + attributes, + } = useURLState({ + getAttributes: availableAttributes, + }); + const handleToggleStreaming = () => { - toggleStreaming({ query }); + toggleStreaming({ query, schema }); }; const handleLoadMoreData = (lastTimestamp: number) => { if (!isLoadingMoreLogsData) { - getMoreLogs({ lastTimestamp, query, direction }); + getMoreLogs({ lastTimestamp, query, direction, schema }); } }; @@ -91,15 +99,15 @@ const LogsPage: React.FC = () => { }; const runQuery = ({ queryToUse }: { queryToUse?: string } = {}) => { - getLogs({ query: queryToUse ?? query, tenant, timeRange, direction }); + getLogs({ query: queryToUse ?? query, tenant, timeRange, direction, schema }); if (isHistogramVisible) { - getHistogram({ query: queryToUse ?? query, tenant, timeRange }); + getHistogram({ query: queryToUse ?? query, tenant, timeRange, schema }); } }; const runVolume = () => { - getVolume({ query, tenant, timeRange }); + getVolume({ query, tenant, timeRange, schema }); }; const handleFiltersChange = (selectedFilters?: Filters) => { @@ -112,7 +120,8 @@ const LogsPage: React.FC = () => { const updatedFilters = filtersFromQuery({ query: queryFromInput, - attributes: initialAvailableAttributes, + attributes: initialAvailableAttributes(schema), + schema: schema, }); setFilters(updatedFilters); @@ -120,7 +129,7 @@ const LogsPage: React.FC = () => { const updateQuery = (selectedFilters?: Filters, selectedTenant?: string): string => { if ((!selectedFilters || Object.keys(selectedFilters).length === 0) && !selectedTenant) { - const defaultQuery = defaultQueryFromTenant(); + const defaultQuery = defaultQueryFromTenant({ schema }); setQueryInURL(defaultQuery); @@ -129,8 +138,9 @@ const LogsPage: React.FC = () => { const updatedQuery = queryFromFilters({ existingQuery: query, filters: selectedFilters, - attributes: initialAvailableAttributes, + attributes: initialAvailableAttributes(schema), tenant: selectedTenant, + schema, }); setQueryInURL(updatedQuery); @@ -139,11 +149,6 @@ const LogsPage: React.FC = () => { } }; - const attributeList = React.useMemo( - () => (tenant ? availableAttributes(tenant, config) : []), - [tenant, config], - ); - const handleRefreshClick = () => { runQuery(); }; @@ -206,6 +211,7 @@ const LogsPage: React.FC = () => { isLoading={isLoadingHistogramData} error={histogramError} onChangeTimeRange={setTimeRangeInURL} + schema={schema} /> )} { showStats={areStatsShown} onShowStatsToggle={setShowStatsInURL} isDisabled={isQueryEmpty} - attributeList={attributeList} + attributeList={attributes} filters={filters} onFiltersChange={handleFiltersChange} onDownloadCSV={() => downloadCSV(logsData)} + schema={schema} + onSchemaSelect={setSchemaInURL} /> {isLoadingLogsData ? ( @@ -277,4 +285,12 @@ const LogsPage: React.FC = () => { ); }; -export default LogsPage; +const LogsPageWrapper: React.FC = () => { + return ( + + + + ); +}; + +export default LogsPageWrapper; diff --git a/web/src/parse-resources.tsx b/web/src/parse-resources.tsx index 36d12e7df..48d37a96e 100644 --- a/web/src/parse-resources.tsx +++ b/web/src/parse-resources.tsx @@ -1,4 +1,4 @@ -import { Resource } from './logs.types'; +import { Resource, Schema } from './logs.types'; import { notUndefined } from './value-utils'; export enum ResourceLabel { @@ -6,9 +6,37 @@ export enum ResourceLabel { Namespace = 'Namespace', Pod = 'Pod', Severity = 'Severity', + LogType = 'LogType', } -const ResourceToStreamLabels: Record = { +export const getOtelLabels = (): Record => { + const otelMap = Object.fromEntries( + Object.entries(ResourceToStreamLabels).map(([key, value]) => [key, value.otel]), + ); + return { + ...otelMap, + Schema: Schema.otel, // manually add Schema key + } as Record; +}; + +export const getViaQLabels = (): Record => { + const otelMap = Object.fromEntries( + Object.entries(ResourceToStreamLabels).map(([key, value]) => [key, value.viaq]), + ); + return { + ...otelMap, + Schema: Schema.viaq, // manually add Schema key + } as Record; +}; + +export const getStreamLabelsFromSchema = (schema: Schema) => { + if (schema == Schema.otel) { + return getOtelLabels(); + } + return getViaQLabels(); +}; + +export const ResourceToStreamLabels: Record = { [ResourceLabel.Container]: { otel: 'k8s_container_name', viaq: 'kubernetes_container_name', @@ -25,6 +53,10 @@ const ResourceToStreamLabels: Record, resourceLabel: ResourceLabel) => { diff --git a/web/src/test-ids.ts b/web/src/test-ids.ts index c7273227c..1fc8e9174 100644 --- a/web/src/test-ids.ts +++ b/web/src/test-ids.ts @@ -22,4 +22,5 @@ export enum TestIds { AttributeOptions = 'AttributeOptions', NamespaceDropdown = 'NamespaceDropdown', NamespaceToggle = 'NamespaceToggle', + SchemaToggle = 'SchemaToggle', } diff --git a/web/src/value-utils.ts b/web/src/value-utils.ts index fe9d366d0..c4cd63bc1 100644 --- a/web/src/value-utils.ts +++ b/web/src/value-utils.ts @@ -1,3 +1,5 @@ +import { DEFAULT_SCHEMA, Schema, SchemaConfig } from './logs.types'; + /** * Converts a value into a string with scale prefix * @example @@ -88,3 +90,17 @@ export const capitalize = (str?: string): string => { } return str.charAt(0).toUpperCase() + str.slice(1); }; + +export const getSchema = (value: string | null | undefined | SchemaConfig): Schema => { + switch (value) { + case Schema.otel: + case SchemaConfig.otel: + return Schema.otel; + case Schema.viaq: + case SchemaConfig.viaq: + return Schema.viaq; + + default: + return DEFAULT_SCHEMA; + } +};