Skip to content
Merged
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
53 changes: 52 additions & 1 deletion web/cypress/integration/logs-dev-page.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ describe('Logs Dev Page', () => {

cy.wait('@resourceQuery').then(({ request }) => {
const url = new URL(request.url);
expect(url.pathname).to.equal('/api/kubernetes/api/v1/namespaces/my-namespace/pods');
expect(url.pathname).to.equal('/api/kubernetes/api/v1/pods');
});
});

Expand Down Expand Up @@ -338,4 +338,55 @@ describe('Logs Dev Page', () => {

cy.getByTestId(TestIds.LogsMetrics).should('exist');
});

it('loads the current namespace as a filter in the query', () => {
cy.intercept(QUERY_RANGE_STREAMS_URL_MATCH, queryRangeMatrixValidResponse());

cy.visit(LOGS_DEV_PAGE_URL);

cy.getByTestId(TestIds.ShowQueryToggle).click();

cy.getByTestId(TestIds.LogsQueryInput).within(() => {
cy.get('textarea').contains('kubernetes_namespace_name="my-namespace"');
});
});

it('updates the query to include the current selected namespace as a filter', () => {
cy.intercept(QUERY_RANGE_STREAMS_URL_MATCH, queryRangeMatrixValidResponse());

cy.visit(LOGS_DEV_PAGE_URL);

cy.getByTestId(TestIds.ShowQueryToggle).click();

cy.getByTestId(TestIds.LogsQueryInput).within(() => {
cy.get('textarea').contains('kubernetes_namespace_name="my-namespace"');
});

cy.getByTestId('namespace-toggle' as TestIds).click();
cy.getByTestId('namespace-dropdown' as TestIds)
.contains('my-namespace-two')
.click();

cy.getByTestId(TestIds.LogsQueryInput).within(() => {
cy.get('textarea').contains('kubernetes_namespace_name="my-namespace-two"');
});
});

it('disables the run query button when there is no selected namespace', () => {
cy.intercept(QUERY_RANGE_STREAMS_URL_MATCH, queryRangeMatrixValidResponse());

cy.visit(LOGS_DEV_PAGE_URL);

cy.getByTestId(TestIds.ShowQueryToggle).click();

cy.getByTestId(TestIds.LogsQueryInput).within(() => {
cy.get('textarea').type('{selectAll}').type('{backspace}').type('{ job = "some_job" }', {
parseSpecialCharSequences: false,
});
});

cy.getByTestId(TestIds.ExecuteQueryButton).should('be.disabled');

cy.contains('Please select a namespace');
});
});
3 changes: 2 additions & 1 deletion web/locales/en/plugin__logging-view-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@
"Start streaming": "Start streaming",
"See related logs": "See related logs",
"Aggregated Logs": "Aggregated Logs",
"Logs": "Logs"
"Logs": "Logs",
"Please select a namespace": "Please select a namespace"
}
30 changes: 29 additions & 1 deletion web/src/attribute-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ type ResourceOptionMapper = (resource: K8sResource) => Option | Array<Option>;

const resourceAbort: Record<string, null | (() => void)> = {};

const projectsDataSource = () => async (): Promise<Array<{ option: string; value: string }>> => {
const { request, abort } = cancellableFetch<K8sResourceListResponse>(
`/api/kubernetes/apis/project.openshift.io/v1/projects`,
);

if (resourceAbort.projects) {
resourceAbort.projects();
}

resourceAbort.projects = abort;

const response = await request();

return response.items
.map((project) => ({
option: project?.metadata?.name ?? '',
value: project?.metadata?.name ?? '',
}))
.filter(({ value }) => notEmptyString(value));
};

