diff --git a/sdk/monitor/monitor-query/CHANGELOG.md b/sdk/monitor/monitor-query/CHANGELOG.md index 9129e46633d6..3e4160a1a030 100644 --- a/sdk/monitor/monitor-query/CHANGELOG.md +++ b/sdk/monitor/monitor-query/CHANGELOG.md @@ -1,14 +1,29 @@ # Release History -## 1.0.0-beta.5 (Unreleased) +## 1.0.0-beta.5 (2021-09-09) ### Features Added -### Breaking Changes +- Added convenience method `getMetricByName` in `MetricsQueryResult` to allow users to fetch results of a particular metric +- Added a flag `throwOnAnyFailure` to be set by user for methods `queryLogs` and `queryLogsBatch` to enable error throwing +- Defining `AggregateBatchError` class for throwing batch errors +- Added status with potential values `("Partial" | "Success" | "Failed")` for responses of `query` and `queryBatch` APIs to indicate whether the status of results of each query. -### Bugs Fixed +### Breaking Changes -### Other Changes +- Updated methods `getMetricDefinitions` and `getMetricNamespaces` to return paginated list of items and renamed to `listMetricDefinitions` and `listMetricNames` respectively +- Renamed methods `queryLogs` and `queryLogsBatch` in `LogsQueryClient` to `query` and `queryBatch` +- Updated the constant names inside `Durations` to drop the `last` prefix +- Updated method `queryBatch` to take argument of `QueryBatch` model instead of `QueryLogsBatch` model +- Renamed `QueryLogsOptions` to `LogsQueryOptions` +- Renamed `QueryLogsBatchResult` to `LogsQueryBatchResult` +- Renamed `QueryMetricsResult` to `MetricsQueryResult` +- Renamed `QueryLogsResult` to `LogsQueryResult`, `QueryLogsBatchOptions` to `LogsQueryBatchOptions` +- Updated type for `timespan` property from `string` to `TimeInterval` type in `query` and `queryBatch` methods of `LogsQueryClient` and in `MetricsQueryResult` interface +- Renamed `MetricColumn` to `LogsColumn` and `column` to `columnDescriptors` in `LogsTable` model +- Renamed `displayDescription` property to `description` in `Metric` and `MetricNames` +- Flattened the `metricNamespaceName` property for `MetricNamespace` +- Renamed `innererror` to `innerError` in `ErrorInfo` ## 1.0.0-beta.4 (2021-08-10) diff --git a/sdk/monitor/monitor-query/README.md b/sdk/monitor/monitor-query/README.md index c2f107389e4c..98d6760fc887 100644 --- a/sdk/monitor/monitor-query/README.md +++ b/sdk/monitor/monitor-query/README.md @@ -127,16 +127,6 @@ Each set of metric values is a time series with the following characteristics: The [`MetricsQueryClient`][msdocs_metrics_client] allows you to query metrics. -#### Resource URI of a resource - -The resource URI must be that of the resource for which metrics are being queried. It's normally of the format `/subscriptions//resourceGroups//providers//topics/`. - -To find the resource URI: - -1. Navigate to your resource's page in the Azure portal. -2. From the **Overview** blade, select the **JSON View** link. -3. In the resulting JSON, copy the value of the `id` property. - ## Examples ### Querying logs @@ -147,7 +137,6 @@ You can use the `Durations` constants provided for some commonly used ISO8601 du ```ts const { LogsQueryClient, Durations } = require("@azure/monitor-query"); const { DefaultAzureCredential } = require("@azure/identity"); -g; const azureLogAnalyticsWorkspaceId = ""; const logsQueryClient = new LogsQueryClient(new DefaultAzureCredential()); @@ -167,7 +156,7 @@ async function run() { console.log(`Results for query '${kustoQuery}'`); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); @@ -190,12 +179,13 @@ Here is a hierarchy of the response: ``` LogsQueryResult |---statistics -|---visalization +|---visualization |---error +|---status ("Partial" | "Success" | "Failed") |---tables (list of `LogsTable` objects) |---name |---rows - |---columns (list of `LogsColumn` objects) + |---columnDescriptors (list of `LogsColumn` objects) |---name |---type ``` @@ -205,7 +195,7 @@ So, to handle a response with tables, ```ts const tablesFromResult = result.tables; for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); @@ -250,10 +240,12 @@ export async function main() { const tokenCredential = new DefaultAzureCredential(); const logsQueryClient = new LogsQueryClient(tokenCredential); - const queriesBatch: QueryBatch[] = [ + + const kqlQuery = "AppEvents | project TimeGenerated, Name, AppRoleInstance | limit 1"; + const queriesBatch = [ { workspaceId: monitorWorkspaceId, - query: "AppEvents | project TimeGenerated, Name, AppRoleInstance | limit 1", + query: kqlQuery, timespan: { duration: "P1D" } }, { @@ -276,16 +268,17 @@ export async function main() { ]; const result = await logsQueryClient.queryBatch(queriesBatch); + if (result.results == null) { throw new Error("No response for query"); } let i = 0; for (const response of result.results) { - console.log(`Results for query with id: ${response.id}`); + console.log(`Results for query with query: ${queriesBatch[i]}`); if (response.error) { - console.log(`Query had errors:`, response.error); + console.log(` Query had errors:`, response.error); } else { if (response.tables == null) { console.log(`No results for query`); @@ -295,7 +288,7 @@ export async function main() { ); for (const table of response.tables) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log(columnHeaderString); @@ -322,33 +315,37 @@ Here is a hierarchy of the response: ``` LogsQueryBatchResult |---results (list of following objects) - |---id - |---status |---statistics - |---visalization + |---visualization |---error + |---status ("Partial" | "Success" | "Failed") |---tables (list of `LogsTable` objects) |---name |---rows - |---columns (list of `LogsColumn` objects) + |---columnDescriptors (list of `LogsColumn` objects) |---name |---type ``` -To handle a batch response, +To handle a batch response: ```ts +let i = 0; for (const response of result.results) { - console.log(`Results for query with id: ${response.id}`); + console.log(`Results for query with query: ${queriesBatch[i]}`); if (response.error) { - console.log(`Query had errors:`, response.error); + console.log(` Query had errors:`, response.error); } else { if (response.tables == null) { console.log(`No results for query`); } else { + console.log( + `Printing results from query '${queriesBatch[i].query}' for '${queriesBatch[i].timespan}'` + ); + for (const table of response.tables) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log(columnHeaderString); @@ -360,14 +357,25 @@ for (const response of result.results) { } } } + // next query + i++; } ``` A full sample can be found [here](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryBatch.ts) +For information on request throttling at the Log Analytics service level, see [Rate limits](https://dev.loganalytics.io/documentation/Using-the-API/Limits). + ### Query metrics -The following example gets metrics for an [Azure Metrics Advisor](https://docs.microsoft.com/azure/applied-ai-services/metrics-advisor/overview) subscription. The resource URI is that of a Metrics Advisor resource. +The following example gets metrics for an [Azure Metrics Advisor](https://docs.microsoft.com/azure/applied-ai-services/metrics-advisor/overview) subscription. +The resource URI must be that of the resource for which metrics are being queried. It's normally of the format `/subscriptions//resourceGroups//providers//topics/`. + +To find the resource URI: + +1. Navigate to your resource's page in the Azure portal. +2. From the **Overview** blade, select the **JSON View** link. +3. In the resulting JSON, copy the value of the `id` property. ```ts import { DefaultAzureCredential } from "@azure/identity"; @@ -386,31 +394,39 @@ export async function main() { throw new Error("METRICS_RESOURCE_ID must be set in the environment for this sample"); } - const result = await metricsQueryClient.getMetricDefinitions(metricsResourceId); - - for (const definition of result.definitions) { - console.log(`Definition = ${definition.name}`); - } - - const firstMetric = result.definitions[0]; - - console.log(`Picking an example metric to query: ${firstMetric.name}`); - - const metricsResponse = await metricsQueryClient.queryMetrics( - metricsResourceId, - { duration: Durations.FiveMinutes }, - { - metricNames: [firstMetric.name!], - interval: "PT1M" + const iterator = metricsQueryClient.listMetricDefinitions(metricsResourceId); + let result = await iterator.next(); + let metricNames: string[] = []; + for await (const result of iterator) { + console.log(` metricDefinitions - ${result.id}, ${result.name}`); + if (result.name) { + metricNames.push(result.name); } - ); + } + const firstMetricName = metricNames[0]; + const secondMetricName = metricNames[1]; + if (firstMetricName && secondMetricName) { + console.log(`Picking an example metric to query: ${firstMetricName} and ${secondMetricName}`); + const metricsResponse = await metricsQueryClient.query( + metricsResourceId, + [firstMetricName, secondMetricName], + { + granularity: "PT1M", + timespan: { duration: Durations.FiveMinutes } + } + ); - console.log( - `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.interval}, time span: ${metricsResponse.timespan}` - ); + console.log( + `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.granularity}, time span: ${metricsResponse.timespan}` + ); - const metrics: Metric[] = metricsResponse.metrics; - console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + const metrics: Metric[] = metricsResponse.metrics; + console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + const metric = metricsResponse.getMetricByName(firstMetricName); + console.log(`Selected Metric: ${firstMetricName}`, JSON.stringify(metric, undefined, 2)); + } else { + console.error(`Metric names are not defined - ${firstMetricName} and ${secondMetricName}`); + } } main().catch((err) => { @@ -419,6 +435,8 @@ main().catch((err) => { }); ``` +In the preceding sample, the ordering of results for the metrics in the `metricResponse` will be in the order in which the user specifies the metric names in the `metricNames` array argument for the query method. If user specifies `[firstMetricName,secondMetricName]`, the result for `firstMetricName` will appear before the result for `secondMetricName` in the `metricResponse`. + ### Handle metrics response The metrics query API returns a `QueryMetricsResult` object. The `QueryMetricsResult` object contains properties such as a list of `Metric`-typed objects, `interval`, `namespace`, and `timespan`. The `Metric` objects list can be accessed using the `metrics` property. Each `Metric` object in this list contains a list of `TimeSeriesElement` objects. Each `TimeSeriesElement` contains `data` and `metadataValues` properties. In visual form, the object hierarchy of the response resembles the following structure: @@ -426,8 +444,8 @@ The metrics query API returns a `QueryMetricsResult` object. The `QueryMetricsRe ``` QueryMetricsResult |---cost -|---timespan -|---interval +|---timespan (of type `TimeInterval`) +|---granularity |---namespace |---resourceRegion |---metrics (list of `Metric` objects) @@ -440,6 +458,13 @@ QueryMetricsResult |---timeseries (list of `TimeSeriesElement` objects) |---metadataValues |---data (list of data points represented by `MetricValue` objects) + |---timeStamp + |---average + |---minimum + |---maximum + |---total + |---count +|---getMetricByName(metricName): Metric | undefined (convenience method) ``` #### Example of handling response @@ -459,7 +484,7 @@ export async function main() { throw new Error("METRICS_RESOURCE_ID must be set in the environment for this sample"); } - console.log(`Picking an example metric to query: ${firstMetric.name}`); + console.log(`Picking an example metric to query: ${firstMetricName}`); const metricsResponse = await metricsQueryClient.queryMetrics( metricsResourceId, @@ -515,7 +540,7 @@ const queryLogsOptions: LogsQueryOptions = { additionalWorkspaces: ["", ""] }; -const kustoQuery = "AppEvents | limit 1"; +const kustoQuery = "AppEvents | limit 10"; const result = await logsQueryClient.queryLogs( azureLogAnalyticsWorkspaceId, kustoQuery, @@ -524,6 +549,19 @@ const result = await logsQueryClient.queryLogs( ); ``` +To view the results for each workspace, use the `TenantId` column to either order the results or filter them in the Kusto query. +**Order results by TenantId** + +``` +AppEvents | order by TenantId +``` + +**Filter results by TenantId** + +``` +AppEvents | filter TenantId == "" +``` + A full sample can be found [here](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryMultipleWorkspaces.ts) For more samples see here: [samples][samples]. diff --git a/sdk/monitor/monitor-query/package.json b/sdk/monitor/monitor-query/package.json index 3037bd09c9f7..9c2cbb58e5ef 100644 --- a/sdk/monitor/monitor-query/package.json +++ b/sdk/monitor/monitor-query/package.json @@ -46,7 +46,7 @@ "build:samples": "dev-tool samples publish --force", "build:test": "tsc -p . && rollup -c 2>&1", "build:types": "downlevel-dts types/latest types/3.1", - "build": "npm run clean && tsc -p . && npm run lint && npm run build:nodebrowser && api-extractor run --local && npm run build:types", + "build": "npm run clean && tsc -p . && npm run build:nodebrowser && api-extractor run --local && npm run build:types", "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", "clean": "rimraf dist dist-* temp types *.tgz *.log coverage coverage-browser", "docs": "typedoc --excludePrivate --excludeNotExported --excludeExternals --stripInternal --mode file --out ./dist/docs ./src", diff --git a/sdk/monitor/monitor-query/review/monitor-query.api.md b/sdk/monitor/monitor-query/review/monitor-query.api.md index 48e6f30d1fc3..ed339ea54f6c 100644 --- a/sdk/monitor/monitor-query/review/monitor-query.api.md +++ b/sdk/monitor/monitor-query/review/monitor-query.api.md @@ -7,7 +7,6 @@ import { CommonClientOptions } from '@azure/core-client'; import { OperationOptions } from '@azure/core-client'; import { PagedAsyncIterableIterator } from '@azure/core-paging'; -import { PipelineOptions } from '@azure/core-rest-pipeline'; import { TokenCredential } from '@azure/core-auth'; // @public @@ -38,11 +37,11 @@ export interface ErrorDetail { } // @public -export interface ErrorInfo { +export interface ErrorInfo extends Error { additionalProperties?: Record; code: string; details?: ErrorDetail[]; - innererror?: ErrorInfo; + innerError?: ErrorInfo; message: string; } @@ -66,15 +65,16 @@ export interface LogsColumn { export type LogsColumnType = string; // @public -export type LogsQueryBatchOptions = OperationOptions; +export interface LogsQueryBatchOptions extends OperationOptions { + throwOnAnyFailure?: boolean; +} // @public export interface LogsQueryBatchResult { - results?: { - id?: string; - status?: number; + results: { tables?: LogsTable[]; error?: ErrorInfo; + status?: LogsQueryResultStatus; statistics?: Record; visualization?: Record; }[]; @@ -101,19 +101,24 @@ export interface LogsQueryOptions extends OperationOptions { includeQueryStatistics?: boolean; includeVisualization?: boolean; serverTimeoutInSeconds?: number; + throwOnAnyFailure?: boolean; } // @public export interface LogsQueryResult { error?: ErrorInfo; statistics?: Record; + status: LogsQueryResultStatus; tables: LogsTable[]; visualization?: Record; } +// @public +export type LogsQueryResultStatus = "Partial" | "Success" | "Failed"; + // @public export interface LogsTable { - columns: LogsColumn[]; + columnDescriptors: LogsColumn[]; name: string; rows: (Date | string | number | Record | boolean)[][]; } @@ -128,6 +133,7 @@ export interface MetadataValue { export interface Metric { description?: string; errorCode?: string; + errorMessage?: string; id: string; name: string; timeseries: TimeSeriesElement[]; @@ -171,7 +177,7 @@ export interface MetricNamespace { } // @public -export interface MetricsClientOptions extends PipelineOptions { +export interface MetricsClientOptions extends CommonClientOptions { endpoint?: string; } @@ -199,11 +205,12 @@ export interface MetricsQueryOptions extends OperationOptions { // @public export interface MetricsQueryResult { cost?: number; + getMetricByName(metricName: string): Metric | undefined; granularity?: string; metrics: Metric[]; namespace?: string; resourceRegion?: string; - timespan: string; + timespan: TimeInterval; } // @public @@ -229,7 +236,7 @@ export interface QueryBatch { includeVisualization?: boolean; query: string; serverTimeoutInSeconds?: number; - timespan?: TimeInterval; + timespan: TimeInterval; workspaceId: string; } diff --git a/sdk/monitor/monitor-query/samples-dev/logsQuery.ts b/sdk/monitor/monitor-query/samples-dev/logsQuery.ts index a3d1c1d6e9bc..dff6a944da6a 100644 --- a/sdk/monitor/monitor-query/samples-dev/logsQuery.ts +++ b/sdk/monitor/monitor-query/samples-dev/logsQuery.ts @@ -59,7 +59,7 @@ export async function main() { ); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples-dev/logsQueryBatch.ts b/sdk/monitor/monitor-query/samples-dev/logsQueryBatch.ts index 4f9f4d20351d..bf3dcbab31cb 100644 --- a/sdk/monitor/monitor-query/samples-dev/logsQueryBatch.ts +++ b/sdk/monitor/monitor-query/samples-dev/logsQueryBatch.ts @@ -54,7 +54,7 @@ export async function main() { let i = 0; for (const response of result.results) { - console.log(`Results for query with id: ${response.id}`); + console.log(`Results for query with query: ${queriesBatch[i]}`); if (response.error) { console.log(` Query had errors:`, response.error); @@ -67,7 +67,7 @@ export async function main() { ); for (const table of response.tables) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log(columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples-dev/logsQueryMultipleWorkspaces.ts b/sdk/monitor/monitor-query/samples-dev/logsQueryMultipleWorkspaces.ts index b7304454d5a0..73102d386d62 100644 --- a/sdk/monitor/monitor-query/samples-dev/logsQueryMultipleWorkspaces.ts +++ b/sdk/monitor/monitor-query/samples-dev/logsQueryMultipleWorkspaces.ts @@ -62,7 +62,7 @@ export async function main() { ); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples-dev/metricsQuery.ts b/sdk/monitor/monitor-query/samples-dev/metricsQuery.ts index aa9353b3a082..55c367036fb4 100644 --- a/sdk/monitor/monitor-query/samples-dev/metricsQuery.ts +++ b/sdk/monitor/monitor-query/samples-dev/metricsQuery.ts @@ -6,7 +6,7 @@ */ import { DefaultAzureCredential } from "@azure/identity"; -import { Durations, Metric, MetricsQueryClient, MetricDefinition } from "@azure/monitor-query"; +import { Durations, Metric, MetricsQueryClient } from "@azure/monitor-query"; import * as dotenv from "dotenv"; dotenv.config(); @@ -22,28 +22,32 @@ export async function main() { } const iterator = metricsQueryClient.listMetricDefinitions(metricsResourceId); - let result = await iterator.next(); - const firstMetric: MetricDefinition = result.value; - - while (!result.done) { - console.log(` metricDefinitions - ${result.value.id}, ${result.value.name}`); - result = await iterator.next(); + let metricNames: string[] = []; + for await (const result of iterator) { + console.log(` metricDefinitions - ${result.id}, ${result.name}`); + if (result.name) { + metricNames.push(result.name); + } } - console.log(`First Metric Definition = ${firstMetric.name}`); - - console.log(`Picking an example metric to query: ${firstMetric.name!}`); - - const metricsResponse = await metricsQueryClient.query(metricsResourceId, [firstMetric.name!], { - granularity: "PT1M", - timespan: { duration: Durations.FiveMinutes } - }); - console.log( - `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.granularity}, time span: ${metricsResponse.timespan}` - ); - - const metrics: Metric[] = metricsResponse.metrics; - console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + if (metricNames.length > 0) { + console.log(`Picking an example list of metrics to query: ${metricNames}`); + const metricsResponse = await metricsQueryClient.query(metricsResourceId, metricNames, { + granularity: "PT1M", + timespan: { duration: Durations.FiveMinutes } + }); + + console.log( + `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.granularity}, time span: ${metricsResponse.timespan}` + ); + + const metrics: Metric[] = metricsResponse.metrics; + console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + const metric = metricsResponse.getMetricByName(metricNames[0]); + console.log(`Selected Metric: ${metricNames[0]}`, JSON.stringify(metric, undefined, 2)); + } else { + console.error(`Metric names are not defined - ${metricNames}`); + } } main().catch((err) => { diff --git a/sdk/monitor/monitor-query/samples/v1/javascript/logsQuery.js b/sdk/monitor/monitor-query/samples/v1/javascript/logsQuery.js index a06a1e7f0598..898026e9dc3f 100644 --- a/sdk/monitor/monitor-query/samples/v1/javascript/logsQuery.js +++ b/sdk/monitor/monitor-query/samples/v1/javascript/logsQuery.js @@ -23,7 +23,7 @@ async function main() { const kustoQuery = "AppEvents | project TimeGenerated, Name, AppRoleInstance | order by TimeGenerated asc | limit 10"; - console.log(`Running '${kustoQuery}' over the last 5 minutes`); + console.log(`Running '${kustoQuery}' over the last One Hour`); const queryLogsOptions = { // explicitly control the amount of time the server can spend processing the query. serverTimeoutInSeconds: 60, @@ -32,13 +32,13 @@ async function main() { includeQueryStatistics: true }; - const result = await logsQueryClient.queryLogs( + const result = await logsQueryClient.query( monitorWorkspaceId, kustoQuery, // The timespan is an ISO8601 formatted time (or interval). Some common aliases - // are available (like lastDay, lastHour, last48Hours, etc..) but any properly formatted ISO8601 + // are available (like OneDay, OneHour, FoutyEightHours, etc..) but any properly formatted ISO8601 // value is valid. - Durations.lastHour, + { duration: Durations.OneHour }, queryLogsOptions ); @@ -59,7 +59,7 @@ async function main() { ); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryBatch.js b/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryBatch.js index e2cbb4d3d650..7ea5e986a80c 100644 --- a/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryBatch.js +++ b/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryBatch.js @@ -25,30 +25,28 @@ async function main() { { workspaceId: monitorWorkspaceId, query: kqlQuery, - timespan: "P1D" + timespan: { duration: "P1D" } }, { workspaceId: monitorWorkspaceId, query: "AzureActivity | summarize count()", - timespan: "PT1H" + timespan: { duration: "PT1H" } }, { workspaceId: monitorWorkspaceId, query: "AppRequests | take 10 | summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId", - timespan: "PT1H" + timespan: { duration: "PT1H" } }, { workspaceId: monitorWorkspaceId, query: "AppRequests | take 2", - timespan: "PT1H", + timespan: { duration: "PT1H" }, includeQueryStatistics: true } ]; - const result = await logsQueryClient.queryLogsBatch({ - queries: queriesBatch - }); + const result = await logsQueryClient.queryBatch(queriesBatch); if (result.results == null) { throw new Error("No response for query"); @@ -56,7 +54,7 @@ async function main() { let i = 0; for (const response of result.results) { - console.log(`Results for query with id: ${response.id}`); + console.log(`Results for query with query: ${queriesBatch[i]}`); if (response.error) { console.log(` Query had errors:`, response.error); @@ -69,7 +67,7 @@ async function main() { ); for (const table of response.tables) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log(columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryMultipleWorkspaces.js b/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryMultipleWorkspaces.js index 0c1c83570b79..0303b2f9bce0 100644 --- a/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryMultipleWorkspaces.js +++ b/sdk/monitor/monitor-query/samples/v1/javascript/logsQueryMultipleWorkspaces.js @@ -35,13 +35,13 @@ async function main() { additionalWorkspaces: [additionalWorkspaces1, additionalWorkspaces2] }; - const result = await logsQueryClient.queryLogs( + const result = await logsQueryClient.query( monitorWorkspaceId, kustoQuery, // The timespan is an ISO8601 formatted time (or interval). Some common aliases - // are available (like lastDay, lastHour, last48Hours, etc..) but any properly formatted ISO8601 + // are available (like durationOf1Day, durationOf1Hour, durationOf48Hours, etc..) but any properly formatted ISO8601 // value is valid. - Durations.lastHour, + { duration: Durations.OneHour }, queryLogsOptions ); @@ -62,7 +62,7 @@ async function main() { ); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples/v1/javascript/metricsQuery.js b/sdk/monitor/monitor-query/samples/v1/javascript/metricsQuery.js index 71f9b82d33ae..ecf7ea903151 100644 --- a/sdk/monitor/monitor-query/samples/v1/javascript/metricsQuery.js +++ b/sdk/monitor/monitor-query/samples/v1/javascript/metricsQuery.js @@ -21,31 +21,33 @@ async function main() { throw new Error("METRICS_RESOURCE_ID must be set in the environment for this sample"); } - const result = await metricsQueryClient.getMetricDefinitions(metricsResourceId); - - for (const definition of result.definitions) { - console.log(`Definition = ${definition.name}`); - } - - const firstMetric = result.definitions[0]; - - console.log(`Picking an example metric to query: ${firstMetric.name}`); - - const metricsResponse = await metricsQueryClient.queryMetrics( - metricsResourceId, - Durations.last5Minutes, - { - metricNames: [firstMetric.name], - interval: "PT1M" + const iterator = metricsQueryClient.listMetricDefinitions(metricsResourceId); + let metricNames = []; + for await (const result of iterator) { + console.log(` metricDefinitions - ${result.id}, ${result.name}`); + if (result.name) { + metricNames.push(result.name); } - ); - - console.log( - `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.interval}, time span: ${metricsResponse.timespan}` - ); + } - const metrics = metricsResponse.metrics; - console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + if (metricNames.length > 0) { + console.log(`Picking an example list of metrics to query: ${metricNames}`); + const metricsResponse = await metricsQueryClient.query(metricsResourceId, metricNames, { + granularity: "PT1M", + timespan: { duration: Durations.FiveMinutes } + }); + + console.log( + `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.granularity}, time span: ${metricsResponse.timespan}` + ); + + const metrics = metricsResponse.metrics; + console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + const metric = metricsResponse.getMetricByName(metricNames[0]); + console.log(`Selected Metric: ${metricNames[0]}`, JSON.stringify(metric, undefined, 2)); + } else { + console.error(`Metric names are not defined - ${metricNames}`); + } } main().catch((err) => { diff --git a/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQuery.ts b/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQuery.ts index 336c0f539a80..dff6a944da6a 100644 --- a/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQuery.ts +++ b/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQuery.ts @@ -6,7 +6,7 @@ */ import { DefaultAzureCredential } from "@azure/identity"; -import { Durations, LogsQueryClient, LogsTable, QueryLogsOptions } from "@azure/monitor-query"; +import { Durations, LogsQueryClient, LogsTable, LogsQueryOptions } from "@azure/monitor-query"; import * as dotenv from "dotenv"; dotenv.config(); @@ -23,8 +23,8 @@ export async function main() { const kustoQuery = "AppEvents | project TimeGenerated, Name, AppRoleInstance | order by TimeGenerated asc | limit 10"; - console.log(`Running '${kustoQuery}' over the last 5 minutes`); - const queryLogsOptions: QueryLogsOptions = { + console.log(`Running '${kustoQuery}' over the last One Hour`); + const queryLogsOptions: LogsQueryOptions = { // explicitly control the amount of time the server can spend processing the query. serverTimeoutInSeconds: 60, // optionally enable returning additional statistics about the query's execution. @@ -32,13 +32,13 @@ export async function main() { includeQueryStatistics: true }; - const result = await logsQueryClient.queryLogs( + const result = await logsQueryClient.query( monitorWorkspaceId, kustoQuery, // The timespan is an ISO8601 formatted time (or interval). Some common aliases - // are available (like lastDay, lastHour, last48Hours, etc..) but any properly formatted ISO8601 + // are available (like OneDay, OneHour, FoutyEightHours, etc..) but any properly formatted ISO8601 // value is valid. - Durations.lastHour, + { duration: Durations.OneHour }, queryLogsOptions ); @@ -50,7 +50,7 @@ export async function main() { } const executionTime = - result.statistics && result.statistics.query && result.statistics.query.executionTime; + result.statistics && result.statistics.query && (result.statistics.query as any).executionTime; console.log( `Results for query '${kustoQuery}', execution time: ${ @@ -59,7 +59,7 @@ export async function main() { ); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryBatch.ts b/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryBatch.ts index 4f4c52517cc2..bf3dcbab31cb 100644 --- a/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryBatch.ts +++ b/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryBatch.ts @@ -25,30 +25,28 @@ export async function main() { { workspaceId: monitorWorkspaceId, query: kqlQuery, - timespan: "P1D" + timespan: { duration: "P1D" } }, { workspaceId: monitorWorkspaceId, query: "AzureActivity | summarize count()", - timespan: "PT1H" + timespan: { duration: "PT1H" } }, { workspaceId: monitorWorkspaceId, query: "AppRequests | take 10 | summarize avgRequestDuration=avg(DurationMs) by bin(TimeGenerated, 10m), _ResourceId", - timespan: "PT1H" + timespan: { duration: "PT1H" } }, { workspaceId: monitorWorkspaceId, query: "AppRequests | take 2", - timespan: "PT1H", + timespan: { duration: "PT1H" }, includeQueryStatistics: true } ]; - const result = await logsQueryClient.queryLogsBatch({ - queries: queriesBatch - }); + const result = await logsQueryClient.queryBatch(queriesBatch); if (result.results == null) { throw new Error("No response for query"); @@ -56,7 +54,7 @@ export async function main() { let i = 0; for (const response of result.results) { - console.log(`Results for query with id: ${response.id}`); + console.log(`Results for query with query: ${queriesBatch[i]}`); if (response.error) { console.log(` Query had errors:`, response.error); @@ -69,7 +67,7 @@ export async function main() { ); for (const table of response.tables) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log(columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryMultipleWorkspaces.ts b/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryMultipleWorkspaces.ts index 99ddf387ee40..73102d386d62 100644 --- a/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryMultipleWorkspaces.ts +++ b/sdk/monitor/monitor-query/samples/v1/typescript/src/logsQueryMultipleWorkspaces.ts @@ -6,7 +6,7 @@ */ import { DefaultAzureCredential } from "@azure/identity"; -import { Durations, LogsQueryClient, LogsTable, QueryLogsOptions } from "@azure/monitor-query"; +import { Durations, LogsQueryClient, LogsTable, LogsQueryOptions } from "@azure/monitor-query"; import * as dotenv from "dotenv"; dotenv.config(); @@ -26,7 +26,7 @@ export async function main() { "AppEvents | project TimeGenerated, Name, AppRoleInstance | order by TimeGenerated asc | limit 10"; console.log(`Running '${kustoQuery}' over the last 5 minutes`); - const queryLogsOptions: QueryLogsOptions = { + const queryLogsOptions: LogsQueryOptions = { // explicitly control the amount of time the server can spend processing the query. serverTimeoutInSeconds: 60, // optionally enable returning additional statistics about the query's execution. @@ -35,13 +35,13 @@ export async function main() { additionalWorkspaces: [additionalWorkspaces1, additionalWorkspaces2] }; - const result = await logsQueryClient.queryLogs( + const result = await logsQueryClient.query( monitorWorkspaceId, kustoQuery, // The timespan is an ISO8601 formatted time (or interval). Some common aliases - // are available (like lastDay, lastHour, last48Hours, etc..) but any properly formatted ISO8601 + // are available (like durationOf1Day, durationOf1Hour, durationOf48Hours, etc..) but any properly formatted ISO8601 // value is valid. - Durations.lastHour, + { duration: Durations.OneHour }, queryLogsOptions ); @@ -53,7 +53,7 @@ export async function main() { } const executionTime = - result.statistics && result.statistics.query && result.statistics.query.executionTime; + result.statistics && result.statistics.query && (result.statistics.query as any).executionTime; console.log( `Results for query '${kustoQuery}', execution time: ${ @@ -62,7 +62,7 @@ export async function main() { ); for (const table of tablesFromResult) { - const columnHeaderString = table.columns + const columnHeaderString = table.columnDescriptors .map((column) => `${column.name}(${column.type}) `) .join("| "); console.log("| " + columnHeaderString); diff --git a/sdk/monitor/monitor-query/samples/v1/typescript/src/metricsQuery.ts b/sdk/monitor/monitor-query/samples/v1/typescript/src/metricsQuery.ts index 6b88df57f00a..55c367036fb4 100644 --- a/sdk/monitor/monitor-query/samples/v1/typescript/src/metricsQuery.ts +++ b/sdk/monitor/monitor-query/samples/v1/typescript/src/metricsQuery.ts @@ -21,31 +21,33 @@ export async function main() { throw new Error("METRICS_RESOURCE_ID must be set in the environment for this sample"); } - const result = await metricsQueryClient.getMetricDefinitions(metricsResourceId); - - for (const definition of result.definitions) { - console.log(`Definition = ${definition.name}`); - } - - const firstMetric = result.definitions[0]; - - console.log(`Picking an example metric to query: ${firstMetric.name}`); - - const metricsResponse = await metricsQueryClient.queryMetrics( - metricsResourceId, - Durations.last5Minutes, - { - metricNames: [firstMetric.name!], - interval: "PT1M" + const iterator = metricsQueryClient.listMetricDefinitions(metricsResourceId); + let metricNames: string[] = []; + for await (const result of iterator) { + console.log(` metricDefinitions - ${result.id}, ${result.name}`); + if (result.name) { + metricNames.push(result.name); } - ); - - console.log( - `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.interval}, time span: ${metricsResponse.timespan}` - ); + } - const metrics: Metric[] = metricsResponse.metrics; - console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + if (metricNames.length > 0) { + console.log(`Picking an example list of metrics to query: ${metricNames}`); + const metricsResponse = await metricsQueryClient.query(metricsResourceId, metricNames, { + granularity: "PT1M", + timespan: { duration: Durations.FiveMinutes } + }); + + console.log( + `Query cost: ${metricsResponse.cost}, interval: ${metricsResponse.granularity}, time span: ${metricsResponse.timespan}` + ); + + const metrics: Metric[] = metricsResponse.metrics; + console.log(`Metrics:`, JSON.stringify(metrics, undefined, 2)); + const metric = metricsResponse.getMetricByName(metricNames[0]); + console.log(`Selected Metric: ${metricNames[0]}`, JSON.stringify(metric, undefined, 2)); + } else { + console.error(`Metric names are not defined - ${metricNames}`); + } } main().catch((err) => { diff --git a/sdk/monitor/monitor-query/src/generated/logquery/src/models/index.ts b/sdk/monitor/monitor-query/src/generated/logquery/src/models/index.ts index 226800828d00..ce6abc7d0591 100644 --- a/sdk/monitor/monitor-query/src/generated/logquery/src/models/index.ts +++ b/sdk/monitor/monitor-query/src/generated/logquery/src/models/index.ts @@ -47,7 +47,7 @@ export interface ErrorInfo { /** error details. */ details?: ErrorDetail[]; /** Inner error details if they exist. */ - innererror?: ErrorInfo; + innerError?: ErrorInfo; /** Additional properties that can be provided on the error info object */ additionalProperties?: Record; } diff --git a/sdk/monitor/monitor-query/src/generated/logquery/src/models/mappers.ts b/sdk/monitor/monitor-query/src/generated/logquery/src/models/mappers.ts index 6e59f80a69ff..1f7c6650867f 100644 --- a/sdk/monitor/monitor-query/src/generated/logquery/src/models/mappers.ts +++ b/sdk/monitor/monitor-query/src/generated/logquery/src/models/mappers.ts @@ -150,7 +150,7 @@ export const ErrorInfo: coreClient.CompositeMapper = { } } }, - innererror: { + innerError: { serializedName: "innererror", type: { name: "Composite", diff --git a/sdk/monitor/monitor-query/src/generated/logquery/src/operations/metadata.ts b/sdk/monitor/monitor-query/src/generated/logquery/src/operations/metadata.ts index 890f0e8cba75..f802a2f962f9 100644 --- a/sdk/monitor/monitor-query/src/generated/logquery/src/operations/metadata.ts +++ b/sdk/monitor/monitor-query/src/generated/logquery/src/operations/metadata.ts @@ -18,7 +18,7 @@ import { MetadataPostResponse } from "../models"; -/** Class representing a Metadata. */ +/** Class containing Metadata operations. */ export class MetadataImpl implements Metadata { private readonly client: AzureLogAnalyticsContext; diff --git a/sdk/monitor/monitor-query/src/generated/logquery/src/operations/query.ts b/sdk/monitor/monitor-query/src/generated/logquery/src/operations/query.ts index 97e7a882c56e..f575317db040 100644 --- a/sdk/monitor/monitor-query/src/generated/logquery/src/operations/query.ts +++ b/sdk/monitor/monitor-query/src/generated/logquery/src/operations/query.ts @@ -22,7 +22,7 @@ import { QueryBatchResponse } from "../models"; -/** Class representing a Query. */ +/** Class containing Query operations. */ export class QueryImpl implements Query { private readonly client: AzureLogAnalyticsContext; diff --git a/sdk/monitor/monitor-query/src/generated/metrics/src/operations/metrics.ts b/sdk/monitor/monitor-query/src/generated/metrics/src/operations/metrics.ts index 4992ca2593b1..cc1e5a60429f 100644 --- a/sdk/monitor/monitor-query/src/generated/metrics/src/operations/metrics.ts +++ b/sdk/monitor/monitor-query/src/generated/metrics/src/operations/metrics.ts @@ -13,7 +13,7 @@ import * as Parameters from "../models/parameters"; import { MonitorManagementClientContext } from "../monitorManagementClientContext"; import { MetricsListOptionalParams, MetricsListResponse } from "../models"; -/** Class representing a Metrics. */ +/** Class containing Metrics operations. */ export class MetricsImpl implements Metrics { private readonly client: MonitorManagementClientContext; diff --git a/sdk/monitor/monitor-query/src/generated/metricsdefinitions/src/operations/metricDefinitions.ts b/sdk/monitor/monitor-query/src/generated/metricsdefinitions/src/operations/metricDefinitions.ts index 7cfce3525b68..bc43a350fbd0 100644 --- a/sdk/monitor/monitor-query/src/generated/metricsdefinitions/src/operations/metricDefinitions.ts +++ b/sdk/monitor/monitor-query/src/generated/metricsdefinitions/src/operations/metricDefinitions.ts @@ -16,7 +16,7 @@ import { MetricDefinitionsListResponse } from "../models"; -/** Class representing a MetricDefinitions. */ +/** Class containing MetricDefinitions operations. */ export class MetricDefinitionsImpl implements MetricDefinitions { private readonly client: MonitorManagementClientContext; diff --git a/sdk/monitor/monitor-query/src/generated/metricsnamespaces/src/operations/metricNamespaces.ts b/sdk/monitor/monitor-query/src/generated/metricsnamespaces/src/operations/metricNamespaces.ts index 32998a25aeb4..273db59b7aa3 100644 --- a/sdk/monitor/monitor-query/src/generated/metricsnamespaces/src/operations/metricNamespaces.ts +++ b/sdk/monitor/monitor-query/src/generated/metricsnamespaces/src/operations/metricNamespaces.ts @@ -16,7 +16,7 @@ import { MetricNamespacesListResponse } from "../models"; -/** Class representing a MetricNamespaces. */ +/** Class containing MetricNamespaces operations. */ export class MetricNamespacesImpl implements MetricNamespaces { private readonly client: MonitorManagementClientContext; diff --git a/sdk/monitor/monitor-query/src/index.ts b/sdk/monitor/monitor-query/src/index.ts index 681b8f1a6ffe..319c34015b26 100644 --- a/sdk/monitor/monitor-query/src/index.ts +++ b/sdk/monitor/monitor-query/src/index.ts @@ -14,7 +14,9 @@ export { // TODO: design issues around this still pending. // QueryStatistics, LogsTable, - LogsColumn + LogsColumn, + LogsQueryResultStatus, + ErrorInfo } from "./models/publicLogsModels"; export { MetricsQueryClient, @@ -42,8 +44,7 @@ export { // TODO: these are the generated model names. We probably want to run them // through a manual review to make them consistent with style. LogsColumnType, - ErrorDetail, - ErrorInfo + ErrorDetail } from "./generated/logquery/src"; // diff --git a/sdk/monitor/monitor-query/src/internal/modelConverters.ts b/sdk/monitor/monitor-query/src/internal/modelConverters.ts index 2baa95dbb970..58d0425e6127 100644 --- a/sdk/monitor/monitor-query/src/internal/modelConverters.ts +++ b/sdk/monitor/monitor-query/src/internal/modelConverters.ts @@ -4,10 +4,12 @@ import { BatchRequest as GeneratedBatchRequest, BatchQueryRequest as GeneratedBatchQueryRequest, - BatchQueryResponse as GeneratedBatchQueryResponse, QueryBatchResponse as GeneratedQueryBatchResponse, + BatchQueryResponse as GeneratedBatchQueryResponse, QueryBody, - Table as GeneratedTable + Table as GeneratedTable, + BatchQueryResults as GeneratedBatchQueryResults, + ErrorInfo as GeneratedErrorInfo } from "../generated/logquery/src"; import { @@ -39,8 +41,12 @@ import { MetricDefinition, TimeSeriesElement } from "../models/publicMetricsModels"; -import { FullOperationResponse } from "../../../../core/core-client/types/latest/core-client"; -import { convertTimespanToInterval } from "../timespanConversion"; +import { FullOperationResponse } from "@azure/core-client"; +import { + convertIntervalToTimeIntervalObject, + convertTimespanToInterval +} from "../timespanConversion"; +import { ErrorInfo, LogsQueryResult } from "../models/publicLogsModels"; /** * @internal @@ -101,13 +107,14 @@ export function convertResponseForQueryBatch( rawResponse: FullOperationResponse ): LogsQueryBatchResult { const fixApplied = fixInvalidBatchQueryResponse(generatedResponse, rawResponse); - /* Sort the ids that are passed in with the queries, as numbers instead of strings * It is not guaranteed that service will return the responses for queries in the same order * as the queries are passed in */ + const responseList = generatedResponse.responses || []; + const newResponse: LogsQueryBatchResult = { - results: generatedResponse.responses + results: responseList ?.sort((a, b) => { let left = 0; if (a.id != null) { @@ -121,19 +128,24 @@ export function convertResponseForQueryBatch( return left - right; }) - ?.map((response: GeneratedBatchQueryResponse) => ({ - id: response.id, - status: response.status, - // hoist fields from the sub-object 'body' to this level - error: response.body?.error, - tables: response.body?.tables?.map(convertGeneratedTable) - })) + ?.map((response: GeneratedBatchQueryResponse) => convertBatchQueryResponseHelper(response)) }; - + // compute status for failed or succeed or partial results + + const resultsCount = newResponse.results?.length ?? 0; + for (let i = 0; i < resultsCount; i++) { + const result = newResponse.results[i]; + if (result.error && result.tables) { + result.status = "Partial"; + } else if (result.tables) { + result.status = "Success"; + } else { + result.status = "Failed"; + } + } (newResponse as any)["__fixApplied"] = fixApplied; return newResponse; } - /** * This is a workaround for a service bug that we're investigating. The 'body' column will occasionally come * back as a JSON string, instead of being a JSON object. @@ -248,11 +260,15 @@ export function convertResponseForMetrics( }); // eslint-disable-next-line @typescript-eslint/no-unused-vars -- eslint doesn't recognize that the extracted variables are prefixed with '_' and are purposefully unused. - const { resourceregion, value: _ignoredValue, interval, ...rest } = generatedResponse; + const { resourceregion, value: _ignoredValue, interval, timespan, ...rest } = generatedResponse; const obj: MetricsQueryResult = { ...rest, - metrics + metrics, + timespan: convertIntervalToTimeIntervalObject(timespan), + getMetricByName(metricName) { + return this.metrics.find((it) => it.name === metricName); + } }; if (resourceregion) { @@ -373,6 +389,48 @@ export function convertGeneratedTable(table: GeneratedTable): LogsTable { } return row; - }) + }), + columnDescriptors: table.columns }; } + +/** + * @internal + */ +export function convertBatchQueryResponseHelper( + response: GeneratedBatchQueryResponse +): Partial { + try { + const parsedResponseBody: GeneratedBatchQueryResults = JSON.parse( + response.body as any + ) as GeneratedBatchQueryResults; + return { + visualization: parsedResponseBody.render, + status: "Success", // Assume success until shown otherwise. + statistics: parsedResponseBody.statistics, + error: mapError(parsedResponseBody.error), // ? { ...parsedResponseBody.error, name: "Error" } : undefined, + tables: parsedResponseBody.tables?.map((table: GeneratedTable) => + convertGeneratedTable(table) + ) + }; + } catch (e) { + return { + visualization: response.body?.render, + status: "Success", // Assume success until shown otherwise. + statistics: response.body?.statistics, + error: mapError(response.body?.error), + tables: response.body?.tables?.map((table: GeneratedTable) => convertGeneratedTable(table)) + }; + } +} + +export function mapError(error?: GeneratedErrorInfo): ErrorInfo | undefined { + if (error) { + return { + ...error, + name: "Error", + innerError: mapError(error.innerError) + }; + } + return undefined; +} diff --git a/sdk/monitor/monitor-query/src/logsQueryClient.ts b/sdk/monitor/monitor-query/src/logsQueryClient.ts index 60abe6204596..ea0eb2413e75 100644 --- a/sdk/monitor/monitor-query/src/logsQueryClient.ts +++ b/sdk/monitor/monitor-query/src/logsQueryClient.ts @@ -9,13 +9,17 @@ import { LogsQueryBatchOptions, LogsQueryBatchResult, LogsQueryOptions, - LogsQueryResult + LogsQueryResult, + AggregateBatchError, + BatchError, + ErrorInfo } from "./models/publicLogsModels"; import { convertGeneratedTable, convertRequestForQueryBatch, - convertResponseForQueryBatch + convertResponseForQueryBatch, + mapError } from "./internal/modelConverters"; import { formatPreferHeader } from "./internal/util"; import { CommonClientOptions, FullOperationResponse, OperationOptions } from "@azure/core-client"; @@ -115,11 +119,28 @@ export class LogsQueryClient { const parsedBody = JSON.parse(rawResponse.bodyAsText!); flatResponse.tables = parsedBody.tables; - return { + const result: LogsQueryResult = { tables: flatResponse.tables.map(convertGeneratedTable), statistics: flatResponse.statistics, - visualization: flatResponse.render + visualization: flatResponse.render, + error: mapError(flatResponse.error), + status: "Success" // Assume success until shown otherwise. }; + if (!result.error) { + // if there is no error field, it is success + result.status = "Success"; + } else { + // result.tables is always present in single query response, even is there is error + if (result.tables.length === 0) { + result.status = "Failed"; + } else { + result.status = "Partial"; + } + } + if (options?.throwOnAnyFailure && result.status !== "Success") { + throw result.error; + } + return result; } /** @@ -137,7 +158,16 @@ export class LogsQueryClient { (paramOptions) => this._logAnalytics.query.batch(generatedRequest, paramOptions), options || {} ); - return convertResponseForQueryBatch(flatResponse, rawResponse); + const result: LogsQueryBatchResult = convertResponseForQueryBatch(flatResponse, rawResponse); + + if (options?.throwOnAnyFailure && result.results.some((it) => it.status !== "Success")) { + const errorResults = result.results + .filter((it) => it.status !== "Success") + .map((x) => x.error); + const batchErrorList = errorResults.map((x) => new BatchError(x as ErrorInfo)); + throw new AggregateBatchError(batchErrorList); + } + return result; } } diff --git a/sdk/monitor/monitor-query/src/metricsQueryClient.ts b/sdk/monitor/monitor-query/src/metricsQueryClient.ts index 9a6e48a3c18c..4ce8b61198c1 100644 --- a/sdk/monitor/monitor-query/src/metricsQueryClient.ts +++ b/sdk/monitor/monitor-query/src/metricsQueryClient.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { TokenCredential } from "@azure/core-auth"; -import { PipelineOptions, bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline"; import { PagedAsyncIterableIterator } from "@azure/core-paging"; +import { CommonClientOptions } from "@azure/core-client"; import { ListMetricDefinitionsOptions, @@ -36,7 +36,7 @@ import { /** * Options for the MetricsQueryClient. */ -export interface MetricsQueryClientOptions extends PipelineOptions { +export interface MetricsQueryClientOptions extends CommonClientOptions { /** Overrides client endpoint. */ endpoint?: string; } @@ -58,29 +58,25 @@ export class MetricsQueryClient { const serviceClientOptions = { ...options, $host: options?.endpoint, - endpoint: options?.endpoint + endpoint: options?.endpoint, + credentialScopes: formatScope(options?.endpoint), + credential: tokenCredential }; - const bearerTokenPolicy = bearerTokenAuthenticationPolicy({ - credential: tokenCredential, - scopes: formatScope(options?.endpoint) - }); + this._metricsClient = new GeneratedMetricsClient( MetricsApiVersion.TwoThousandEighteen0101, serviceClientOptions ); - this._metricsClient.pipeline.addPolicy(bearerTokenPolicy); this._definitionsClient = new GeneratedMetricsDefinitionsClient( MetricDefinitionsApiVersion.TwoThousandEighteen0101, serviceClientOptions ); - this._definitionsClient.pipeline.addPolicy(bearerTokenPolicy); this._namespacesClient = new GeneratedMetricsNamespacesClient( MetricNamespacesApiVersion.TwoThousandSeventeen1201Preview, serviceClientOptions ); - this._namespacesClient.pipeline.addPolicy(bearerTokenPolicy); } /** diff --git a/sdk/monitor/monitor-query/src/models/publicLogsModels.ts b/sdk/monitor/monitor-query/src/models/publicLogsModels.ts index 7e96bae67b2f..80c4098a8cf9 100644 --- a/sdk/monitor/monitor-query/src/models/publicLogsModels.ts +++ b/sdk/monitor/monitor-query/src/models/publicLogsModels.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { OperationOptions } from "@azure/core-client"; -import { ErrorInfo, LogsColumnType } from "../generated/logquery/src"; +import { ErrorDetail, LogsColumnType } from "../generated/logquery/src"; import { TimeInterval } from "./timeInterval"; // https://dev.loganalytics.io/documentation/Using-the-API/RequestOptions @@ -35,6 +35,10 @@ export interface LogsQueryOptions extends OperationOptions { * Results will also include visualization information, in JSON format. */ includeVisualization?: boolean; + /** + * If true, will cause this operation to throw if query operation did not succeed. + */ + throwOnAnyFailure?: boolean; } /** @@ -48,22 +52,76 @@ export interface QueryStatistics { [key: string]: unknown; } +/** The code and message for an error. */ +export interface ErrorInfo extends Error { + /** A machine readable error code. */ + code: string; + /** A human readable error message. */ + message: string; + /** error details. */ + details?: ErrorDetail[]; + /** Inner error details if they exist. */ + innerError?: ErrorInfo; + /** Additional properties that can be provided on the error info object */ + additionalProperties?: Record; +} + +export class BatchError extends Error implements ErrorInfo { + /** A machine readable error code. */ + code: string; + /** A human readable error message. */ + message: string; + /** error details. */ + details?: ErrorDetail[]; + /** Inner error details if they exist. */ + innerError?: ErrorInfo; + /** Additional properties that can be provided on the error info object */ + additionalProperties?: Record; + + constructor(errorInfo: ErrorInfo) { + super(); + this.name = "Error"; + this.code = errorInfo.code; + this.message = errorInfo.message; + this.details = errorInfo.details; + this.innerError = errorInfo.innerError; + this.additionalProperties = errorInfo.additionalProperties; + } +} +export class AggregateBatchError extends Error { + errors: BatchError[]; + constructor(errors: ErrorInfo[]) { + super(); + this.errors = errors.map((x) => new BatchError(x)); + } +} /** * Tables and statistic results from a logs query. */ + export interface LogsQueryResult { - /** The list of tables, columns and rows. */ + /** Populated results from the query. */ tables: LogsTable[]; + /** error information for partial errors or failed queries */ + error?: ErrorInfo; + /** Indicates if a query succeeded or failed or partially failed. + * Represented by "Partial" | "Success" | "Failed". + * For partially failed queries, users can find data in "tables" attribute + * and error information in "error" attribute */ + status: LogsQueryResultStatus; /** Statistics represented in JSON format. */ statistics?: Record; /** Visualization data in JSON format. */ visualization?: Record; - /** The code and message for an error. */ - error?: ErrorInfo; } -/** Configurable HTTP request settings for the Logs query batch operation. */ -export type LogsQueryBatchOptions = OperationOptions; +/** Configurable HTTP request settings and `throwOnAnyFailure` setting for the Logs query batch operation. */ +export interface LogsQueryBatchOptions extends OperationOptions { + /** + * If true, will cause the batch operation to throw if any query operations in the batch did not succeed. + */ + throwOnAnyFailure?: boolean; +} /** The Analytics query. Learn more about the [Analytics query syntax](https://azure.microsoft.com/documentation/articles/app-insights-analytics-reference/) */ // NOTE: 'id' is added automatically by our LogsQueryClient. @@ -77,7 +135,7 @@ export interface QueryBatch { /** The query to execute. */ query: string; /** The timespan over which to query data. This timespan is applied in addition to any that are specified in the query expression. */ - timespan?: TimeInterval; + timespan: TimeInterval; /** * A list of workspaces that are included in the query, except for the one set as the `workspaceId` parameter * These may consist of the following identifier formats: @@ -106,31 +164,36 @@ export interface QueryBatch { /** Results for a batch query. */ export interface LogsQueryBatchResult { /** An array of responses corresponding to each individual request in a batch. */ - results?: { - id?: string; - status?: number; - /** The list of tables, columns and rows. */ - // (hoisted up from `LogQueryResult`) + results: { + /** Populated results from the query */ tables?: LogsTable[]; + /** error information for partial errors or failed queries */ error?: ErrorInfo; + /** Indicates if a query succeeded or failed or partially failed. + * Represented by "Partial" | "Success" | "Failed". + * For partially failed queries, users can find data in "tables" attribute + * and error information in "error" attribute */ + status?: LogsQueryResultStatus; /** Statistics represented in JSON format. */ statistics?: Record; /** Visualization data in JSON format. */ visualization?: Record; }[]; - - // TODO: this is omitted from the Java models. - /** Error response for a batch request */ - // error?: BatchResponseError; } +/** Indicates if a query succeeded or failed or partially failed. + * Represented by "Partial" | "Success" | "Failed". + * For partially failed queries, users can find data in "tables" attribute + * and error information in "error" attribute */ +export type LogsQueryResultStatus = "Partial" | "Success" | "Failed"; + /** Contains the columns and rows for one table in a query response. */ export interface LogsTable { /** The name of the table. */ name: string; /** The list of columns in this table. */ - columns: LogsColumn[]; - /** The resulting rows from this query. */ + columnDescriptors: LogsColumn[]; + /** The two dimensional array of results from this query indexed by row and column. */ rows: (Date | string | number | Record | boolean)[][]; } diff --git a/sdk/monitor/monitor-query/src/models/publicMetricsModels.ts b/sdk/monitor/monitor-query/src/models/publicMetricsModels.ts index f268546671a9..5ec72be553c9 100644 --- a/sdk/monitor/monitor-query/src/models/publicMetricsModels.ts +++ b/sdk/monitor/monitor-query/src/models/publicMetricsModels.ts @@ -66,6 +66,8 @@ export interface Metric { description?: string; /** 'Success' or the error details on query failures for this metric. */ errorCode?: string; + /** Error message encountered querying this specific metric. */ + errorMessage?: string; /** the unit of the metric. */ unit: MetricUnit; /** the time series returned when a data query is performed. */ @@ -100,7 +102,7 @@ export interface MetricsQueryResult { /** The integer value representing the cost of the query, for data case. */ cost?: number; /** The timespan for which the data was retrieved. Its value consists of two datetimes concatenated, separated by '/'. This may be adjusted in the future and returned back from what was originally requested. */ - timespan: string; + timespan: TimeInterval; /** The interval (window size) for which the metric data was returned in. This may be adjusted in the future and returned back from what was originally requested. This is not present if a metadata request was made. */ granularity?: string; /** The namespace of the metrics been queried */ @@ -109,6 +111,8 @@ export interface MetricsQueryResult { resourceRegion?: string; /** the value of the collection. */ metrics: Metric[]; + /** convenience method to get metric by metric name */ + getMetricByName(metricName: string): Metric | undefined; } /** diff --git a/sdk/monitor/monitor-query/src/timespanConversion.ts b/sdk/monitor/monitor-query/src/timespanConversion.ts index 4500dabb0926..5d1b303e3f2e 100644 --- a/sdk/monitor/monitor-query/src/timespanConversion.ts +++ b/sdk/monitor/monitor-query/src/timespanConversion.ts @@ -21,6 +21,22 @@ export function convertTimespanToInterval(timespan: TimeInterval): string { throw new TypeError("Invalid Timespan - no valid fields assigned."); } +export function convertIntervalToTimeIntervalObject(timespan: string): TimeInterval { + if (timespan.includes("/")) { + const intervalUnits: string[] = timespan.split("/"); + if (Date.parse(intervalUnits[0]) && Date.parse(intervalUnits[2])) { + return { startTime: new Date(intervalUnits[0]), endTime: new Date(intervalUnits[2]) }; + } else if (Date.parse(intervalUnits[0]) && !Date.parse(intervalUnits[2])) { + return { startTime: new Date(intervalUnits[0]), duration: intervalUnits[2] }; + } else if (!Date.parse(intervalUnits[0]) && Date.parse(intervalUnits[2])) { + return { duration: intervalUnits[0], endTime: new Date(intervalUnits[2]) }; + } else { + return { duration: timespan }; + } + } else { + return { duration: timespan }; + } +} /** * Helper TypeGuard that checks if the input is an object with the specified property. * Note: The property may be inherited. diff --git a/sdk/monitor/monitor-query/swagger/logquery.md b/sdk/monitor/monitor-query/swagger/logquery.md index 178d972b0a57..732e44f414db 100644 --- a/sdk/monitor/monitor-query/swagger/logquery.md +++ b/sdk/monitor/monitor-query/swagger/logquery.md @@ -8,7 +8,7 @@ input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/operationalinsights/data-plane/Microsoft.OperationalInsights/preview/2021-05-19_Preview/OperationalInsights.json output-folder: ../src/generated/logquery package-name: "monitor-log-query" -package-version: "1.0.0-beta.4" +package-version: "1.0.0-beta.5" clear-output-folder: true generate-metadata: false add-credentials: false @@ -22,3 +22,18 @@ hide-clients: true v3: true typescript: true ``` + +## Customizations for Track 2 Generator + +See the [AutoRest samples](https://github.com/Azure/autorest/tree/master/Samples/3b-custom-transformations) +for more about how we're customizing things. + +### ErrorInfo - renaming `errorinfo` property name to `errorInfo` + +```yaml +directive: + - from: swagger-document + where: $.definitions.errorInfo + transform: > + $.properties.innererror["x-ms-client-name"] = "innerError"; +``` diff --git a/sdk/monitor/monitor-query/swagger/metric-definitions.md b/sdk/monitor/monitor-query/swagger/metric-definitions.md index 5ef11f2bd3a3..5e1af02dc750 100644 --- a/sdk/monitor/monitor-query/swagger/metric-definitions.md +++ b/sdk/monitor/monitor-query/swagger/metric-definitions.md @@ -9,7 +9,7 @@ input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/ service-name: MetricsDefinitions output-folder: ../src/generated/metricsdefinitions package-name: "monitor-metrics-definitions" -package-version: "1.0.0-beta.4" +package-version: "1.0.0-beta.5" clear-output-folder: true generate-metadata: false add-credentials: false diff --git a/sdk/monitor/monitor-query/swagger/metric-namespaces.md b/sdk/monitor/monitor-query/swagger/metric-namespaces.md index f5af68e091cf..99bb34b7b434 100644 --- a/sdk/monitor/monitor-query/swagger/metric-namespaces.md +++ b/sdk/monitor/monitor-query/swagger/metric-namespaces.md @@ -8,7 +8,7 @@ input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/monitor/resource-manager/Microsoft.Insights/preview/2017-12-01-preview/metricNamespaces_API.json output-folder: ../src/generated/metricsnamespaces package-name: "monitor-metrics-namespaces" -package-version: "1.0.0-beta.4" +package-version: "1.0.0-beta.5" clear-output-folder: true generate-metadata: false add-credentials: false diff --git a/sdk/monitor/monitor-query/swagger/metrics.md b/sdk/monitor/monitor-query/swagger/metrics.md index 93cfae129fa7..66319e68eeb8 100644 --- a/sdk/monitor/monitor-query/swagger/metrics.md +++ b/sdk/monitor/monitor-query/swagger/metrics.md @@ -8,7 +8,7 @@ input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/monitor/resource-manager/Microsoft.Insights/stable/2018-01-01/metrics_API.json output-folder: ../src/generated/metrics package-name: "monitor-metrics" -package-version: "1.0.0-beta.4" +package-version: "1.0.0-beta.5" clear-output-folder: true generate-metadata: false add-credentials: false diff --git a/sdk/monitor/monitor-query/test/internal/unit/modelConverters.unittest.spec.ts b/sdk/monitor/monitor-query/test/internal/unit/modelConverters.unittest.spec.ts index 5d1ef04ed794..9030dffdc453 100644 --- a/sdk/monitor/monitor-query/test/internal/unit/modelConverters.unittest.spec.ts +++ b/sdk/monitor/monitor-query/test/internal/unit/modelConverters.unittest.spec.ts @@ -24,12 +24,7 @@ import { SerializerOptions } from "@azure/core-client"; import { OperationTracingOptions } from "@azure/core-tracing"; -import { - Durations, - ListMetricDefinitionsOptions, - MetricsQueryOptions, - MetricsQueryResult -} from "../../../src"; +import { Durations, ListMetricDefinitionsOptions, MetricsQueryOptions } from "../../../src"; import { AbortSignalLike } from "@azure/abort-controller"; describe("Model unit tests", () => { @@ -74,7 +69,6 @@ describe("Model unit tests", () => { additionalWorkspaces: ["additionalWorkspace", "resourceId1"] } ]); - console.log(JSON.stringify(generatedRequest.requests?.[1])); assert.deepEqual(generatedRequest.requests?.[1], { body: { workspaces: ["additionalWorkspace", "resourceId1"], @@ -111,7 +105,7 @@ describe("Model unit tests", () => { requestOptions, resultType: "Data", top: 10, - timespan: { duration: "arbitraryTimespan" }, + timespan: { duration: "P20H" }, tracingOptions, serializerOptions, onResponse @@ -132,7 +126,7 @@ describe("Model unit tests", () => { orderby: "orderByClause", requestOptions, resultType: "Data", - timespan: "arbitraryTimespan", + timespan: "P20H", top: 10, tracingOptions, serializerOptions, @@ -153,7 +147,7 @@ describe("Model unit tests", () => { const generatedResponse: Required = { // all of these fields are just copied over verbatim... - timespan: "aTimespan", + timespan: "P10H", value: [ { id: "fakeMetric", @@ -198,8 +192,8 @@ describe("Model unit tests", () => { }; const actualConvertedResponse = convertResponseForMetrics(generatedResponse); - const expectedResponse: MetricsQueryResult = { - timespan: "aTimespan", + const expectedResponse = { + timespan: { duration: "P10H" }, metrics: [ { id: "fakeMetric", @@ -238,7 +232,9 @@ describe("Model unit tests", () => { // NOTE: _response is not returned as part of our track 2 response. }; - assert.deepEqual(actualConvertedResponse, expectedResponse); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { getMetricByName, ...rest } = actualConvertedResponse; + assert.deepEqual({ ...rest }, expectedResponse); }); it("convertRequestOptionsForMetricsDefinitions (all fields)", () => { diff --git a/sdk/monitor/monitor-query/test/public/logsQueryClient.spec.ts b/sdk/monitor/monitor-query/test/public/logsQueryClient.spec.ts index 9943e2720b10..22321c800f8d 100644 --- a/sdk/monitor/monitor-query/test/public/logsQueryClient.spec.ts +++ b/sdk/monitor/monitor-query/test/public/logsQueryClient.spec.ts @@ -212,7 +212,7 @@ describe("LogsQueryClient live tests", function() { type: "dynamic" } ], - table.columns + table.columnDescriptors ); table.rows.map((rowValues) => { @@ -270,8 +270,15 @@ describe("LogsQueryClient live tests", function() { const table = result.results?.[0].tables?.[0]; console.log(JSON.stringify(result.results?.[0].tables)); + if (table == null) { - throw new Error("No table returned for query"); + throw new Error(JSON.stringify(result.results?.[0].error)); + } + + if (result.results?.[0].status === "Partial") { + throw new Error( + JSON.stringify({ ...result.results?.[0].error, ...result.results?.[0].tables }) + ); } // check the column types all match what we expect. @@ -306,7 +313,7 @@ describe("LogsQueryClient live tests", function() { type: "dynamic" } ], - table.columns + table.columnDescriptors ); table.rows.map((rowValues) => { @@ -487,8 +494,8 @@ function getInnermostErrorDetails(thrownError: any): undefined | ErrorInfo { let errorInfo: ErrorInfo = thrownError.details.error; - while (errorInfo.innererror) { - errorInfo = errorInfo.innererror; + while (errorInfo.innerError) { + errorInfo = errorInfo.innerError; } return errorInfo; diff --git a/sdk/monitor/monitor-query/test/public/shared/testShared.ts b/sdk/monitor/monitor-query/test/public/shared/testShared.ts index 1c8989a460ba..ba6aba2bac17 100644 --- a/sdk/monitor/monitor-query/test/public/shared/testShared.ts +++ b/sdk/monitor/monitor-query/test/public/shared/testShared.ts @@ -126,7 +126,9 @@ function getRequiredEnvVar(mochaContext: Pick, variableName: st export function printLogQueryTables(tables: LogsTable[]): void { for (const table of tables) { - const columnHeaderString = table.columns.map((c) => `${c.name}(${c.type}) `).join("| "); + const columnHeaderString = table.columnDescriptors + .map((c) => `${c.name}(${c.type}) `) + .join("| "); console.log(columnHeaderString); for (const row of table.rows) { @@ -153,7 +155,7 @@ export function assertQueryTable( { name: table.name, rows: table.rows, - columns: table.columns.map((c) => c.name) + columns: table.columnDescriptors.map((c) => c.name) }, expectedTable, `${message}: tables weren't equal`