Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions hack/docker-compose/loki/promtail-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,5 @@ scrape_configs:
k8s_namespace_name: kube-public
log_type: application
__path__: /var/log/*log
kubernetes_pod_name: alertmanager-main-0
kubernetes_container_name: test-container
kubernetes_pod_name: kube-public-0
kubernetes_container_name: test-container-kube
8 changes: 7 additions & 1 deletion web/locales/en/plugin__logging-view-plugin.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"No namespaces found": "No namespaces found",
"No pods found in the selected namespace(s)": "No pods found in the selected namespace(s)",
"No pods found": "No pods found",
"No containers found in the selected namespace(s)": "No containers found in the selected namespace(s)",
"No containers found": "No containers found",
"Label {{tenantKey}} is required to display the alert metrics": "label {{tenantKey}} is required to display the alert metrics",
"Select a smaller time range to reduce the number of results": "Select a smaller time range to reduce the number of results",
"Select a namespace, pod, or container filter to improve the query performance": "Select a namespace, pod, or container filter to improve the query performance",
Expand All @@ -18,6 +23,7 @@
"Explain Log Volume": "Explain Log Volume",
"Search by {{attributeName}}": "Search by {{attributeName}}",
"Attribute": "Attribute",
"No results found": "No results found",
"Filter by {{attributeName}}": "Filter by {{attributeName}}",
"Search": "Search",
"Loading...": "Loading...",
Expand Down Expand Up @@ -141,4 +147,4 @@
"Aggregated Logs": "Aggregated Logs",
"Logs": "Logs",
"Please select a namespace": "Please select a namespace"
}
}
135 changes: 120 additions & 15 deletions web/src/attribute-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@ const lokiSeriesDataSource =
mapper: (data: SeriesResponse) => Array<{ option: string; value: string }>;
}) =>
async (): Promise<Array<{ option: string; value: string }>> => {
const { abort, request } = executeSeries({ match, tenant, config });
const { abort, request } = executeSeries({
match: match.filter(notEmptyString),
tenant,
config,
});

if (resourceAbort.lokiSeries) {
resourceAbort.lokiSeries();
Expand Down Expand Up @@ -142,12 +146,14 @@ const resourceDataSource =

const { request, abort } = cancellableFetch<K8sResourceListResponse>(endpoint);

const abortFunction = resourceAbort[resource];
const abortKey = namespace ? `${resource}-${namespace}` : resource;

const abortFunction = resourceAbort[abortKey];
if (abortFunction) {
abortFunction();
}

resourceAbort[resource] = abort;
resourceAbort[abortKey] = abort;

const response = await request();

Expand Down Expand Up @@ -211,10 +217,12 @@ export const availableAttributes = ({
tenant,
config,
schema,
t,
}: {
tenant: string;
config: Config;
schema: Schema;
t: (key: string) => string;
}): AttributeList => {
const { namespaceLabel, podLabel, containerLabel } = getAttributeLabels(schema);

Expand Down Expand Up @@ -249,19 +257,35 @@ export const availableAttributes = ({
},
}),
valueType: 'checkbox-select',
emptyStateMessage: t('No namespaces found'),
},
{
name: 'Pods',
label: podLabel,
id: 'pod',
options: getPodAttributeOptions(tenant, config, schema),
options: (filters) => {
const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined;

return getPodAttributeOptions(tenant, config, schema, selectedNamespaces)();
},
valueType: 'checkbox-select',
emptyStateMessage: (filters) => {
const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined;
if (selectedNamespaces && selectedNamespaces.length > 0) {
return t('No pods found in the selected namespace(s)');
}
return t('No pods found');
},
},
{
name: 'Containers',
label: containerLabel,
id: 'container',
options: getContainerAttributeOptions(tenant, config, schema),
options: (filters) => {
const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined;

return getContainerAttributeOptions(tenant, config, schema, selectedNamespaces)();
},
expandSelection: (selections) => {
const podSelections = new Set<string>();
const containerSelections = new Set<string>();
Expand Down Expand Up @@ -313,6 +337,13 @@ export const availableAttributes = ({
return filters.pod.has(pod) && filters.container.has(container);
},
valueType: 'checkbox-select',
emptyStateMessage: (filters) => {
const selectedNamespaces = filters?.namespace ? Array.from(filters.namespace) : undefined;
if (selectedNamespaces && selectedNamespaces.length > 0) {
return t('No containers found in the selected namespace(s)');
}
return t('No containers found');
},
},
];
};
Expand Down Expand Up @@ -724,17 +755,51 @@ const getPodAttributeOptions = (
tenant: string,
config: Config,
schema: Schema,
namespaces?: Array<string>,
): (() => Promise<Option[]>) => {
const { podLabel } = getAttributeLabels(schema);

const namespacedPodsResources: Array<Promise<Option[]>> = [];

// get pods in selected namespaces for users that have restricted access
for (const ns of namespaces || []) {
namespacedPodsResources.push(resourceDataSource({ resource: 'pods', namespace: ns })());
}

const namespaceLabel = getStreamLabelsFromSchema(schema).Namespace;
const namespacesQuery =
namespaces && namespaces.length > 0 ? `${namespaceLabel}=~"${namespaces.join('|')}"` : '';

const podResource =
namespaces && namespaces.length > 0
? namespacedPodsResources
: [
resourceDataSource({
resource: 'pods',
filter: (resource) => {
switch (tenant) {
case 'infrastructure':
return namespaceBelongsToInfrastructureTenant(resource.metadata?.namespace || '');
case 'application':
return !namespaceBelongsToInfrastructureTenant(
resource.metadata?.namespace || '',
);
}

return true;
},
})(),
];

return () =>
Promise.allSettled<Promise<Option[]>>([
lokiLabelValuesDataSource({
config,
tenant,
labelName: podLabel,
query: namespacesQuery ? `{ ${namespacesQuery} }` : undefined,
})(),
resourceDataSource({ resource: 'pods' })(),
...podResource,
]).then((results) => {
const podOptions: Set<Option> = new Set();
results.forEach((result) => {
Expand All @@ -752,17 +817,64 @@ const getContainerAttributeOptions = (
tenant: string,
config: Config,
schema: Schema,
namespaces?: Array<string>,
): (() => Promise<Option[]>) => {
const { containerLabel, podLabel } = getAttributeLabels(schema);

const seriesQuery = `{ ${containerLabel}!="", ${podLabel}!="" }`;

const namespacedPodsResources: Array<Promise<Option[]>> = [];

// get containers in selected namespaces for users that have restricted access
for (const ns of namespaces || []) {
namespacedPodsResources.push(
resourceDataSource({
resource: 'pods',
namespace: ns,
mapper: (resource) =>
resource?.spec?.containers.map((container) => ({
option: `${resource?.metadata?.name} / ${container.name}`,
value: `${resource?.metadata?.name} / ${container.name}`,
})) ?? [],
})(),
);
}

const namespaceLabel = getStreamLabelsFromSchema(schema).Namespace;
const namespacesQuery = namespaces ? `${namespaceLabel}=~"${namespaces.join('|')}"` : '';

const podResource =
namespaces && namespaces.length > 0
? namespacedPodsResources
: [
resourceDataSource({
resource: 'pods',
filter: (resource) => {
switch (tenant) {
case 'infrastructure':
return namespaceBelongsToInfrastructureTenant(resource.metadata?.namespace || '');
case 'application':
return !namespaceBelongsToInfrastructureTenant(
resource.metadata?.namespace || '',
);
}

return true;
},
mapper: (resource) =>
resource?.spec?.containers.map((container) => ({
option: `${resource?.metadata?.name} / ${container.name}`,
value: `${resource?.metadata?.name} / ${container.name}`,
})) ?? [],
})(),
];

return () =>
Promise.allSettled<Promise<Option[]>>([
lokiSeriesDataSource({
config,
tenant,
match: [seriesQuery],
match: [seriesQuery, namespacesQuery ? `{ ${namespacesQuery} }` : ''],
mapper: (response) => {
const uniqueContainers = new Set<string>();

Expand All @@ -778,14 +890,7 @@ const getContainerAttributeOptions = (
}));
},
})(),
resourceDataSource({
resource: 'pods',
mapper: (resource) =>
resource?.spec?.containers.map((container) => ({
option: `${resource?.metadata?.name} / ${container.name}`,
value: `${resource?.metadata?.name} / ${container.name}`,
})) ?? [],
})(),
...podResource,
]).then((results) => {
const uniqueContainers = new Set<Option>();
results.forEach((result) => {
Expand Down
9 changes: 6 additions & 3 deletions web/src/components/filters/attribute-filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,15 @@ export const AttributeFilter: React.FC<AttributeFilterProps> = ({
</MenuToggle>
);

const namespaces = filters['namespace'] ? Array.from(filters['namespace']) : [];
const namespacesKey = namespaces.sort().join('-');

const renderAttributeValueComponent = (attribute: Attribute) => {
switch (attribute.valueType) {
case 'text': {
return (
<TextInput
key={`text-${attribute.id}`}
key={`text-${attribute.id}-${namespacesKey}`}
placeholder={t('Search by {{attributeName}}', {
attributeName: attribute.name,
})}
Expand All @@ -191,7 +194,7 @@ export const AttributeFilter: React.FC<AttributeFilterProps> = ({
case 'select':
return (
<SearchSelect
key={`select-${attribute.id}`}
key={`select-${attribute.id}-${namespacesKey}`}
attribute={attribute}
onSelect={handleAttributeValueChange}
filters={filters}
Expand All @@ -201,7 +204,7 @@ export const AttributeFilter: React.FC<AttributeFilterProps> = ({
case 'checkbox-select':
return (
<SearchSelect
key={`checkbox-select-${attribute.id}`}
key={`checkbox-select-${attribute.id}-${namespacesKey}`}
attribute={attribute}
onSelect={handleAttributeValueChange}
filters={filters}
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/filters/attribute-value-data.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { Attribute, Option } from './filter.types';
import { Attribute, Filters, Option } from './filter.types';
import { useBoolean } from '../../hooks/useBoolean';

type UseAttributeValueDataHookResult = {
getAttributeOptions: (searchQuery?: string) => void;
getAttributeOptions: (filters?: Filters) => void;
attributeOptions: Array<Option>;
attributeError: Error | undefined;
attributeLoading: boolean;
Expand All @@ -21,15 +21,15 @@ export const useAttributeValueData = (attribute: Attribute): UseAttributeValueDa
const [attributeError, setAttributeError] = React.useState<Error | undefined>();

const getAttributeOptions = React.useCallback(
(searchQuery?: string) => {
(filters?: Filters) => {
setAttributeError(undefined);
if (attribute.options) {
if (Array.isArray(attribute.options)) {
setAttributeLoading(false);
setAttributeOptions(uniqueOptions(attribute.options));
} else {
attribute
.options(searchQuery)
.options(filters)
.then((asyncOptions) => {
setAttributeOptions(uniqueOptions(asyncOptions ?? []));
})
Expand Down
3 changes: 2 additions & 1 deletion web/src/components/filters/filter.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ export type Attribute = {
label?: string;
name: string;
id: string;
options?: Array<Option> | ((searchQuery?: string) => Promise<Array<Option>>);
options?: Array<Option> | ((filters?: Filters) => Promise<Array<Option>>);
valueType: 'text' | 'select' | 'checkbox-select';
// upon selection of an option, this function is called to expand filters into other attributes
expandSelection?: (value: Set<string>) => Map<string, Set<string>>;
// determines if an option is selected based on other filters
isItemSelected?: (value: string, filters: Filters) => boolean;
emptyStateMessage?: ((filters: Filters) => string) | string;
};

export type AttributeList = Array<Attribute>;
24 changes: 23 additions & 1 deletion web/src/components/filters/search-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ const getOptionComponents = (
selections: Array<string>,
focusedItemIndex: number | null,
onInputKeyDown: (event: React.KeyboardEvent) => void,
emptyStateMessage?: string | null,
) => {
const { t } = useTranslation('plugin__logging-view-plugin');

if (attributeLoading) {
return [
<SelectOption isLoading key="custom-loading" value="loading" hasCheckbox={false}>
Expand Down Expand Up @@ -80,6 +83,19 @@ const getOptionComponents = (
return String(a.value).localeCompare(String(b.value));
});

if (sortedOptions.length === 0) {
return [
<SelectOption key="no-results" value={NO_RESULTS} isAriaDisabled>
<Alert
variant="warning"
isInline
isPlain
title={emptyStateMessage || t('No results found')}
/>
</SelectOption>,
];
}

return sortedOptions.map((attributeOption, index) => (
<SelectOption
key={attributeOption.value || attributeOption.children}
Expand Down Expand Up @@ -226,8 +242,13 @@ export const SearchSelect: React.FC<SearchSelectProps> = ({
textInputRef?.current?.focus();
};

const emptyStateMessage =
typeof attribute.emptyStateMessage === 'function'
? attribute.emptyStateMessage(filters)
: attribute.emptyStateMessage;

React.useEffect(() => {
getAttributeOptions();
getAttributeOptions(filters);
}, [tenant]);

const resetActiveAndFocusedItem = () => {
Expand Down Expand Up @@ -369,6 +390,7 @@ export const SearchSelect: React.FC<SearchSelectProps> = ({
selections,
focusedItemIndex,
onInputKeyDown,
emptyStateMessage,
)}
</SelectList>
</Select>
Expand Down
Loading