diff --git a/README.md b/README.md index e366af57b..9527d4449 100644 --- a/README.md +++ b/README.md @@ -75,5 +75,27 @@ Once the docker container started, navigate to http://localhost:3000/a/grafana-l In order to run the setup locally and build the plugin by your own, follow these steps: 1. `yarn install` -2. `yarn dev` this builds the plugin continously +2. `yarn dev` this builds the plugin continuously 3. `yarn server` this spins up the docker setup, including a Loki instance and the fake data generator + +## Supported Features + +This section outlines the supported features available by page: Service Selection and Service Detail. + +### Service Selection + +Service Selection is the entry step where users can choose a service. List of features and functionalities: + +**1. Fetching of services** - Services are fetched using the Loki [/loki/api/v1/index/volume](https://grafana.com/docs/loki/latest/reference/loki-http-api/#query-log-volume) endpoint and ordered by their volume. Services are re-fetched when the time range significantly changes to ensure correct data. Services are updated if: +- The time range scope changes (hours vs. days). +- The new time range is under 6 hours and the difference exceeds 30 minutes. +- The new time range is under 1 day and the difference exceeds 1 hour. +- The new time range is over 1 day and the difference exceeds 1 day. + +**2. Showing of services** - Services are shown based on volume and are lazy-loaded. Metrics and logs are queried only for services that are scrolled to. + +**3. Previously selected services** - Previously selected services are displayed at the top of the list for easier access. + +**4. Searching of services** - The search input can be used to filter services that include the specified string. + +### Service Details diff --git a/src/Components/ServiceSelectionScene/ConfigureVolumeError.tsx b/src/Components/ServiceSelectionScene/ConfigureVolumeError.tsx new file mode 100644 index 000000000..1a3e1dc5a --- /dev/null +++ b/src/Components/ServiceSelectionScene/ConfigureVolumeError.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { GrotError } from 'Components/GrotError'; +import { TextLink, Text } from '@grafana/ui'; + +export const ConfigureVolumeError = () => { + return ( + +

Log volume has not been configured.

+

+ + Instructions to enable volume in the Loki config: + +

