diff --git a/.changeset/red-icons-flow.md b/.changeset/red-icons-flow.md new file mode 100644 index 000000000000..baa20619a011 --- /dev/null +++ b/.changeset/red-icons-flow.md @@ -0,0 +1,27 @@ +--- +"wrangler": patch +--- + +feat: add an experimental `insights` command to `wrangler d1` + +This PR adds a `wrangler d1 insights ` command, to let D1 users figure out which of their queries to D1 need to be optimised. + +This command defaults to fetching the top 5 queries that took the longest to run in total over the last 24 hours. + +You can also fetch the top 5 queries that consumed the most rows read over the last week, for example: + +```bash +npx wrangler d1 insights northwind --sortBy reads --timePeriod 7d +``` + +Or the top 5 queries that consumed the most rows written over the last month, for example: + +```bash +npx wrangler d1 insights northwind --sortBy writes --timePeriod 31d +``` + +Or the top 5 most frequently run queries in the last 24 hours, for example: + +```bash +npx wrangler d1 insights northwind --sortBy count +``` diff --git a/packages/wrangler/src/__tests__/d1/d1.test.ts b/packages/wrangler/src/__tests__/d1/d1.test.ts index 14396c12fe3a..a689d08411ec 100644 --- a/packages/wrangler/src/__tests__/d1/d1.test.ts +++ b/packages/wrangler/src/__tests__/d1/d1.test.ts @@ -19,6 +19,7 @@ describe("d1", () => { Commands: wrangler d1 list List D1 databases wrangler d1 info Get information about a D1 database, including the current database size and state. + wrangler d1 insights Experimental command. Get information about the queries run on a D1 database. wrangler d1 create Create D1 database wrangler d1 delete Delete D1 database wrangler d1 backup Interact with D1 Backups @@ -59,6 +60,7 @@ describe("d1", () => { Commands: wrangler d1 list List D1 databases wrangler d1 info Get information about a D1 database, including the current database size and state. + wrangler d1 insights Experimental command. Get information about the queries run on a D1 database. wrangler d1 create Create D1 database wrangler d1 delete Delete D1 database wrangler d1 backup Interact with D1 Backups diff --git a/packages/wrangler/src/d1/index.ts b/packages/wrangler/src/d1/index.ts index 9cc9cc7af110..c1b32059b8d1 100644 --- a/packages/wrangler/src/d1/index.ts +++ b/packages/wrangler/src/d1/index.ts @@ -3,6 +3,7 @@ import * as Create from "./create"; import * as Delete from "./delete"; import * as Execute from "./execute"; import * as Info from "./info"; +import * as Insights from "./insights"; import * as List from "./list"; import * as Migrations from "./migrations"; import * as TimeTravel from "./timeTravel"; @@ -19,6 +20,12 @@ export function d1(yargs: CommonYargsArgv) { Info.Options, Info.Handler ) + .command( + "insights ", + "Experimental command. Get information about the queries run on a D1 database.", + Insights.Options, + Insights.Handler + ) .command( "create ", "Create D1 database", diff --git a/packages/wrangler/src/d1/info.tsx b/packages/wrangler/src/d1/info.tsx index 2f79902f6410..f5dce3f74b23 100644 --- a/packages/wrangler/src/d1/info.tsx +++ b/packages/wrangler/src/d1/info.tsx @@ -49,7 +49,7 @@ export const Handler = withConfig( output["database_size"] = output["file_size"]; delete output["file_size"]; } - if (result.version === "beta") { + if (result.version !== "alpha") { const today = new Date(); const yesterday = new Date(new Date(today).setDate(today.getDate() - 1)); diff --git a/packages/wrangler/src/d1/insights.ts b/packages/wrangler/src/d1/insights.ts new file mode 100644 index 000000000000..9692739bd803 --- /dev/null +++ b/packages/wrangler/src/d1/insights.ts @@ -0,0 +1,170 @@ +import { printWranglerBanner } from ".."; +import { fetchGraphqlResult } from "../cfetch"; +import { withConfig } from "../config"; +import { logger } from "../logger"; +import { requireAuth } from "../user"; +import { + d1BetaWarning, + getDatabaseByNameOrBinding, + getDatabaseInfoFromId, +} from "./utils"; +import type { + CommonYargsArgv, + StrictYargsOptionsToInterface, +} from "../yargs-types"; +import type { D1QueriesGraphQLResponse, Database } from "./types"; + +export function Options(d1ListYargs: CommonYargsArgv) { + return d1ListYargs + .positional("name", { + describe: "The name of the DB", + type: "string", + demandOption: true, + }) + .option("timePeriod", { + choices: ["1d", "7d", "31d"] as const, + describe: "Fetch data from now to the provided time period", + default: "1d" as const, + }) + .option("sort-type", { + choices: ["sum", "avg"] as const, + describe: "Choose the operation you want to sort insights by", + default: "sum" as const, + }) + .option("sort-by", { + choices: ["time", "reads", "writes", "count"] as const, + describe: "Choose the field you want to sort insights by", + default: "time" as const, + }) + .option("sort-direction", { + choices: ["ASC", "DESC"] as const, + describe: "Choose a sort direction", + default: "DESC" as const, + }) + .option("count", { + describe: "fetch insights about the first X queries", + type: "number", + default: 5, + }) + .option("json", { + describe: "return output as clean JSON", + type: "boolean", + default: false, + }) + .epilogue(d1BetaWarning); +} + +const cliOptionToGraphQLOption = { + time: "queryDurationMs", + reads: "rowsRead", + writes: "rowsWritten", + count: "count", +}; + +type HandlerOptions = StrictYargsOptionsToInterface; +export const Handler = withConfig( + async ({ + name, + config, + json, + count, + timePeriod, + sortType, + sortBy, + sortDirection, + }): Promise => { + const accountId = await requireAuth(config); + const db: Database = await getDatabaseByNameOrBinding( + config, + accountId, + name + ); + + const result = await getDatabaseInfoFromId(accountId, db.uuid); + + const output: Record[] = []; + + if (result.version !== "alpha") { + const convertedTimePeriod = Number(timePeriod.replace("d", "")); + const endDate = new Date(); + const startDate = new Date( + new Date(endDate).setDate(endDate.getDate() - convertedTimePeriod) + ); + const parsedSortBy = cliOptionToGraphQLOption[sortBy]; + const orderByClause = + parsedSortBy === "count" + ? `${parsedSortBy}_${sortDirection}` + : `${sortType}_${parsedSortBy}_${sortDirection}`; + const graphqlQueriesResult = + await fetchGraphqlResult({ + method: "POST", + body: JSON.stringify({ + query: `query getD1QueriesOverviewQuery($accountTag: string, $filter: ZoneWorkersRequestsFilter_InputObject) { + viewer { + accounts(filter: {accountTag: $accountTag}) { + d1QueriesAdaptiveGroups(limit: ${count}, filter: $filter, orderBy: [${orderByClause}]) { + sum { + queryDurationMs + rowsRead + rowsWritten + } + avg { + queryDurationMs + rowsRead + rowsWritten + } + count + dimensions { + query + } + } + } + } + }`, + operationName: "getD1QueriesOverviewQuery", + variables: { + accountTag: accountId, + filter: { + AND: [ + { + datetimeHour_geq: startDate.toISOString(), + datetimeHour_leq: endDate.toISOString(), + databaseId: db.uuid, + }, + ], + }, + }, + }), + headers: { + "Content-Type": "application/json", + }, + }); + + graphqlQueriesResult?.data?.viewer?.accounts[0]?.d1QueriesAdaptiveGroups?.forEach( + (row) => { + if (!row.dimensions.query) return; + output.push({ + query: row.dimensions.query, + avgRowsRead: row?.avg?.rowsRead ?? 0, + totalRowsRead: row?.sum?.rowsRead ?? 0, + avgRowsWritten: row?.avg?.rowsWritten ?? 0, + totalRowsWritten: row?.sum?.rowsWritten ?? 0, + avgDurationMs: row?.avg?.queryDurationMs ?? 0, + totalDurationMs: row?.sum?.queryDurationMs ?? 0, + numberOfTimesRun: row?.count ?? 0, + }); + } + ); + } + + if (json) { + logger.log(JSON.stringify(output, null, 2)); + } else { + await printWranglerBanner(); + logger.log( + "-------------------\n🚧 `wrangler d1 insights` is an experimental command.\n🚧 Flags for this command, their descriptions, and output may change between wrangler versions.\n-------------------\n" + ); + logger.log(JSON.stringify(output, null, 2)); + } + } +); diff --git a/packages/wrangler/src/d1/types.ts b/packages/wrangler/src/d1/types.ts index 3c40045f3a45..7d76d3c2a4fa 100644 --- a/packages/wrangler/src/d1/types.ts +++ b/packages/wrangler/src/d1/types.ts @@ -72,3 +72,35 @@ export interface D1MetricsGraphQLResponse { }; }; } + +export interface D1Queries { + avg?: { + queryDurationMs?: number; + rowsRead?: number; + rowsWritten?: number; + }; + sum?: { + queryDurationMs?: number; + rowsRead?: number; + rowsWritten?: number; + }; + count?: number; + dimensions: { + query?: string; + databaseId?: string; + date?: string; + datetime?: string; + datetimeMinute?: string; + datetimeFiveMinutes?: string; + datetimeFifteenMinutes?: string; + datetimeHour?: string; + }; +} + +export interface D1QueriesGraphQLResponse { + data: { + viewer: { + accounts: { d1QueriesAdaptiveGroups?: D1Queries[] }[]; + }; + }; +}