Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ServiceSelectionScene: Manual query runners #868

Merged
merged 11 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -489,3 +489,4 @@ primarylabels
Shortlink
uninterpolated
Retriable
Descendents
185 changes: 134 additions & 51 deletions src/Components/ServiceSelectionScene/ServiceSelectionScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { ServiceSelectionTabsScene } from './ServiceSelectionTabsScene';
import { FavoriteServiceHeaderActionScene } from './FavoriteServiceHeaderActionScene';
import { pushUrlHandler } from '../../services/navigate';
import { NoServiceVolume } from './NoServiceVolume';
import { getQueryRunnerFromChildren } from '../../services/scenes';

const aggregatedMetricsEnabled: boolean | undefined = config.featureToggles.exploreLogsAggregatedMetrics;
// Don't export AGGREGATED_SERVICE_NAME, we want to rename things so the rest of the application is agnostic to how we got the services
Expand Down Expand Up @@ -371,10 +372,6 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
return getServiceSelectionPrimaryLabel(this).state.filters[0]?.key;
}

getSelectedTabLabel() {
return getServiceSelectionPrimaryLabel(this).state.filters[0].key;
}

selectDefaultLabelTab() {
// Need to update the history before the state with replace instead of push, or we'll get invalid services saved to url state after changing datasource
this.addLabelChangeToBrowserHistory(SERVICE_NAME, true);
Expand Down Expand Up @@ -408,13 +405,16 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
// If service was previously selected, we show it in the title
.setTitle(primaryLabelValue)
.setData(
getQueryRunner([
buildDataQuery(this.getMetricExpression(primaryLabelValue, serviceLabelVar, primaryLabelVar), {
legendFormat: `{{${LEVEL_VARIABLE_VALUE}}}`,
splitDuration,
refId: `ts-${primaryLabelValue}`,
}),
])
getQueryRunner(
[
buildDataQuery(this.getMetricExpression(primaryLabelValue, serviceLabelVar, primaryLabelVar), {
legendFormat: `{{${LEVEL_VARIABLE_VALUE}}}`,
splitDuration,
refId: `ts-${primaryLabelValue}`,
}),
],
{ runQueriesMode: 'manual' }
)
)
.setCustomFieldConfig('stacking', { mode: StackingMode.Normal })
.setCustomFieldConfig('fillOpacity', 100)
Expand Down Expand Up @@ -444,10 +444,16 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
this.extendTimeSeriesLegendBus(primaryLabelName, primaryLabelValue, context, panel),
});