+ +
+          
+            limits_config:
+            
+   volume_enabled: true +
+
+
+
+ ); +}; diff --git a/src/Components/ServiceSelectionScene/ServiceSelectionScene.tsx b/src/Components/ServiceSelectionScene/ServiceSelectionScene.tsx index ffa9f0de2..966d43672 100644 --- a/src/Components/ServiceSelectionScene/ServiceSelectionScene.tsx +++ b/src/Components/ServiceSelectionScene/ServiceSelectionScene.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import { debounce } from 'lodash'; import React, { useCallback, useState } from 'react'; -import { BusEventBase, GrafanaTheme2 } from '@grafana/data'; +import { BusEventBase, GrafanaTheme2, TimeRange } from '@grafana/data'; import { AdHocFiltersVariable, PanelBuilders, @@ -14,27 +14,17 @@ import { SceneVariable, VariableDependencyConfig, } from '@grafana/scenes'; -import { - DrawStyle, - Field, - Icon, - Input, - LoadingPlaceholder, - StackingMode, - Text, - TextLink, - useStyles2, -} from '@grafana/ui'; +import { DrawStyle, Field, Icon, Input, LoadingPlaceholder, StackingMode, useStyles2 } from '@grafana/ui'; import { getLokiDatasource } from 'services/scenes'; import { getFavoriteServicesFromStorage } from 'services/store'; import { testIds } from 'services/testIds'; import { LEVEL_VARIABLE_VALUE, VAR_DATASOURCE, VAR_FILTERS } from 'services/variables'; -import { GrotError } from '../GrotError'; import { SelectFieldButton } from './SelectFieldButton'; import { PLUGIN_ID } from 'services/routing'; import { buildLokiQuery } from 'services/query'; import { USER_EVENTS_ACTIONS, USER_EVENTS_PAGES, reportAppInteraction } from 'services/analytics'; import { getQueryRunner, setLeverColorOverrides } from 'services/panel'; +import { ConfigureVolumeError } from './ConfigureVolumeError'; export const SERVICE_NAME = 'service_name'; @@ -62,7 +52,8 @@ export class ServiceSelectionComponent extends SceneObjectBase { const { name } = variable.state; if (name === VAR_DATASOURCE) { - this._getServicesByVolume(); + // If datasource changes, we need to fetch services by volume for the new datasource + this.getServicesByVolume(); } }, }); @@ -88,15 +79,16 @@ export class ServiceSelectionComponent extends SceneObjectBase { - // Updates servicesToQuery when servicesByVolume is changed - should happen only once when the list of services is fetched during initialization + // Updates servicesToQuery when servicesByVolume is changed if (newState.servicesByVolume !== oldState.servicesByVolume) { - const ds = sceneGraph.lookupVariable(VAR_DATASOURCE, this)?.getValue(); - const servicesToQuery = createListOfServicesToQuery( - newState.servicesByVolume ?? [], - getFavoriteServicesFromStorage(ds) - ); + const ds = sceneGraph.lookupVariable(VAR_DATASOURCE, this)?.getValue()?.toString(); + let servicesToQuery: string[] = []; + if (ds && newState.servicesByVolume) { + servicesToQuery = createListOfServicesToQuery(newState.servicesByVolume, ds, this.state.searchServicesString); + } this.setState({ servicesToQuery, }); @@ -104,14 +96,10 @@ export class ServiceSelectionComponent extends SceneObjectBase - service.toLowerCase().includes(newState.searchServicesString?.toLowerCase() ?? '') - ); - let servicesToQuery = services ?? []; - // If user is not searching for anything, add favorite services to the top - if (newState.searchServicesString === '') { - const ds = sceneGraph.lookupVariable(VAR_DATASOURCE, this)?.getValue(); - servicesToQuery = createListOfServicesToQuery(servicesToQuery, getFavoriteServicesFromStorage(ds)); + const ds = sceneGraph.lookupVariable(VAR_DATASOURCE, this)?.getValue()?.toString(); + let servicesToQuery: string[] = []; + if (ds && this.state.servicesByVolume) { + servicesToQuery = createListOfServicesToQuery(this.state.servicesByVolume, ds, newState.searchServicesString); } this.setState({ servicesToQuery, @@ -123,10 +111,16 @@ export class ServiceSelectionComponent extends SceneObjectBase { + if (shouldUpdateServicesByVolume(newTime.value, oldTime.value)) { + this.getServicesByVolume(); + } + }); } - // Run on initialization to fetch list of services ordered by volume - private async _getServicesByVolume() { + // Run to fetch services by volume + private async getServicesByVolume() { const timeRange = sceneGraph.getTimeRange(this).state.value; this.setState({ isServicesByVolumeLoading: true, @@ -181,9 +175,10 @@ export class ServiceSelectionComponent extends SceneObjectBase= 4 && timeRange.to.diff(timeRange.from, 'hours') <= 26) { + splitDuration = '2h'; + } return new SceneCSSGridItem({ body: PanelBuilders.timeseries() // If service was previously selected, we show it in the title @@ -213,7 +212,7 @@ export class ServiceSelectionComponent extends SceneObjectBase
- {/** This is on top to show that we are loading Showing: X of X services div */} - {isServicesByVolumeLoading && } - {!isServicesByVolumeLoading && <>Showing {servicesToQuery?.length} services} + {/** When services fetched, show how many services are we showing */} + {isServicesByVolumeLoading && ( + + )} + {!isServicesByVolumeLoading && <>Showing {servicesToQuery?.length ?? 0} services}
- {isServicesByVolumeLoading && } {/** If we don't have any servicesByVolume, volume endpoint is probably not enabled */} - {!isServicesByVolumeLoading && !servicesByVolume?.length && ( - -

Log volume has not been configured.

-

- - Instructions to enable volume in the Loki config: - -

- -
-                  
-                    limits_config:
-                    
-   volume_enabled: true -
-
-
-
- )} + {!isServicesByVolumeLoading && !servicesByVolume?.length && } {!isServicesByVolumeLoading && servicesToQuery && servicesToQuery.length > 0 && (
@@ -318,14 +300,57 @@ export class ServiceSelectionComponent extends SceneObjectBase service.toLowerCase().includes(searchString.toLowerCase())); + const favoriteServicesToQuery = getFavoriteServicesFromStorage(ds).filter((service) => + service.toLowerCase().includes(searchString.toLowerCase()) + ); + + // Deduplicate + return Array.from(new Set([...favoriteServicesToQuery, ...servicesToQuery])); +} + +function shouldUpdateServicesByVolume(newTime: TimeRange, oldTime: TimeRange) { + // Update if the time range is not within the same scope (hours vs. days) + if (newTime.to.diff(newTime.from, 'days') > 1 !== oldTime.to.diff(oldTime.from, 'days') > 1) { + return true; + } + // Update if the time range is less than 6 hours and the difference between the old and new 'from' and 'to' times is greater than 30 minutes + if (newTime.to.diff(newTime.from, 'hours') < 6 && timeDiffBetweenRangesLargerThan(newTime, oldTime, 'minutes', 30)) { + return true; + } + // Update if the time range is less than 1 day and the difference between the old and new 'from' and 'to' times is greater than 1 hour + if (newTime.to.diff(newTime.from, 'days') < 1 && timeDiffBetweenRangesLargerThan(newTime, oldTime, 'hours', 1)) { + return true; + } + // Update if the time range is more than 1 day and the difference between the old and new 'from' and 'to' times is greater than 1 day + if (newTime.to.diff(newTime.from, 'days') > 1 && timeDiffBetweenRangesLargerThan(newTime, oldTime, 'days', 1)) { + return true; + } + + return false; +} + +// Helper function to check if difference between two time ranges is larger than value +function timeDiffBetweenRangesLargerThan( + newTimeRange: TimeRange, + oldTimeRange: TimeRange, + unit: 'minutes' | 'hours' | 'days', + value: number +) { + const toChange = + newTimeRange.to.diff(oldTimeRange.to, unit) > value || newTimeRange.to.diff(oldTimeRange.to, unit) < -value; + const fromChange = + newTimeRange.from.diff(oldTimeRange.from, unit) > value || newTimeRange.from.diff(oldTimeRange.from, unit) < -value; + return toChange || fromChange; } function getStyles(theme: GrafanaTheme2) { diff --git a/src/services/query.ts b/src/services/query.ts index 6f3fbb22c..74a237a97 100644 --- a/src/services/query.ts +++ b/src/services/query.ts @@ -7,6 +7,7 @@ export type LokiQuery = { supportingQueryType: string; expr: string; legendFormat?: string; + splitDuration?: string; }; export const buildLokiQuery = (expr: string, queryParamsOverrides?: Record): LokiQuery => { return {