From 5828597d1bee59d5803627891c2edaf57aa946fb Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Wed, 9 Nov 2022 13:30:32 -0500 Subject: [PATCH] ui: move events endpoint to use sql-over-http Part of: #89429 Addresses: #90272 (blocked from resolving by #80789) This change migrates the existing `/events` request to use the sql-over-http endpoint on apiV2, making this request tenant-scoped once the sql-over-http endpoint is scoped to tenants (this should be the case when #91323 is completed). Release note: None --- .../cluster-ui/src/api/eventsApi.ts | 99 +++++++++++++++++++ pkg/ui/workspaces/cluster-ui/src/api/index.ts | 1 + .../db-console/src/redux/apiReducers.ts | 4 +- .../workspaces/db-console/src/redux/events.ts | 2 +- pkg/ui/workspaces/db-console/src/util/api.ts | 19 ---- .../workspaces/db-console/src/util/events.ts | 22 ++--- .../views/cluster/containers/events/index.tsx | 26 ++--- 7 files changed, 122 insertions(+), 51 deletions(-) create mode 100644 pkg/ui/workspaces/cluster-ui/src/api/eventsApi.ts diff --git a/pkg/ui/workspaces/cluster-ui/src/api/eventsApi.ts b/pkg/ui/workspaces/cluster-ui/src/api/eventsApi.ts new file mode 100644 index 000000000000..85b31e615363 --- /dev/null +++ b/pkg/ui/workspaces/cluster-ui/src/api/eventsApi.ts @@ -0,0 +1,99 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// 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, included in the file +// licenses/APL.txt. + +import { + executeInternalSql, + SqlExecutionRequest, + sqlResultsAreEmpty, + SqlStatement, +} from "./sqlApi"; +import { withTimeout } from "./util"; +import moment from "moment"; + +export const defaultAPIEventLimit = 1000; + +export type EventColumns = { + timestamp: string; + eventType: string; + reportingID: string; + info: string; + uniqueID: string; +}; + +type NonRedactedEventsRequest = { + type?: string; + limit?: number; + offset?: number; +}; + +export type EventsResponse = EventColumns[]; + +export const baseEventsQuery = `SELECT timestamp, \"eventType\", \"reportingID\", info, \"uniqueID\" FROM system.eventlog`; + +function buildEventStatement({ + type, + limit, + offset, +}: NonRedactedEventsRequest): SqlStatement { + let placeholder = 1; + const eventsStmt: SqlStatement = { + sql: baseEventsQuery, + arguments: [], + }; + if (type) { + eventsStmt.sql += ` WHERE "eventType" = ` + type; + eventsStmt.arguments.push(type); + } + eventsStmt.sql += ` ORDER BY timestamp DESC`; + if (!limit || limit === 0) { + limit = defaultAPIEventLimit; + } + if (limit > 0) { + eventsStmt.sql += ` LIMIT $${placeholder}`; + eventsStmt.arguments.push(limit); + placeholder++; + } + if (offset && offset > 0) { + eventsStmt.sql += ` OFFSET $${placeholder}`; + eventsStmt.arguments.push(offset); + } + eventsStmt.sql += ";"; + return eventsStmt; +} + +// getEvents fetches events logs from the database. Callers of +// getEvents from cluster-ui will need to pass a timeout argument for +// promise timeout handling (callers from db-console already have promise +// timeout handling as part of the cacheDataReducer). +// Note that this endpoint is not able to redact event log information. +export function getNonRedactedEvents( + req: NonRedactedEventsRequest = {}, + timeout?: moment.Duration, +): Promise { + const eventsStmt: SqlStatement = buildEventStatement(req); + const eventsRequest: SqlExecutionRequest = { + statements: [eventsStmt], + execute: true, + }; + return withTimeout( + executeInternalSql(eventsRequest), + timeout, + ).then(result => { + // If request succeeded but query failed, throw error (caught by saga/cacheDataReducer). + if (result.error) { + throw result.error; + } + + if (sqlResultsAreEmpty(result)) { + return []; + } + return result.execution.txn_results[0].rows; + }); +} diff --git a/pkg/ui/workspaces/cluster-ui/src/api/index.ts b/pkg/ui/workspaces/cluster-ui/src/api/index.ts index 45d73b4d236f..fcfb8d923da5 100644 --- a/pkg/ui/workspaces/cluster-ui/src/api/index.ts +++ b/pkg/ui/workspaces/cluster-ui/src/api/index.ts @@ -21,3 +21,4 @@ export * from "./schedulesApi"; export * from "./sqlApi"; export * from "./tracezApi"; export * from "./databasesApi"; +export * from "./eventsApi"; diff --git a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts index 5682b39dcd64..bbc08ad5754f 100644 --- a/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts +++ b/pkg/ui/workspaces/db-console/src/redux/apiReducers.ts @@ -43,7 +43,7 @@ export const clusterReducerObj = new CachedDataReducer( export const refreshCluster = clusterReducerObj.refresh; const eventsReducerObj = new CachedDataReducer( - api.getEvents, + clusterUiApi.getNonRedactedEvents, "events", moment.duration(10, "s"), ); @@ -486,7 +486,7 @@ export const refreshSnapshot = snapshotReducerObj.refresh; export interface APIReducersState { cluster: CachedDataReducerState; - events: CachedDataReducerState; + events: CachedDataReducerState; health: HealthState; nodes: CachedDataReducerState; raft: CachedDataReducerState; diff --git a/pkg/ui/workspaces/db-console/src/redux/events.ts b/pkg/ui/workspaces/db-console/src/redux/events.ts index 67161f2291c1..073e6ef4e613 100644 --- a/pkg/ui/workspaces/db-console/src/redux/events.ts +++ b/pkg/ui/workspaces/db-console/src/redux/events.ts @@ -14,7 +14,7 @@ import { AdminUIState } from "src/redux/state"; * eventsSelector selects the list of events from the store. */ export function eventsSelector(state: AdminUIState) { - return state.cachedData.events.data && state.cachedData.events.data.events; + return state.cachedData.events.data; } /** diff --git a/pkg/ui/workspaces/db-console/src/util/api.ts b/pkg/ui/workspaces/db-console/src/util/api.ts index a2e5c9b8421b..0efd874160ae 100644 --- a/pkg/ui/workspaces/db-console/src/util/api.ts +++ b/pkg/ui/workspaces/db-console/src/util/api.ts @@ -31,11 +31,6 @@ export type TableDetailsRequestMessage = export type TableDetailsResponseMessage = protos.cockroach.server.serverpb.TableDetailsResponse; -export type EventsRequestMessage = - protos.cockroach.server.serverpb.EventsRequest; -export type EventsResponseMessage = - protos.cockroach.server.serverpb.EventsResponse; - export type LocationsRequestMessage = protos.cockroach.server.serverpb.LocationsRequest; export type LocationsResponseMessage = @@ -449,20 +444,6 @@ export function setUIData( ); } -// getEvents gets event data -export function getEvents( - req: EventsRequestMessage, - timeout?: moment.Duration, -): Promise { - const queryString = propsToQueryString(_.pick(req, ["type"])); - return timeoutFetch( - serverpb.EventsResponse, - `${API_PREFIX}/events?unredacted_events=true&${queryString}`, - null, - timeout, - ); -} - export function getLocations( _req: LocationsRequestMessage, timeout?: moment.Duration, diff --git a/pkg/ui/workspaces/db-console/src/util/events.ts b/pkg/ui/workspaces/db-console/src/util/events.ts index 30eff7b07f84..b42967745d7b 100644 --- a/pkg/ui/workspaces/db-console/src/util/events.ts +++ b/pkg/ui/workspaces/db-console/src/util/events.ts @@ -8,24 +8,18 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. -import * as protobuf from "protobufjs/minimal"; - -import * as protos from "src/js/protos"; import * as eventTypes from "src/util/eventTypes"; - -type Event$Properties = protos.cockroach.server.serverpb.EventsResponse.IEvent; +import { api as clusterUiApi } from "@cockroachlabs/cluster-ui"; /** * getEventDescription returns a short summary of an event. */ -export function getEventDescription(e: Event$Properties): string { - const info: EventInfo = protobuf.util.isset(e, "info") - ? JSON.parse(e.info) - : {}; +export function getEventDescription(e: clusterUiApi.EventColumns): string { + const info: EventInfo = e.info ? JSON.parse(e.info) : {}; let privs = ""; let comment = ""; - switch (e.event_type) { + switch (e.eventType) { case eventTypes.CREATE_DATABASE: return `Database Created: User ${info.User} created database ${info.DatabaseName}`; case eventTypes.DROP_DATABASE: { @@ -203,14 +197,10 @@ export function getEventDescription(e: Event$Properties): string { eventTypes.UNSAFE_UPSERT_DESCRIPTOR, eventTypes.UNSAFE_DELETE_DESCRIPTOR): return `Unsafe: User ${info.User} executed crdb_internal.${ - e.event_type + e.eventType }, Info: ${JSON.stringify(info, null, 2)}`; default: - return `Event: ${e.event_type}, content: ${JSON.stringify( - info, - null, - 2, - )}`; + return `Event: ${e.eventType}, content: ${JSON.stringify(info, null, 2)}`; } } diff --git a/pkg/ui/workspaces/db-console/src/views/cluster/containers/events/index.tsx b/pkg/ui/workspaces/db-console/src/views/cluster/containers/events/index.tsx index 210147c96448..a49df4cc7fab 100644 --- a/pkg/ui/workspaces/db-console/src/views/cluster/containers/events/index.tsx +++ b/pkg/ui/workspaces/db-console/src/views/cluster/containers/events/index.tsx @@ -14,7 +14,6 @@ import React from "react"; import { Helmet } from "react-helmet"; import { Link, RouteComponentProps, withRouter } from "react-router-dom"; import { connect } from "react-redux"; -import * as protos from "src/js/protos"; import { refreshEvents } from "src/redux/apiReducers"; import { eventsLastErrorSelector, @@ -23,15 +22,17 @@ import { } from "src/redux/events"; import { LocalSetting } from "src/redux/localsettings"; import { AdminUIState } from "src/redux/state"; -import { util } from "@cockroachlabs/cluster-ui"; import { getEventDescription } from "src/util/events"; import { DATE_FORMAT_24_UTC } from "src/util/format"; import { ToolTipWrapper } from "src/views/shared/components/toolTip"; -import { Loading, SortSetting, SortedTable } from "@cockroachlabs/cluster-ui"; +import { + Loading, + SortSetting, + SortedTable, + api as clusterUiApi, +} from "@cockroachlabs/cluster-ui"; import "./events.styl"; -type Event$Properties = protos.cockroach.server.serverpb.EventsResponse.IEvent; - // Number of events to show in the sidebar. const EVENT_BOX_NUM_EVENTS = 5; @@ -50,18 +51,17 @@ export interface SimplifiedEvent { class EventSortedTable extends SortedTable {} export interface EventRowProps { - event: Event$Properties; + event: clusterUiApi.EventColumns; } -export function getEventInfo(e: Event$Properties): SimplifiedEvent { +export function getEventInfo(e: clusterUiApi.EventColumns): SimplifiedEvent { return { - fromNowString: util - .TimestampToMoment(e.timestamp) + fromNowString: moment(e.timestamp) .format(DATE_FORMAT_24_UTC) .replace("second", "sec") .replace("minute", "min"), content: {getEventDescription(e)}, - sortableTimestamp: util.TimestampToMoment(e.timestamp), + sortableTimestamp: moment(e.timestamp), }; } @@ -83,7 +83,7 @@ export class EventRow extends React.Component { } export interface EventBoxProps { - events: Event$Properties[]; + events: clusterUiApi.EventsResponse; // eventsValid is needed so that this component will re-render when the events // data becomes invalid, and thus trigger a refresh. eventsValid: boolean; @@ -109,7 +109,7 @@ export class EventBoxUnconnected extends React.Component { {_.map( _.take(events, EVENT_BOX_NUM_EVENTS), - (e: Event$Properties, i: number) => { + (e: clusterUiApi.EventColumns, i: number) => { return ; }, )} @@ -126,7 +126,7 @@ export class EventBoxUnconnected extends React.Component { } export interface EventPageProps { - events: Event$Properties[]; + events: clusterUiApi.EventsResponse; // eventsValid is needed so that this component will re-render when the events // data becomes invalid, and thus trigger a refresh. eventsValid: boolean;