return new SceneCSSGridItem({
const cssGridItem = new SceneCSSGridItem({
$behaviors: [new behaviors.CursorSync({ key: 'serviceCrosshairSync', sync: DashboardCursorSync.Crosshair })],
body: panel,
});

cssGridItem.addActivationHandler(() => {
this.runPanelQuery(cssGridItem);
});

return cssGridItem;
}

isAggregatedMetricsActive() {
Expand All @@ -472,24 +478,35 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
// Creates a layout with logs panel
buildServiceLogsLayout = (labelName: string, labelValue: string) => {
const levelFilter = this.getLevelFilterForService(labelValue);
return new SceneCSSGridItem({
const cssGridItem = new SceneCSSGridItem({
$behaviors: [new behaviors.CursorSync({ sync: DashboardCursorSync.Off })],
body: PanelBuilders.logs()
// Hover header set to true removes unused header padding, displaying more logs
.setHoverHeader(true)
.setData(
getQueryRunner([
buildDataQuery(this.getLogExpression(labelName, labelValue, levelFilter), {
maxLines: 100,
refId: `logs-${labelValue}`,
}),
])
getQueryRunner(
[
buildDataQuery(this.getLogExpression(labelName, labelValue, levelFilter), {
maxLines: 100,
refId: `logs-${labelValue}`,
}),
],
{
runQueriesMode: 'manual',
}
)
)
.setTitle(labelValue)
.setOption('showTime', true)
.setOption('enableLogDetails', false)
.build(),
});

cssGridItem.addActivationHandler(() => {
this.runPanelQuery(cssGridItem);
});

return cssGridItem;
};

formatPrimaryLabelForUI() {
Expand Down Expand Up @@ -521,22 +538,13 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
if (newState.filterExpression !== prevState.filterExpression) {
const newKey = newState.filters[0].key;
this.addLabelChangeToBrowserHistory(newKey);
// Will need to tear down volume instance when we introduce the ability to select other labels, as we'll need to pass the selected tab to parse the volume response
this.runVolumeQuery();
}
})
);

this._subs.add(
this.state.$data.subscribeToState((newState, prevState) => {
// update body if the data is done loading, and the dataframes have changed
if (
newState.data?.state === LoadingState.Done &&
!areArraysEqual(prevState?.data?.series, newState?.data?.series)
) {
this.updateBody();
}
})
);
this.subscribeToVolume();

if (this.isTimeRangeTooEarlyForAggMetrics()) {
this.onUnsupportedAggregatedMetricTimeRange();
Expand All @@ -551,16 +559,7 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
}

// Update labels on time range change
this._subs.add(
sceneGraph.getTimeRange(this).subscribeToState(() => {
if (this.isTimeRangeTooEarlyForAggMetrics()) {
this.onUnsupportedAggregatedMetricTimeRange();
} else {
this.onSupportedAggregatedMetricTimeRange();
}
this.runVolumeQuery();
})
);
this.subscribeToTimeRange();

// Update labels on datasource change
this._subs.add(
Expand All @@ -573,12 +572,30 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
this._subs.add(
this.getQueryOptionsToolbar()?.subscribeToState((newState, prevState) => {
if (newState.options.aggregatedMetrics.userOverride !== prevState.options.aggregatedMetrics.userOverride) {
this.runVolumeQuery();
this.runVolumeQuery(true);
}
})
);

// agg metrics need parser and unwrap, have to tear down and rebuild panels when the variable changes
this.subscribeToAggregatedMetricVariable();
}

private setVolumeQueryRunner() {
this.setState({
$data: getSceneQueryRunner({
queries: [buildResourceQuery(`{${VAR_PRIMARY_LABEL_EXPR}}`, 'volume')],
runQueriesMode: 'manual',
}),
});

this.subscribeToVolume();
}

/**
* agg metrics need parser and unwrap, have to tear down and rebuild panels when the variable changes
* @private
*/
private subscribeToAggregatedMetricVariable() {
this._subs.add(
getAggregatedMetricsVariable(this).subscribeToState((newState, prevState) => {
if (newState.value !== prevState.value) {
Expand All @@ -587,8 +604,35 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
body: new SceneCSSGridLayout({ children: [] }),
});
// And re-init with the new query
this.updateBody();
this.updateBody(true);
}
})
);
}

private subscribeToVolume() {
this._subs.add(
this.state.$data.subscribeToState((newState, prevState) => {
// update body if the data is done loading, and the dataframes have changed
if (
newState.data?.state === LoadingState.Done &&
!areArraysEqual(prevState?.data?.series, newState?.data?.series)
) {
this.updateBody(true);
}
})
);
}

private subscribeToTimeRange() {
this._subs.add(
sceneGraph.getTimeRange(this).subscribeToState(() => {
if (this.isTimeRangeTooEarlyForAggMetrics()) {
this.onUnsupportedAggregatedMetricTimeRange();
} else {
this.onSupportedAggregatedMetricTimeRange();
}
this.runVolumeQuery();
})
);
}
Expand Down Expand Up @@ -654,7 +698,16 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
return input;
}

private runVolumeQuery() {
/**
* Executes the Volume API call
* @param resetQueryRunner - optional param which will replace the query runner state with a new instantiation
* @private
*/
private runVolumeQuery(resetQueryRunner = false) {
if (resetQueryRunner) {
this.setVolumeQueryRunner();
}

this.updateAggregatedMetricVariable();
this.state.$data.runQueries();
}
Expand All @@ -677,7 +730,30 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
}
}

