mirror of https://github.com/grafana/grafana
Loki: Full range logs volume (#39327)
* Basic implementation of getLogsVolumeQuery method * Add todos * Add a switcher to automatically load logs volume * De-scope dismissing logs volume panel * De-scope logs volume query cancellation * Remove todo * Aggregate logs volume components in single panel * Show logs volume only when it's available * Aggregate logs volume by level * Simplify aggregation * Handle no logs volume data * Add error handling * Do not show auto-load logs volume switcher when loading logs volume is not available * Remove old logs volume graph * Clean up * Make getting data provider more generic * Provide complete logs volume data (error, isLoading) * Display more specific error message * Add missing props to mocks * Remove setRequest method * Mark getQueryRelatedDataProviders as internal * Add missing dataQueryRequest and add a todo * Remove redundant loading state * Do not mutate existing queries * Apply fix for zooming-in from main * Post-merge fixes * Create collection for data provider results * Use more generic names * Move aggregation logic to Loki logs volume provider * Move LogsVolume to common types * Update tests * Post-merge fixes * Fix mapping related data values * Simplify prop mappings * Add docs * Fix property name * Clean-up * Mark new types as internal * Reduce number of providers to logs volume only * Simplify data structure to DataQueryResponse * Move Logs Volume panel to a separate component * Test logsVolumeProvider.ts * Add observable version of datasource mock * Test getLogsVolumeDataProvider method * Test LogsVolumePanel * Test logs volume reducer * Clean up * Clean up * Fix test * Use sum by to use level field directly * Fix strict type errors * Fix strict type errors * Use "logs" instead of "unknown" if only one level was detected * Add docs about logs volume * Rename histogramRequest to logsVolumeRequest * Use LogsVolumeContentWrapper all content types * Move `autoLoadLogsVolume` local storage handling * Fix strict error * Move getting autoLoadLogsVolume to initial state * Cancel current logs volume subscription * Test cancelling subscriptions * Update docs/sources/datasources/loki.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update packages/grafana-data/src/types/explore.ts Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Inline container styles * Ensure logs volume is aggregated per each subscription separately * Simplify logs volume provider * Type-guard support for logs volume provider * Simplify event handlers to avoid casting * Clean up and docs * Move auto-load switcher to logs volume panel * Fix test * Move DataSourceWithLogsVolumeSupport to avoid cross referencing * Simplify interface * Bring back old histogram and hide the new one behind a feature flag * Add missing props to logs histogram panel * Clean up the provider when it's not supported * Simplify storing autoLoadLogsVolume * Remove docs * Update packages/grafana-data/src/types/logsVolume.ts Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com> * Skip dataframes without fields (instant queries) * Revert styles changes * Revert styles changes * Add release tag Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>pull/39862/head
parent
b7a68a9516
commit
124e9daf26
@ -0,0 +1,22 @@ |
|||||||
|
import { DataQuery } from './query'; |
||||||
|
import { DataQueryRequest, DataQueryResponse } from './datasource'; |
||||||
|
import { Observable } from 'rxjs'; |
||||||
|
|
||||||
|
/** |
||||||
|
* TODO: This should be added to ./logs.ts but because of cross reference between ./datasource.ts and ./logs.ts it can |
||||||
|
* be done only after decoupling "logs" from "datasource" (https://github.com/grafana/grafana/pull/39536)
|
||||||
|
* |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
export interface DataSourceWithLogsVolumeSupport<TQuery extends DataQuery> { |
||||||
|
getLogsVolumeDataProvider(request: DataQueryRequest<TQuery>): Observable<DataQueryResponse> | undefined; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
export const hasLogsVolumeSupport = <TQuery extends DataQuery>( |
||||||
|
datasource: any |
||||||
|
): datasource is DataSourceWithLogsVolumeSupport<TQuery> => { |
||||||
|
return (datasource as DataSourceWithLogsVolumeSupport<TQuery>).getLogsVolumeDataProvider !== undefined; |
||||||
|
}; |
@ -0,0 +1,56 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { render, screen } from '@testing-library/react'; |
||||||
|
import { LogsVolumePanel } from './LogsVolumePanel'; |
||||||
|
import { ExploreId } from '../../types'; |
||||||
|
import { DataQueryResponse, LoadingState } from '@grafana/data'; |
||||||
|
|
||||||
|
jest.mock('./ExploreGraph', () => { |
||||||
|
const ExploreGraph = () => <span>ExploreGraph</span>; |
||||||
|
return { |
||||||
|
ExploreGraph, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
function renderPanel(logsVolumeData?: DataQueryResponse) { |
||||||
|
render( |
||||||
|
<LogsVolumePanel |
||||||
|
exploreId={ExploreId.left} |
||||||
|
loadLogsVolumeData={() => {}} |
||||||
|
absoluteRange={{ from: 0, to: 1 }} |
||||||
|
timeZone="timeZone" |
||||||
|
splitOpen={() => {}} |
||||||
|
width={100} |
||||||
|
onUpdateTimeRange={() => {}} |
||||||
|
logsVolumeData={logsVolumeData} |
||||||
|
autoLoadLogsVolume={false} |
||||||
|
onChangeAutoLogsVolume={() => {}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
describe('LogsVolumePanel', () => { |
||||||
|
it('shows loading message', () => { |
||||||
|
renderPanel({ state: LoadingState.Loading, error: undefined, data: [] }); |
||||||
|
expect(screen.getByText('Logs volume is loading...')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows no volume data', () => { |
||||||
|
renderPanel({ state: LoadingState.Done, error: undefined, data: [] }); |
||||||
|
expect(screen.getByText('No volume data.')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('renders logs volume histogram graph', () => { |
||||||
|
renderPanel({ state: LoadingState.Done, error: undefined, data: [{}] }); |
||||||
|
expect(screen.getByText('ExploreGraph')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows error message', () => { |
||||||
|
renderPanel({ state: LoadingState.Error, error: { data: { message: 'Error message' } }, data: [] }); |
||||||
|
expect(screen.getByText('Failed to load volume logs for this query: Error message')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('shows button to load logs volume', () => { |
||||||
|
renderPanel(undefined); |
||||||
|
expect(screen.getByText('Load logs volume')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,114 @@ |
|||||||
|
import { AbsoluteTimeRange, DataQueryResponse, LoadingState, SplitOpen, TimeZone } from '@grafana/data'; |
||||||
|
import { Button, Collapse, InlineField, InlineFieldRow, InlineSwitch, useTheme2 } from '@grafana/ui'; |
||||||
|
import { ExploreGraph } from './ExploreGraph'; |
||||||
|
import React, { useCallback } from 'react'; |
||||||
|
import { ExploreId } from '../../types'; |
||||||
|
import { css } from '@emotion/css'; |
||||||
|
|
||||||
|
type Props = { |
||||||
|
exploreId: ExploreId; |
||||||
|
loadLogsVolumeData: (exploreId: ExploreId) => void; |
||||||
|
logsVolumeData?: DataQueryResponse; |
||||||
|
absoluteRange: AbsoluteTimeRange; |
||||||
|
timeZone: TimeZone; |
||||||
|
splitOpen: SplitOpen; |
||||||
|
width: number; |
||||||
|
onUpdateTimeRange: (timeRange: AbsoluteTimeRange) => void; |
||||||
|
autoLoadLogsVolume: boolean; |
||||||
|
onChangeAutoLogsVolume: (value: boolean) => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function LogsVolumePanel(props: Props) { |
||||||
|
const { |
||||||
|
width, |
||||||
|
logsVolumeData, |
||||||
|
exploreId, |
||||||
|
loadLogsVolumeData, |
||||||
|
absoluteRange, |
||||||
|
timeZone, |
||||||
|
splitOpen, |
||||||
|
onUpdateTimeRange, |
||||||
|
autoLoadLogsVolume, |
||||||
|
onChangeAutoLogsVolume, |
||||||
|
} = props; |
||||||
|
const theme = useTheme2(); |
||||||
|
const spacing = parseInt(theme.spacing(2).slice(0, -2), 10); |
||||||
|
const height = 150; |
||||||
|
|
||||||
|
let LogsVolumePanelContent; |
||||||
|
|
||||||
|
if (!logsVolumeData) { |
||||||
|
LogsVolumePanelContent = ( |
||||||
|
<Button |
||||||
|
onClick={() => { |
||||||
|
loadLogsVolumeData(exploreId); |
||||||
|
}} |
||||||
|
> |
||||||
|
Load logs volume |
||||||
|
</Button> |
||||||
|
); |
||||||
|
} else if (logsVolumeData?.error) { |
||||||
|
LogsVolumePanelContent = ( |
||||||
|
<span> |
||||||
|
Failed to load volume logs for this query:{' '} |
||||||
|
{logsVolumeData.error.data?.message || logsVolumeData.error.statusText} |
||||||
|
</span> |
||||||
|
); |
||||||
|
} else if (logsVolumeData?.state === LoadingState.Loading) { |
||||||
|
LogsVolumePanelContent = <span>Logs volume is loading...</span>; |
||||||
|
} else if (logsVolumeData?.data) { |
||||||
|
if (logsVolumeData.data.length > 0) { |
||||||
|
LogsVolumePanelContent = ( |
||||||
|
<ExploreGraph |
||||||
|
loadingState={LoadingState.Done} |
||||||
|
data={logsVolumeData.data} |
||||||
|
height={height} |
||||||
|
width={width - spacing} |
||||||
|
absoluteRange={absoluteRange} |
||||||
|
onChangeTime={onUpdateTimeRange} |
||||||
|
timeZone={timeZone} |
||||||
|
splitOpenFn={splitOpen} |
||||||
|
/> |
||||||
|
); |
||||||
|
} else { |
||||||
|
LogsVolumePanelContent = <span>No volume data.</span>; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleOnChangeAutoLogsVolume = useCallback( |
||||||
|
(event: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
const { target } = event; |
||||||
|
if (target) { |
||||||
|
onChangeAutoLogsVolume(target.checked); |
||||||
|
} |
||||||
|
}, |
||||||
|
[onChangeAutoLogsVolume] |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Collapse label="Logs volume" isOpen={true} loading={logsVolumeData?.state === LoadingState.Loading}> |
||||||
|
<div |
||||||
|
style={{ height }} |
||||||
|
className={css({ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center', |
||||||
|
})} |
||||||
|
> |
||||||
|
{LogsVolumePanelContent} |
||||||
|
</div> |
||||||
|
<div |
||||||
|
className={css({ |
||||||
|
display: 'flex', |
||||||
|
justifyContent: 'end', |
||||||
|
})} |
||||||
|
> |
||||||
|
<InlineFieldRow> |
||||||
|
<InlineField label="Auto-load logs volume" transparent> |
||||||
|
<InlineSwitch value={autoLoadLogsVolume} onChange={handleOnChangeAutoLogsVolume} transparent /> |
||||||
|
</InlineField> |
||||||
|
</InlineFieldRow> |
||||||
|
</div> |
||||||
|
</Collapse> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,107 @@ |
|||||||
|
import { MockObservableDataSourceApi } from '../../../../../test/mocks/datasource_srv'; |
||||||
|
import { createLokiLogsVolumeProvider } from './logsVolumeProvider'; |
||||||
|
import LokiDatasource from '../datasource'; |
||||||
|
import { DataQueryRequest, DataQueryResponse, FieldType, LoadingState, toDataFrame } from '@grafana/data'; |
||||||
|
import { LokiQuery } from '../types'; |
||||||
|
import { Observable } from 'rxjs'; |
||||||
|
|
||||||
|
function createFrame(labels: object, timestamps: number[], values: number[]) { |
||||||
|
return toDataFrame({ |
||||||
|
fields: [ |
||||||
|
{ name: 'Time', type: FieldType.time, values: timestamps }, |
||||||
|
{ |
||||||
|
name: 'Number', |
||||||
|
type: FieldType.number, |
||||||
|
values, |
||||||
|
labels, |
||||||
|
}, |
||||||
|
], |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function createExpectedFields(levelName: string, timestamps: number[], values: number[]) { |
||||||
|
return [ |
||||||
|
{ name: 'Time', values: { buffer: timestamps } }, |
||||||
|
{ |
||||||
|
name: 'Value', |
||||||
|
config: { displayNameFromDS: levelName }, |
||||||
|
values: { buffer: values }, |
||||||
|
}, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
describe('LokiLogsVolumeProvider', () => { |
||||||
|
let volumeProvider: Observable<DataQueryResponse>, |
||||||
|
datasource: MockObservableDataSourceApi, |
||||||
|
request: DataQueryRequest<LokiQuery>; |
||||||
|
|
||||||
|
function setup(datasourceSetup: () => void) { |
||||||
|
datasourceSetup(); |
||||||
|
request = ({ |
||||||
|
targets: [{ expr: '{app="app01"}' }, { expr: '{app="app02"}' }], |
||||||
|
} as unknown) as DataQueryRequest<LokiQuery>; |
||||||
|
volumeProvider = createLokiLogsVolumeProvider((datasource as unknown) as LokiDatasource, request); |
||||||
|
} |
||||||
|
|
||||||
|
function setupMultipleResults() { |
||||||
|
// level=unknown
|
||||||
|
const resultAFrame1 = createFrame({ app: 'app01' }, [100, 200, 300], [5, 5, 5]); |
||||||
|
// level=error
|
||||||
|
const resultAFrame2 = createFrame({ app: 'app01', level: 'error' }, [100, 200, 300], [0, 1, 0]); |
||||||
|
// level=unknown
|
||||||
|
const resultBFrame1 = createFrame({ app: 'app02' }, [100, 200, 300], [1, 2, 3]); |
||||||
|
// level=error
|
||||||
|
const resultBFrame2 = createFrame({ app: 'app02', level: 'error' }, [100, 200, 300], [1, 1, 1]); |
||||||
|
|
||||||
|
datasource = new MockObservableDataSourceApi('loki', [ |
||||||
|
{ |
||||||
|
data: [resultAFrame1, resultAFrame2], |
||||||
|
}, |
||||||
|
{ |
||||||
|
data: [resultBFrame1, resultBFrame2], |
||||||
|
}, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
function setupErrorResponse() { |
||||||
|
datasource = new MockObservableDataSourceApi('loki', [], undefined, 'Error message'); |
||||||
|
} |
||||||
|
|
||||||
|
it('aggregates data frames by level', async () => { |
||||||
|
setup(setupMultipleResults); |
||||||
|
|
||||||
|
await expect(volumeProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ state: LoadingState.Loading, error: undefined, data: [] }, |
||||||
|
{ |
||||||
|
state: LoadingState.Done, |
||||||
|
error: undefined, |
||||||
|
data: [ |
||||||
|
{ |
||||||
|
fields: createExpectedFields('unknown', [100, 200, 300], [6, 7, 8]), |
||||||
|
}, |
||||||
|
{ |
||||||
|
fields: createExpectedFields('error', [100, 200, 300], [1, 2, 1]), |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('returns error', async () => { |
||||||
|
setup(setupErrorResponse); |
||||||
|
|
||||||
|
await expect(volumeProvider).toEmitValuesWith((received) => { |
||||||
|
expect(received).toMatchObject([ |
||||||
|
{ state: LoadingState.Loading, error: undefined, data: [] }, |
||||||
|
{ |
||||||
|
state: LoadingState.Error, |
||||||
|
error: 'Error message', |
||||||
|
data: [], |
||||||
|
}, |
||||||
|
'Error message', |
||||||
|
]); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,175 @@ |
|||||||
|
import { |
||||||
|
DataFrame, |
||||||
|
DataQueryRequest, |
||||||
|
DataQueryResponse, |
||||||
|
FieldCache, |
||||||
|
FieldColorModeId, |
||||||
|
FieldConfig, |
||||||
|
FieldType, |
||||||
|
getLogLevelFromKey, |
||||||
|
Labels, |
||||||
|
LoadingState, |
||||||
|
LogLevel, |
||||||
|
MutableDataFrame, |
||||||
|
toDataFrame, |
||||||
|
} from '@grafana/data'; |
||||||
|
import { LokiQuery } from '../types'; |
||||||
|
import { Observable } from 'rxjs'; |
||||||
|
import { cloneDeep } from 'lodash'; |
||||||
|
import LokiDatasource, { isMetricsQuery } from '../datasource'; |
||||||
|
import { LogLevelColor } from '../../../../core/logs_model'; |
||||||
|
import { BarAlignment, GraphDrawStyle, StackingMode } from '@grafana/schema'; |
||||||
|
|
||||||
|
export function createLokiLogsVolumeProvider( |
||||||
|
datasource: LokiDatasource, |
||||||
|
dataQueryRequest: DataQueryRequest<LokiQuery> |
||||||
|
): Observable<DataQueryResponse> { |
||||||
|
const logsVolumeRequest = cloneDeep(dataQueryRequest); |
||||||
|
logsVolumeRequest.targets = logsVolumeRequest.targets |
||||||
|
.filter((target) => target.expr && !isMetricsQuery(target.expr)) |
||||||
|
.map((target) => { |
||||||
|
return { |
||||||
|
...target, |
||||||
|
expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
return new Observable((observer) => { |
||||||
|
let rawLogsVolume: DataFrame[] = []; |
||||||
|
observer.next({ |
||||||
|
state: LoadingState.Loading, |
||||||
|
error: undefined, |
||||||
|
data: [], |
||||||
|
}); |
||||||
|
|
||||||
|
const subscription = datasource.query(logsVolumeRequest).subscribe({ |
||||||
|
complete: () => { |
||||||
|
const aggregatedLogsVolume = aggregateRawLogsVolume(rawLogsVolume); |
||||||
|
observer.next({ |
||||||
|
state: LoadingState.Done, |
||||||
|
error: undefined, |
||||||
|
data: aggregatedLogsVolume, |
||||||
|
}); |
||||||
|
observer.complete(); |
||||||
|
}, |
||||||
|
next: (dataQueryResponse: DataQueryResponse) => { |
||||||
|
rawLogsVolume = rawLogsVolume.concat(dataQueryResponse.data.map(toDataFrame)); |
||||||
|
}, |
||||||
|
error: (error) => { |
||||||
|
observer.next({ |
||||||
|
state: LoadingState.Error, |
||||||
|
error: error, |
||||||
|
data: [], |
||||||
|
}); |
||||||
|
observer.error(error); |
||||||
|
}, |
||||||
|
}); |
||||||
|
return () => { |
||||||
|
subscription?.unsubscribe(); |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add up values for the same level and create a single data frame for each level |
||||||
|
*/ |
||||||
|
function aggregateRawLogsVolume(rawLogsVolume: DataFrame[]): DataFrame[] { |
||||||
|
const logsVolumeByLevelMap: { [level in LogLevel]?: DataFrame[] } = {}; |
||||||
|
let levels = 0; |
||||||
|
rawLogsVolume.forEach((dataFrame) => { |
||||||
|
let valueField; |
||||||
|
try { |
||||||
|
valueField = new FieldCache(dataFrame).getFirstFieldOfType(FieldType.number); |
||||||
|
} catch {} |
||||||
|
// If value field doesn't exist skip the frame (it may happen with instant queries)
|
||||||
|
if (!valueField) { |
||||||
|
return; |
||||||
|
} |
||||||
|
const level: LogLevel = valueField.labels ? getLogLevelFromLabels(valueField.labels) : LogLevel.unknown; |
||||||
|
if (!logsVolumeByLevelMap[level]) { |
||||||
|
logsVolumeByLevelMap[level] = []; |
||||||
|
levels++; |
||||||
|
} |
||||||
|
logsVolumeByLevelMap[level]!.push(dataFrame); |
||||||
|
}); |
||||||
|
|
||||||
|
return Object.keys(logsVolumeByLevelMap).map((level: string) => { |
||||||
|
return aggregateFields(logsVolumeByLevelMap[level as LogLevel]!, getFieldConfig(level as LogLevel, levels)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
function getFieldConfig(level: LogLevel, levels: number) { |
||||||
|
const name = levels === 1 && level === LogLevel.unknown ? 'logs' : level; |
||||||
|
const color = LogLevelColor[level]; |
||||||
|
return { |
||||||
|
displayNameFromDS: name, |
||||||
|
color: { |
||||||
|
mode: FieldColorModeId.Fixed, |
||||||
|
fixedColor: color, |
||||||
|
}, |
||||||
|
custom: { |
||||||
|
drawStyle: GraphDrawStyle.Bars, |
||||||
|
barAlignment: BarAlignment.Center, |
||||||
|
barWidthFactor: 0.9, |
||||||
|
barMaxWidth: 5, |
||||||
|
lineColor: color, |
||||||
|
pointColor: color, |
||||||
|
fillColor: color, |
||||||
|
lineWidth: 1, |
||||||
|
fillOpacity: 100, |
||||||
|
stacking: { |
||||||
|
mode: StackingMode.Normal, |
||||||
|
group: 'A', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new data frame with a single field and values creating by adding field values |
||||||
|
* from all provided data frames |
||||||
|
*/ |
||||||
|
function aggregateFields(dataFrames: DataFrame[], config: FieldConfig): DataFrame { |
||||||
|
const aggregatedDataFrame = new MutableDataFrame(); |
||||||
|
if (!dataFrames.length) { |
||||||
|
return aggregatedDataFrame; |
||||||
|
} |
||||||
|
|
||||||
|
const totalLength = dataFrames[0].length; |
||||||
|
const timeField = new FieldCache(dataFrames[0]).getFirstFieldOfType(FieldType.time); |
||||||
|
|
||||||
|
if (!timeField) { |
||||||
|
return aggregatedDataFrame; |
||||||
|
} |
||||||
|
|
||||||
|
aggregatedDataFrame.addField({ name: 'Time', type: FieldType.time }, totalLength); |
||||||
|
aggregatedDataFrame.addField({ name: 'Value', type: FieldType.number, config }, totalLength); |
||||||
|
|
||||||
|
dataFrames.forEach((dataFrame) => { |
||||||
|
dataFrame.fields.forEach((field) => { |
||||||
|
if (field.type === FieldType.number) { |
||||||
|
for (let pointIndex = 0; pointIndex < totalLength; pointIndex++) { |
||||||
|
const currentValue = aggregatedDataFrame.get(pointIndex).Value; |
||||||
|
const valueToAdd = field.values.get(pointIndex); |
||||||
|
const totalValue = |
||||||
|
currentValue === null && valueToAdd === null ? null : (currentValue || 0) + (valueToAdd || 0); |
||||||
|
aggregatedDataFrame.set(pointIndex, { Value: totalValue, Time: timeField.values.get(pointIndex) }); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
return aggregatedDataFrame; |
||||||
|
} |
||||||
|
|
||||||
|
function getLogLevelFromLabels(labels: Labels): LogLevel { |
||||||
|
const labelNames = ['level', 'lvl', 'loglevel']; |
||||||
|
let levelLabel; |
||||||
|
for (let labelName of labelNames) { |
||||||
|
if (labelName in labels) { |
||||||
|
levelLabel = labelName; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return levelLabel ? getLogLevelFromKey(labels[levelLabel]) : LogLevel.unknown; |
||||||
|
} |
Loading…
Reference in new issue