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:
+39
-6
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
+12
-2
@@ -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}
|
||||
|
||||
+12
-2
@@ -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 = [
|
||||
|
||||
+36
-5
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user