private updateBody() {
private getGridItems(): SceneCSSGridItem[] {
return this.state.body.state.children as SceneCSSGridItem[];
}

private getVizPanel(child: SceneCSSGridItem) {
return child.state.body instanceof VizPanel ? child.state.body : undefined;
}

/**
* Runs logs/volume panel queries if lazy loaded grid item is active
* @param child
* @private
*/
private runPanelQuery(child: SceneCSSGridItem) {
if (child.isActive) {
const queryRunners = getQueryRunnerFromChildren(child);
if (queryRunners.length === 1) {
const queryRunner = queryRunners[0];
queryRunner.runQueries();
}
}
}

private updateBody(runQueries = false) {
const { labelsToQuery } = this.getLabels(this.state.$data.state.data?.series);
this.updateTabs();
// If no services are to be queried, clear the body
Expand All @@ -695,7 +771,7 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene

for (const primaryLabelValue of labelsToQuery.slice(0, SERVICES_LIMIT)) {
const existing = existingChildren.filter((child) => {
const vizPanel = child.state.body as VizPanel | undefined;
const vizPanel = this.getVizPanel(child);
return vizPanel?.state.title === primaryLabelValue;
});

Expand All @@ -718,6 +794,13 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
}
}

// Run queries for active panels
newChildren.forEach((child) => {
if (child.isActive && runQueries) {
this.runPanelQuery(child);
}
});

this.state.body.setState({
children: newChildren,
isLazy: true,
Expand Down Expand Up @@ -745,11 +828,11 @@ export class ServiceSelectionScene extends SceneObjectBase<ServiceSelectionScene
if (serviceIndex === undefined || serviceIndex < 0) {
return;
}
if (this.state.body) {
let newChildren = [...this.state.body.state.children];
newChildren.splice(serviceIndex * 2 + 1, 1, this.buildServiceLogsLayout(labelName, labelValue));
this.state.body.setState({ children: newChildren });
}
// if (this.state.body) {
let newChildren = [...this.getGridItems()];
newChildren.splice(serviceIndex * 2 + 1, 1, this.buildServiceLogsLayout(labelName, labelValue));
this.state.body.setState({ children: newChildren });
// }
}

private getLogExpression(labelName: string, labelValue: string, levelFilter: string) {
Expand Down
1 change: 1 addition & 0 deletions src/services/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function getQueryRunner(queries: LokiQuery[], queryRunnerOptions?: Partia
$data: getSceneQueryRunner({
datasource: { uid: WRAPPED_LOKI_DS_UID },
queries: queries,
...queryRunnerOptions,
}),
transformations: [sortLevelTransformation],
});
Expand Down
6 changes: 5 additions & 1 deletion src/services/scenes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { urlUtil } from '@grafana/data';
import { config, DataSourceWithBackend, getDataSourceSrv } from '@grafana/runtime';
import { sceneGraph, SceneObject, SceneObjectUrlValues } from '@grafana/scenes';
import { sceneGraph, SceneObject, SceneObjectUrlValues, SceneQueryRunner } from '@grafana/scenes';
import { LOG_STREAM_SELECTOR_EXPR, VAR_DATASOURCE_EXPR, VAR_LABELS_EXPR } from './variables';
import { EXPLORATIONS_ROUTE } from './routing';
import { IndexScene } from 'Components/IndexScene/IndexScene';
Expand Down Expand Up @@ -40,3 +40,7 @@ export async function getLokiDatasource(sceneObject: SceneObject) {
export function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}

export function getQueryRunnerFromChildren(sceneObject: SceneObject) {
return sceneGraph.findDescendents(sceneObject, SceneQueryRunner);
}
8 changes: 4 additions & 4 deletions tests/exploreServices.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect } from '@grafana/plugin-e2e';
import { ExplorePage } from './fixtures/explore';
import { testIds } from '../src/services/testIds';
import { mockVolumeApiResponse } from './mocks/mockVolumeApiResponse';
import { getMockVolumeApiResponse } from './mocks/getMockVolumeApiResponse';
import { isNumber } from 'lodash';
import { Page } from '@playwright/test';

Expand Down Expand Up @@ -137,15 +137,15 @@ test.describe('explore services page', () => {
logsQueryCount = 0;

await page.route('**/index/volume*', async (route) => {
const volumeResponse = mockVolumeApiResponse;
const volumeResponse = getMockVolumeApiResponse();
logsVolumeCount++;
await page.waitForTimeout(25);
await page.waitForTimeout(15);
await route.fulfill({ json: volumeResponse });
});

await page.route('**/ds/query*', async (route) => {
logsQueryCount++;
await page.waitForTimeout(50);
await page.waitForTimeout(30);
await route.fulfill({ json: {} });
});

Expand Down
Loading
Loading