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
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`queries > buildStatementLoggingMaxSampleRateQuery produces the expected query 1`] = `
{
"parameters": [],
"sql": "SHOW statement_logging_max_sample_rate",
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

import { queryBuilder } from "~/api/materialize";

import { buildStatementLoggingMaxSampleRateQuery } from "./statementLoggingMaxSampleRate";

describe("queries", () => {
it("buildStatementLoggingMaxSampleRateQuery produces the expected query", () => {
const { sql, parameters } =
buildStatementLoggingMaxSampleRateQuery().compile(queryBuilder);
expect({ sql, parameters }).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright Materialize, Inc. and contributors. All rights reserved.
//
// Use of this software is governed by the Business Source License
// included in the LICENSE file.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0.

import { QueryKey } from "@tanstack/react-query";
import { sql } from "kysely";

import { executeSqlV2, queryBuilder } from "~/api/materialize";

export const buildStatementLoggingMaxSampleRateQuery = () => {
return sql<{
statement_logging_max_sample_rate: string;
}>`SHOW statement_logging_max_sample_rate`;
};

/**
* Fetches the system-wide cap on the statement logging sample rate. The effective rate is
* `min(session statement_logging_sample_rate, this)`, so a value of `0` means statement
* logging is off for everyone and query history can never have rows.
*
* Returns `null` if the variable could not be read as a number.
*/
export default async function fetchStatementLoggingMaxSampleRate({
queryKey,
requestOptions,
}: {
queryKey: QueryKey;
requestOptions: RequestInit;
}) {
const compiledQuery =
buildStatementLoggingMaxSampleRateQuery().compile(queryBuilder);

const response = await executeSqlV2({
queries: compiledQuery,
queryKey,
requestOptions,
});

const rate = parseFloat(response.rows[0]?.statement_logging_max_sample_rate);

return Number.isFinite(rate) ? rate : null;
}
60 changes: 56 additions & 4 deletions console/src/platform/query-history/QueryHistoryList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,15 @@ const useFetchQueryHistoryUsersColumns: Array<Column> = [
buildColumn({ name: "email" }),
];

// The schema defaults `dateRange` to a window ending at `new Date()`, so handlers must
// reuse the same parsed filters the component is rendered with in order to match.
const PARSED_DEFAULT_SCHEMA_VALUES = queryHistoryListSchema.parse(
DEFAULT_SCHEMA_VALUES,
);

const DEFAULT_FETCH_QUERY_LIST_HANDLER = buildSqlQueryHandlerV2({
queryKey: queryHistoryQueryKeys.list({
filters: queryHistoryListSchema.parse(DEFAULT_SCHEMA_VALUES),
filters: PARSED_DEFAULT_SCHEMA_VALUES,
isRedacted: false,
isV0_132_0: false,
}),
Expand All @@ -94,9 +100,13 @@ const DEFAULT_FETCH_QUERY_HISTORY_USERS_HANDLER = buildSqlQueryHandlerV2({
}),
});

const PARSED_DEFAULT_SCHEMA_VALUES = queryHistoryListSchema.parse(
DEFAULT_SCHEMA_VALUES,
);
const buildMaxSampleRateHandler = (rate: string) =>
buildSqlQueryHandlerV2({
queryKey: queryHistoryQueryKeys.statementLoggingMaxSampleRate(),
results: mapKyselyToTabular({
rows: [{ statement_logging_max_sample_rate: rate }],
}),
});

const ALL_COLUMNS = COLUMNS.map(({ key }) => key);

Expand Down Expand Up @@ -127,6 +137,7 @@ describe("QueryHistoryList", () => {
server.use(DEFAULT_FETCH_QUERY_LIST_HANDLER);
server.use(DEFAULT_FETCH_CLUSTER_LIST_HANDLER);
server.use(DEFAULT_FETCH_QUERY_HISTORY_USERS_HANDLER);
server.use(buildMaxSampleRateHandler("0.99"));
});

afterEach(() => {
Expand Down Expand Up @@ -387,6 +398,47 @@ describe("QueryHistoryList", () => {
expect(await screen.findByText("No results found.")).toBeVisible();
});

it("Should show the filter-oriented empty state when sampling is enabled", async () => {
await renderComponent(
<QueryHistoryList
initialFilters={PARSED_DEFAULT_SCHEMA_VALUES}
initialColumns={DEFAULT_COLUMNS}
/>,
{
initializeState: ({ set }) =>
setFakeEnvironment(set, "aws/us-east-1", healthyEnvironment),
},
);

expect(await screen.findByText("No results found.")).toBeVisible();
expect(
screen.queryByText("Statement logging is turned off."),
).not.toBeInTheDocument();
});

it("Should show a sampling disabled empty state when the max sample rate is zero", async () => {
server.use(buildMaxSampleRateHandler("0"));

await renderComponent(
<QueryHistoryList
initialFilters={PARSED_DEFAULT_SCHEMA_VALUES}
initialColumns={DEFAULT_COLUMNS}
/>,
{
initializeState: ({ set }) =>
setFakeEnvironment(set, "aws/us-east-1", healthyEnvironment),
},
);

expect(
await screen.findByText("Statement logging is turned off."),
).toBeVisible();
expect(
screen.getByText(/ALTER SYSTEM SET statement_logging_max_sample_rate/),
).toBeVisible();
expect(screen.queryByText("No results found.")).not.toBeInTheDocument();
});

// TODO (robinclowers): Fix and renenable this https://github.com/MaterializeInc/console/issues/2482
it.skip(
"Should show an error state when we fail to fetch the query history list",
Expand Down
58 changes: 52 additions & 6 deletions console/src/platform/query-history/QueryHistoryList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ import ClusterFilter from "./ClusterFilter";
import ColumnFilter from "./ColumnFilter";
import DateRangeInput from "./DateRangeInput";
import FilterMenu from "./FilterMenu";
import { useFetchQueryHistoryList } from "./queries";
import {
useFetchQueryHistoryList,
useFetchStatementLoggingMaxSampleRate,
} from "./queries";
import QueryHistoryTable from "./QueryHistoryTable";
import {
formatSelectedDates,
Expand Down Expand Up @@ -104,6 +107,34 @@ const EmptyState = () => {
);
};

const SamplingDisabledState = () => {
const { colors } = useTheme<MaterializeTheme>();

return (
<EmptyListWrapper>
<EmptyListHeader>
<Circle p={2} bg={colors.background.secondary}>
<ActivityIcon color={colors.foreground.secondary} />
</Circle>
<EmptyListHeaderContents
title="Statement logging is turned off."
helpText={
<>
The statement logging sample rate is set to zero, so no queries
are recorded. Ask an administrator to raise it with{" "}
<Text as="span" textStyle="monospace">
ALTER SYSTEM SET statement_logging_max_sample_rate
</Text>
. In self-managed deployments it can also be set through the
Materialize operator&apos;s Helm chart values.
</>
}
/>
</EmptyListHeader>
</EmptyListWrapper>
);
};

export const UnauthorizedState = () => {
const { colors } = useTheme<MaterializeTheme>();
return (
Expand Down Expand Up @@ -189,12 +220,23 @@ export const QueryHistoryList = ({
},
);

const isEmpty = queryHistoryListData?.rows.length === 0;

// Only needed to disambiguate an empty result set, so keep it off the path
// that renders rows.
const { data: maxSampleRate, isLoading: isMaxSampleRateLoading } =
useFetchStatementLoggingMaxSampleRate({ enabled: isEmpty });

useSyncObjectToSearchParams(urlParamObject);

const isError = isPrivilegesError || isQueryHistoryListError;

const isLoading = isPrivilegesLoading || isQueryHistoryListLoading;
const isEmpty = queryHistoryListData?.rows.length === 0;
const isLoading =
isPrivilegesLoading || isQueryHistoryListLoading || isMaxSampleRateLoading;
const isSamplingDisabled = isEmpty && maxSampleRate === 0;
// Sampling being off, not an over-narrow filter, is what the user has to fix,
// so don't draw attention to the filter controls.
const isFilterEmpty = isEmpty && !isSamplingDisabled;

const isUnauthorized = isPrivilegesSuccess && !isAuthorized;

Expand All @@ -215,12 +257,14 @@ export const QueryHistoryList = ({
<HStack>
<UserFilter
submitForm={submitForm}
variant={isEmpty ? "focused" : "default"}
variant={isFilterEmpty ? "focused" : "default"}
/>
<ClusterFilter submitForm={submitForm} />
<DateRangeInput
onSubmit={onSubmit}
toggleButtonProps={isEmpty ? { variant: "focused" } : undefined}
toggleButtonProps={
isFilterEmpty ? { variant: "focused" } : undefined
}
/>
<FilterMenu onSubmit={onSubmit} />
</HStack>
Expand All @@ -246,7 +290,9 @@ export const QueryHistoryList = ({
>
<Spinner data-testid="loading-spinner" />
</Stack>
) : isEmpty ? (
) : isSamplingDisabled ? (
<SamplingDisabledState />
) : isFilterEmpty ? (
<EmptyState />
) : (
<QueryHistoryTable
Expand Down
21 changes: 21 additions & 0 deletions console/src/platform/query-history/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
QueryHistoryListRow,
} from "~/api/materialize/query-history/queryHistoryList";
import { fetchQueryHistoryUsers } from "~/api/materialize/query-history/queryHistoryUsers";
import fetchStatementLoggingMaxSampleRate from "~/api/materialize/query-history/statementLoggingMaxSampleRate";
import {
getRecentQueryData,
initialPlaceholderDataBuilders,
Expand Down Expand Up @@ -63,6 +64,11 @@ export const queryHistoryQueryKeys = {
[...queryHistoryQueryKeys.all(), buildQueryKeyPart("clusters")] as const,
users: () =>
[...queryHistoryQueryKeys.all(), buildQueryKeyPart("users")] as const,
statementLoggingMaxSampleRate: () =>
[
...queryHistoryQueryKeys.all(),
buildQueryKeyPart("statement-logging-max-sample-rate"),
] as const,
detail: ({ executionId }: { executionId: string }) =>
[
...queryHistoryQueryKeys.all(),
Expand Down Expand Up @@ -133,6 +139,21 @@ export function useFetchQueryHistoryUsers() {
});
}

export function useFetchStatementLoggingMaxSampleRate(options?: {
enabled?: boolean;
}) {
return useQuery({
queryKey: queryHistoryQueryKeys.statementLoggingMaxSampleRate(),
queryFn: ({ queryKey, signal }) => {
return fetchStatementLoggingMaxSampleRate({
queryKey,
requestOptions: { signal },
});
},
enabled: options?.enabled,
});
}

export function useFetchQueryHistoryStatementInfo(
parameters: QueryHistoryStatementInfoParameters,
options?: { enabled?: boolean },
Expand Down
Loading