const resourceDataSource =
({
resource,
Expand Down Expand Up @@ -113,11 +134,18 @@ export const availableDevConsoleAttributes = (namespace: string): AttributeList
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: resourceDataSource({ resource: 'pods', namespace }),
options: resourceDataSource({ resource: 'pods' }),
valueType: 'checkbox-select',
},
{
Expand Down
29 changes: 22 additions & 7 deletions web/src/components/logs-query-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ interface LogsQueryInputProps {
value: string;
onChange?: (expression: string) => void;
onRun?: () => void;
isDisabled?: boolean;
invalidQueryErrorMessage?: string | null;
}

export const LogsQueryInput: React.FC<LogsQueryInputProps> = ({ value = '', onChange, onRun }) => {
export const LogsQueryInput: React.FC<LogsQueryInputProps> = ({
value = '',
onChange,
onRun,
isDisabled,
invalidQueryErrorMessage,
}) => {
const { t } = useTranslation('plugin__logging-view-plugin');

const [internalValue, setInternalValue] = React.useState(value);
Expand All @@ -39,16 +47,23 @@ export const LogsQueryInput: React.FC<LogsQueryInputProps> = ({ value = '', onCh
onChange?.(text);
};

const hasError =
!isValid || (invalidQueryErrorMessage !== undefined && invalidQueryErrorMessage !== null);

return (
<div className="co-logs-expression-input" data-test={TestIds.LogsQueryInput}>
<Form className="co-logs-expression-input__form">
<FormGroup
type="string"
helperTextInvalid={`${t(
'Invalid log stream selector. Please select a namespace, pod or container as filter, or add a log stream selector like: ',
)} { log_type =~ ".+" } | json`}
helperTextInvalid={
!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`
: invalidQueryErrorMessage
}
fieldId="selection"
validated={!isValid ? 'error' : undefined}
validated={hasError ? 'error' : undefined}
>
<TextArea
className="co-logs-expression-input__searchInput"
Expand All @@ -57,14 +72,14 @@ export const LogsQueryInput: React.FC<LogsQueryInputProps> = ({ value = '', onCh
onChange={handleOnChange}
onKeyDown={handleKeyDown}
aria-label="LogQL Query"
validated={!isValid ? 'error' : undefined}
validated={hasError ? 'error' : undefined}
/>
</FormGroup>
</Form>
{onRun && (
<ExecuteQueryButton
onClick={onRun}
isDisabled={value === undefined || value.length === 0}
isDisabled={value === undefined || value.length === 0 || isDisabled}
/>
)}
</div>
Expand Down
26 changes: 22 additions & 4 deletions web/src/components/logs-toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Alert,
Select,
SelectOption,
SelectOptionObject,
Expand Down Expand Up @@ -29,6 +30,7 @@ interface LogsToolbarProps {
query: string;
onQueryChange?: (query: string) => void;
onQueryRun?: () => void;
invalidQueryErrorMessage?: string | null;
tenant?: string;
onTenantSelect?: (tenant: string) => void;
enableStreaming?: boolean;
Expand Down Expand Up @@ -59,6 +61,7 @@ export const LogsToolbar: React.FC<LogsToolbarProps> = ({
query,
onQueryChange,
onQueryRun,
invalidQueryErrorMessage,
tenant = 'application',
onTenantSelect,
onStreamingToggle,
Expand Down Expand Up @@ -177,9 +180,16 @@ export const LogsToolbar: React.FC<LogsToolbarProps> = ({
</ToolbarGroup>

{!isQueryShown && (
<ToolbarGroup>
<ExecuteQueryButton onClick={onQueryRun} isDisabled={isDisabled} />
</ToolbarGroup>
<>
<ToolbarGroup>
<ExecuteQueryButton onClick={onQueryRun} isDisabled={isDisabled} />
</ToolbarGroup>
{invalidQueryErrorMessage && (
<ToolbarGroup>
<Alert variant="danger" isInline isPlain title={invalidQueryErrorMessage} />
</ToolbarGroup>
)}
</>
)}

<Spacer />
Expand All @@ -201,7 +211,15 @@ export const LogsToolbar: React.FC<LogsToolbarProps> = ({
)}
</ToolbarContent>

{isQueryShown && <LogsQueryInput value={query} onRun={onQueryRun} onChange={onQueryChange} />}
{isQueryShown && (
<LogsQueryInput
value={query}
onRun={onQueryRun}
onChange={onQueryChange}
invalidQueryErrorMessage={invalidQueryErrorMessage}
isDisabled={isDisabled}
/>
)}
</Toolbar>
);
};
19 changes: 19 additions & 0 deletions web/src/hooks/useLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ export const useLogs = (
const currentTenant = React.useRef<string>(initialTenant);
const currentTimeRange = React.useRef<TimeRange>(initialTimeRange);
const currentTime = React.useRef<number>(Date.now());
const lastExecutionTime = React.useRef<{ logs?: number; histogram?: number }>({
logs: undefined,
histogram: undefined,
});
const currentDirection = React.useRef<Direction>('backward');
const logsAbort = React.useRef<() => void | undefined>();
const histogramAbort = React.useRef<() => void | undefined>();
Expand Down Expand Up @@ -353,10 +357,16 @@ export const useLogs = (
return;
}

// Throttle requests
if (lastExecutionTime.current.logs && Date.now() - lastExecutionTime.current.logs < 50) {
return;
}

try {
currentQuery.current = query;
currentTenant.current = tenant ?? currentTenant.current;
currentTime.current = Date.now();
lastExecutionTime.current.logs = Date.now();
currentTimeRange.current = timeRange ?? currentTimeRange.current;
currentDirection.current = direction ?? currentDirection.current;

Expand Down Expand Up @@ -485,10 +495,19 @@ export const useLogs = (
return;
}

// Throttle histogram requests
if (
lastExecutionTime.current.histogram &&
Date.now() - lastExecutionTime.current.histogram < 50
) {
return;
}

try {
currentQuery.current = query;
currentTenant.current = tenant ?? currentTenant.current;
currentTime.current = Date.now();
lastExecutionTime.current.histogram = Date.now();
currentTimeRange.current = timeRange ?? currentTimeRange.current;

// TODO split on multiple/parallel queries for long timespans and concat results
Expand Down
Loading