AzureMonitor: Improve selection of Basic Logs tables in the query builder (#103820)

* Add function for retrieving logs table plan

- Add URL builder method
- Add types

* Add auto-switching for basic logs tables

* Set dashboardTime property

* Ensure useEffect doesn't run on every query change

* Fix basicLogs property

* Add isLoading for schema
This commit is contained in:
Andreas Christou
2025-04-10 14:11:00 -06:00
committed by GitHub
parent 05338838b0
commit 3a71a48a88
9 changed files with 178 additions and 21 deletions
@@ -7,23 +7,26 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run
import { getCredentials } from '../credentials';
import TimegrainConverter from '../time_grain_converter';
import {
AzureAPIResponse,
AzureMetricQuery,
AzureMonitorDataSourceInstanceSettings,
AzureMonitorDataSourceJsonData,
AzureMonitorLocations,
AzureMonitorMetricsMetadataResponse,
AzureMonitorProvidersResponse,
AzureMonitorQuery,
AzureQueryType,
DatasourceValidationResult,
GetLogAnalyticsTableResponse,
GetMetricMetadataQuery,
GetMetricNamespacesQuery,
GetMetricNamesQuery,
GetMetricMetadataQuery,
AzureMetricQuery,
AzureMonitorLocations,
AzureMonitorProvidersResponse,
AzureAPIResponse,
Subscription,
instanceOfLogAnalyticsTableError,
Location,
Metric,
MetricNamespace,
Subscription,
TablePlan,
} from '../types';
import { replaceTemplateVariables, routeNames } from '../utils/common';
import migrateQuery from '../utils/migrateQuery';
@@ -274,6 +277,36 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<
return undefined;
}
async getWorkspaceTablePlan(resources: string[], tableName: string): Promise<TablePlan> {
let workspaceUri = '';
if (resources) {
workspaceUri = resources[0];
}
if (!workspaceUri) {
return TablePlan.Analytics;
}
if (workspaceUri && !workspaceUri.toLowerCase().includes('microsoft.operationalinsights/workspaces')) {
// Not a Log Analytics workspace so default to Analytics
return TablePlan.Analytics;
}
const url = UrlBuilder.buildAzureMonitorGetLogsTableUrl(
this.resourcePath,
this.templateSrv.replace(workspaceUri),
this.templateSrv.replace(tableName)
);
const tableResult = await this.getResource<GetLogAnalyticsTableResponse>(url);
if (!tableResult || instanceOfLogAnalyticsTableError(tableResult)) {
return TablePlan.Analytics;
}
return tableResult.properties.plan || TablePlan.Analytics;
}
private isValidConfigField(field?: string): boolean {
return typeof field === 'string' && field.length > 0;
}
@@ -113,4 +113,13 @@ export default class UrlBuilder {
return url;
}
static buildAzureMonitorGetLogsTableUrl(
baseUrl: string,
resourceUri: string,
tableName: string,
apiVersion = '2025-02-01'
) {
return `${baseUrl}${resourceUri}/tables/${tableName}?api-version=${apiVersion}`;
}
}
@@ -42,10 +42,11 @@ interface LogsQueryBuilderProps {
templateVariableOptions: SelectableValue<string>;
datasource: Datasource;
timeRange?: TimeRange;
isLoadingSchema: boolean;
}
export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
const { query, onQueryChange, schema, datasource, timeRange } = props;
const { query, onQueryChange, schema, datasource, timeRange, isLoadingSchema } = props;
const [isKQLPreviewHidden, setIsKQLPreviewHidden] = useState<boolean>(true);
const tables: AzureLogAnalyticsMetadataTable[] = useMemo(() => {
@@ -70,6 +71,7 @@ export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
orderBy,
columns,
from,
basicLogsQuery,
}: {
limit?: number;
reduce?: BuilderQueryEditorReduceExpression[];
@@ -79,6 +81,7 @@ export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
orderBy?: BuilderQueryEditorOrderByExpression[];
columns?: string[];
from?: BuilderQueryEditorPropertyExpression;
basicLogsQuery?: boolean;
}) => {
const datetimeColumn = allColumns.find((col) => col.type === 'datetime')?.name || 'TimeGenerated';
@@ -122,6 +125,7 @@ export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
...query.azureLogAnalytics,
builderQuery: updatedBuilderQuery,
query: updatedQueryString,
basicLogsQuery: from ? basicLogsQuery : query.azureLogAnalytics?.basicLogsQuery,
},
});
},
@@ -134,7 +138,13 @@ export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => {
{schema && tables.length === 0 && (
<Alert severity="warning" title="Resource loaded successfully but without any tables" />
)}
<TableSection {...props} tables={tables} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} />
<TableSection
{...props}
tables={tables}
allColumns={allColumns}
buildAndUpdateQuery={buildAndUpdateQuery}
isLoadingSchema={isLoadingSchema}
/>
<FilterSection
{...props}
allColumns={allColumns}
@@ -5,7 +5,12 @@ import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/p
import { Button, Select } from '@grafana/ui';
import { BuilderQueryEditorExpressionType, BuilderQueryEditorPropertyType } from '../../dataquery.gen';
import { AzureMonitorQuery, AzureLogAnalyticsMetadataColumn, AzureLogAnalyticsMetadataTable } from '../../types';
import {
AzureMonitorQuery,
AzureLogAnalyticsMetadataColumn,
AzureLogAnalyticsMetadataTable,
TablePlan,
} from '../../types';
import { BuildAndUpdateOptions, inputFieldSize } from './utils';
@@ -15,16 +20,19 @@ interface TableSectionProps {
query: AzureMonitorQuery;
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void;
templateVariableOptions?: SelectableValue<string>;
onQueryChange: (newQuery: AzureMonitorQuery) => void;
isLoadingSchema: boolean;
}
export const TableSection: React.FC<TableSectionProps> = (props) => {
const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions } = props;
const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions, isLoadingSchema } = props;
const builderQuery = query.azureLogAnalytics?.builderQuery;
const selectedColumns = query.azureLogAnalytics?.builderQuery?.columns?.columns || [];
const tableOptions: Array<SelectableValue<string>> = tables.map((t) => ({
label: t.name,
value: t.name,
description: t.plan === TablePlan.Basic ? 'Selecting this table will switch the query mode to Basic Logs' : '',
}));
const columnOptions: Array<SelectableValue<string>> = allColumns.map((col) => ({
@@ -68,6 +76,7 @@ export const TableSection: React.FC<TableSectionProps> = (props) => {
groupBy: [],
orderBy: [],
columns: [],
basicLogsQuery: selectedTable.plan === TablePlan.Basic,
});
};
@@ -138,6 +147,7 @@ export const TableSection: React.FC<TableSectionProps> = (props) => {
placeholder="Select a table"
onChange={handleTableChange}
width={inputFieldSize}
isLoading={isLoadingSchema}
/>
</EditorField>
<EditorField label="Columns">
@@ -3,14 +3,14 @@ import { escapeRegExp } from 'lodash';
import { SelectableValue } from '@grafana/data';
import {
BuilderQueryExpression,
BuilderQueryEditorExpressionType,
BuilderQueryEditorPropertyType,
BuilderQueryEditorReduceExpression,
BuilderQueryEditorWhereExpression,
BuilderQueryEditorGroupByExpression,
BuilderQueryEditorOrderByExpression,
BuilderQueryEditorPropertyExpression,
BuilderQueryEditorPropertyType,
BuilderQueryEditorReduceExpression,
BuilderQueryEditorWhereExpression,
BuilderQueryExpression,
} from '../../dataquery.gen';
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types';
@@ -88,6 +88,7 @@ export interface BuildAndUpdateOptions {
orderBy?: BuilderQueryEditorOrderByExpression[];
columns?: string[];
from?: BuilderQueryEditorPropertyExpression;
basicLogsQuery?: boolean;
}
export const aggregateOptions = [
@@ -8,7 +8,14 @@ import { Alert, LinkButton, Space, Text, TextLink } from '@grafana/ui';
import { LogsEditorMode } from '../../dataquery.gen';
import Datasource from '../../datasource';
import { selectors } from '../../e2e/selectors';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat, EngineSchema } from '../../types';
import {
AzureMonitorErrorish,
AzureMonitorOption,
AzureMonitorQuery,
ResultFormat,
EngineSchema,
AzureLogAnalyticsMetadataTable,
} from '../../types';
import { LogsQueryBuilder } from '../LogsQueryBuilder/LogsQueryBuilder';
import ResourceField from '../ResourceField';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
@@ -60,6 +67,7 @@ const LogsQueryEditor = ({
const to = templateSrv?.replace('$__to');
const templateVariableOptions = templateSrv.getVariables();
const isBasicLogsQuery = (basicLogsEnabled && query.azureLogAnalytics?.basicLogsQuery) ?? false;
const [isLoadingSchema, setIsLoadingSchema] = useState<boolean>(false);
const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => {
if (selectedRows.length === 0) {
@@ -83,12 +91,34 @@ const LogsQueryEditor = ({
const [schema, setSchema] = useState<EngineSchema | undefined>();
useEffect(() => {
if (query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length) {
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]).then((schema) => {
setSchema(schema);
const resources = query.azureLogAnalytics?.resources;
if (resources) {
setIsLoadingSchema(true);
const fetchAllPlans = async (tables: AzureLogAnalyticsMetadataTable[]) => {
const promises = [];
for (const table of tables) {
promises.push({
...table,
plan: await datasource.azureMonitorDatasource.getWorkspaceTablePlan(resources, table.name),
});
}
const tablesWithPlan = await Promise.all(promises);
return tablesWithPlan;
};
datasource.azureLogAnalyticsDatasource.getKustoSchema(resources[0]).then((schema) => {
if (schema?.database?.tables) {
fetchAllPlans(schema?.database?.tables).then(async (t) => {
if (schema.database?.tables) {
schema.database.tables = t;
}
setSchema(schema);
});
}
setIsLoadingSchema(false);
});
}
}, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource]);
}, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource, datasource.azureMonitorDatasource]);
useEffect(() => {
if (shouldShowBasicLogsToggle(query.azureLogAnalytics?.resources || [], basicLogsEnabled)) {
@@ -254,6 +284,7 @@ const LogsQueryEditor = ({
templateVariableOptions={templateVariableOptions}
datasource={datasource}
timeRange={timeRange}
isLoadingSchema={isLoadingSchema}
/>
) : (
<QueryField
@@ -64,6 +64,7 @@ export const QueryHeader = ({
azureLogAnalytics: {
...query.azureLogAnalytics,
mode: LogsEditorMode.Builder,
dashboardTime: true,
},
};
onQueryChange(updatedQuery);
@@ -97,6 +98,7 @@ export const QueryHeader = ({
mode,
query: '',
builderQuery: mode === LogsEditorMode.Raw ? undefined : query.azureLogAnalytics?.builderQuery,
dashboardTime: mode === LogsEditorMode.Builder ? true : undefined,
},
};
onQueryChange(updatedQuery);
@@ -1,3 +1,5 @@
import { TablePlan } from './types';
export interface AzureLogAnalyticsMetadata {
functions: AzureLogAnalyticsMetadataFunction[];
resourceTypes: AzureLogAnalyticsMetadataResourceType[];
@@ -68,6 +70,8 @@ export interface AzureLogAnalyticsMetadataTable {
related: AzureLogAnalyticsMetadataTableRelated;
isTroubleshootingAllowed?: boolean;
hasData?: boolean;
// TablePlan does not come directly from the API - we determine it
plan?: TablePlan;
}
export interface AzureLogAnalyticsMetadataColumn {
@@ -1,6 +1,6 @@
import { ScalarParameter, TabularParameter, Function, EntityGroup } from '@kusto/monaco-kusto';
import { EntityGroup, Function, ScalarParameter, TabularParameter } from '@kusto/monaco-kusto';
import { AzureDataSourceSecureJsonData, AzureDataSourceJsonData } from '@grafana/azure-sdk';
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData } from '@grafana/azure-sdk';
import { DataSourceInstanceSettings, DataSourceSettings, PanelData, SelectableValue, TimeRange } from '@grafana/data';
import Datasource from '../datasource';
@@ -469,3 +469,60 @@ export enum AggregateFunctions {
Min = 'min',
Percentile = 'percentile',
}
export enum TablePlan {
Analytics = 'Analytics',
Basic = 'Basic',
}
export interface GetLogAnalyticsTableSuccessResponse {
properties: {
totalRetentionInDays: number;
archiveRetentionInDays: number;
lastPlanModifiedDate?: string;
plan: TablePlan;
restoredLogs?: Record<string, string | undefined>;
retentionInDaysAsDefault: boolean;
totalRetentionInDaysAsDefault: boolean;
schema: {
tableSubType: string;
name: string;
tableType: string;
columns: Array<Record<string, string | undefined>>;
standardColumns: Array<Record<string, string | undefined>>;
solutions: string[];
isTroubleshootingAllowed: boolean;
description?: string;
displayName?: string;
labels?: string[];
source?: string;
};
resultStatistics: Record<string, string | number | undefined>;
provisioningState: string;
retentionInDays: number;
searchResults?: Record<string, string | number | undefined>;
systemData?: Record<string, string | number | undefined>;
};
id: string;
name: string;
type?: string;
}
export interface GetLogAnalyticsTableErrorResponse {
error: {
target: string;
message: string;
code: string;
};
}
export type GetLogAnalyticsTableResponse = GetLogAnalyticsTableSuccessResponse | GetLogAnalyticsTableErrorResponse;
export function instanceOfLogAnalyticsTableError(
response: GetLogAnalyticsTableSuccessResponse | GetLogAnalyticsTableErrorResponse
): response is GetLogAnalyticsTableErrorResponse {
if (!response) {
return false;
}
return response.hasOwnProperty('error');
}