mirror of https://github.com/grafana/grafana
Live: remove measurement controller (#32622)
parent
db12818d25
commit
d2afcdd415
@ -0,0 +1,17 @@ |
|||||||
|
import { LiveChannelScope, parseLiveChannelAddress } from './live'; |
||||||
|
|
||||||
|
describe('parse address', () => { |
||||||
|
it('simple address', () => { |
||||||
|
const addr = parseLiveChannelAddress('plugin/testdata/random-flakey-stream'); |
||||||
|
expect(addr?.scope).toBe(LiveChannelScope.Plugin); |
||||||
|
expect(addr?.namespace).toBe('testdata'); |
||||||
|
expect(addr?.path).toBe('random-flakey-stream'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('suppors full path', () => { |
||||||
|
const addr = parseLiveChannelAddress('plugin/testdata/a/b/c/d '); |
||||||
|
expect(addr?.scope).toBe(LiveChannelScope.Plugin); |
||||||
|
expect(addr?.namespace).toBe('testdata'); |
||||||
|
expect(addr?.path).toBe('a/b/c/d'); |
||||||
|
}); |
||||||
|
}); |
@ -1,77 +0,0 @@ |
|||||||
import { FieldType } from '@grafana/data'; |
|
||||||
import { MeasurementCollector } from './collector'; |
|
||||||
|
|
||||||
describe('MeasurementCollector', () => { |
|
||||||
it('should collect values', () => { |
|
||||||
const collector = new MeasurementCollector(); |
|
||||||
collector.addBatch({ |
|
||||||
batch: [ |
|
||||||
{ |
|
||||||
key: 'aaa', |
|
||||||
schema: { |
|
||||||
fields: [ |
|
||||||
{ name: 'time', type: FieldType.time }, |
|
||||||
{ name: 'value', type: FieldType.number }, |
|
||||||
], |
|
||||||
}, |
|
||||||
data: { |
|
||||||
values: [ |
|
||||||
[100, 200], |
|
||||||
[1, 2], |
|
||||||
], |
|
||||||
}, |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: 'aaa', |
|
||||||
data: { values: [[300], [3]] }, |
|
||||||
}, |
|
||||||
{ |
|
||||||
key: 'aaa', |
|
||||||
data: { values: [[400], [4]] }, |
|
||||||
}, |
|
||||||
], |
|
||||||
}); |
|
||||||
|
|
||||||
const frames = collector.getData(); |
|
||||||
expect(frames.length).toEqual(1); |
|
||||||
expect(frames[0]).toMatchInlineSnapshot(` |
|
||||||
StreamingDataFrame { |
|
||||||
"fields": Array [ |
|
||||||
Object { |
|
||||||
"config": Object {}, |
|
||||||
"labels": undefined, |
|
||||||
"name": "time", |
|
||||||
"type": "time", |
|
||||||
"values": Array [ |
|
||||||
100, |
|
||||||
200, |
|
||||||
300, |
|
||||||
400, |
|
||||||
], |
|
||||||
}, |
|
||||||
Object { |
|
||||||
"config": Object {}, |
|
||||||
"labels": undefined, |
|
||||||
"name": "value", |
|
||||||
"type": "number", |
|
||||||
"values": Array [ |
|
||||||
1, |
|
||||||
2, |
|
||||||
3, |
|
||||||
4, |
|
||||||
], |
|
||||||
}, |
|
||||||
], |
|
||||||
"length": 4, |
|
||||||
"meta": undefined, |
|
||||||
"name": undefined, |
|
||||||
"options": Object { |
|
||||||
"maxDelta": Infinity, |
|
||||||
"maxLength": 600, |
|
||||||
}, |
|
||||||
"refId": undefined, |
|
||||||
"timeFieldIndex": 0, |
|
||||||
} |
|
||||||
`);
|
|
||||||
}); |
|
||||||
}); |
|
@ -1,86 +0,0 @@ |
|||||||
import { DataFrame, DataFrameJSON, StreamingDataFrame, StreamingFrameOptions } from '@grafana/data'; |
|
||||||
import { MeasurementBatch, LiveMeasurements, MeasurementsQuery } from './types'; |
|
||||||
|
|
||||||
/** |
|
||||||
* This will collect |
|
||||||
* |
|
||||||
* @alpha -- experimental |
|
||||||
*/ |
|
||||||
export class MeasurementCollector implements LiveMeasurements { |
|
||||||
measurements = new Map<string, StreamingDataFrame>(); |
|
||||||
config: StreamingFrameOptions = { |
|
||||||
maxLength: 600, // Default capacity 10min @ 1hz
|
|
||||||
}; |
|
||||||
|
|
||||||
//------------------------------------------------------
|
|
||||||
// Public
|
|
||||||
//------------------------------------------------------
|
|
||||||
|
|
||||||
getData(query?: MeasurementsQuery): DataFrame[] { |
|
||||||
const { key, fields } = query || {}; |
|
||||||
|
|
||||||
// Find the data
|
|
||||||
let data: StreamingDataFrame[] = []; |
|
||||||
if (key) { |
|
||||||
const f = this.measurements.get(key); |
|
||||||
if (!f) { |
|
||||||
return []; |
|
||||||
} |
|
||||||
data.push(f); |
|
||||||
} else { |
|
||||||
// Add all frames
|
|
||||||
for (const f of this.measurements.values()) { |
|
||||||
data.push(f); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Filter the fields we want
|
|
||||||
if (fields && fields.length) { |
|
||||||
let filtered: DataFrame[] = []; |
|
||||||
for (const frame of data) { |
|
||||||
const match = frame.fields.filter((f) => fields.includes(f.name)); |
|
||||||
if (match.length > 0) { |
|
||||||
filtered.push({ ...frame, fields: match, length: frame.length }); // Copy the frame with fewer fields
|
|
||||||
} |
|
||||||
} |
|
||||||
if (filtered.length) { |
|
||||||
return filtered; |
|
||||||
} |
|
||||||
} |
|
||||||
return data; |
|
||||||
} |
|
||||||
|
|
||||||
getKeys(): string[] { |
|
||||||
return Object.keys(this.measurements); |
|
||||||
} |
|
||||||
|
|
||||||
ensureCapacity(size: number) { |
|
||||||
// TODO...
|
|
||||||
} |
|
||||||
|
|
||||||
//------------------------------------------------------
|
|
||||||
// Collector
|
|
||||||
//------------------------------------------------------
|
|
||||||
|
|
||||||
addBatch = (msg: MeasurementBatch) => { |
|
||||||
// HACK! sending one message from the backend, not a batch
|
|
||||||
if (!msg.batch) { |
|
||||||
const df: DataFrameJSON = msg as any; |
|
||||||
msg = { batch: [df] }; |
|
||||||
console.log('NOTE converting message to batch'); |
|
||||||
} |
|
||||||
|
|
||||||
for (const measure of msg.batch) { |
|
||||||
const key = measure.key ?? measure.schema?.name ?? ''; |
|
||||||
|
|
||||||
let s = this.measurements.get(key); |
|
||||||
if (s) { |
|
||||||
s.push(measure); |
|
||||||
} else { |
|
||||||
s = new StreamingDataFrame(measure, this.config); //
|
|
||||||
this.measurements.set(key, s); |
|
||||||
} |
|
||||||
} |
|
||||||
return this; |
|
||||||
}; |
|
||||||
} |
|
@ -1,3 +1 @@ |
|||||||
export * from './types'; |
|
||||||
export * from './collector'; |
|
||||||
export * from './query'; |
export * from './query'; |
||||||
|
@ -1,77 +1,114 @@ |
|||||||
import { |
import { |
||||||
|
DataFrame, |
||||||
|
DataFrameJSON, |
||||||
DataQueryResponse, |
DataQueryResponse, |
||||||
isLiveChannelMessageEvent, |
isLiveChannelMessageEvent, |
||||||
isLiveChannelStatusEvent, |
isLiveChannelStatusEvent, |
||||||
isValidLiveChannelAddress, |
isValidLiveChannelAddress, |
||||||
LiveChannelAddress, |
LiveChannelAddress, |
||||||
|
LiveChannelConnectionState, |
||||||
|
LiveChannelEvent, |
||||||
LoadingState, |
LoadingState, |
||||||
|
StreamingDataFrame, |
||||||
|
StreamingFrameOptions, |
||||||
} from '@grafana/data'; |
} from '@grafana/data'; |
||||||
import { LiveMeasurements, MeasurementsQuery } from './types'; |
|
||||||
import { getGrafanaLiveSrv } from '../services/live'; |
import { getGrafanaLiveSrv } from '../services/live'; |
||||||
|
|
||||||
import { Observable, of } from 'rxjs'; |
import { Observable, of } from 'rxjs'; |
||||||
import { map } from 'rxjs/operators'; |
import { toDataQueryError } from '../utils/queryResponse'; |
||||||
|
|
||||||
|
export interface LiveDataFilter { |
||||||
|
fields?: string[]; |
||||||
|
} |
||||||
|
|
||||||
/** |
/** |
||||||
* @alpha -- experimental |
* @alpha |
||||||
*/ |
*/ |
||||||
export function getLiveMeasurements(addr: LiveChannelAddress): LiveMeasurements | undefined { |
export interface LiveDataStreamOptions { |
||||||
if (!isValidLiveChannelAddress(addr)) { |
key?: string; |
||||||
return undefined; |
addr: LiveChannelAddress; |
||||||
} |
buffer?: StreamingFrameOptions; |
||||||
|
filter?: LiveDataFilter; |
||||||
const live = getGrafanaLiveSrv(); |
|
||||||
if (!live) { |
|
||||||
return undefined; |
|
||||||
} |
|
||||||
|
|
||||||
const channel = live.getChannel<LiveMeasurements>(addr); |
|
||||||
const getController = channel?.config?.getController; |
|
||||||
return getController ? getController() : undefined; |
|
||||||
} |
} |
||||||
|
|
||||||
/** |
/** |
||||||
* When you know the stream will be managed measurements |
* Continue executing requests as long as `getNextQuery` returns a query |
||||||
* |
* |
||||||
* @alpha -- experimental |
* @alpha |
||||||
*/ |
*/ |
||||||
export function getLiveMeasurementsObserver( |
export function getLiveDataStream(options: LiveDataStreamOptions): Observable<DataQueryResponse> { |
||||||
addr: LiveChannelAddress, |
if (!isValidLiveChannelAddress(options.addr)) { |
||||||
requestId: string, |
return of({ error: toDataQueryError('invalid address'), data: [] }); |
||||||
query?: MeasurementsQuery |
|
||||||
): Observable<DataQueryResponse> { |
|
||||||
const rsp: DataQueryResponse = { data: [] }; |
|
||||||
if (!addr || !addr.path) { |
|
||||||
return of(rsp); // Address not configured yet
|
|
||||||
} |
} |
||||||
|
|
||||||
const live = getGrafanaLiveSrv(); |
const live = getGrafanaLiveSrv(); |
||||||
if (!live) { |
if (!live) { |
||||||
// This will only happen with the feature flag is not enabled
|
return of({ error: toDataQueryError('grafana live is not initalized'), data: [] }); |
||||||
rsp.error = { message: 'Grafana live is not initalized' }; |
|
||||||
return of(rsp); |
|
||||||
} |
} |
||||||
|
|
||||||
rsp.key = requestId; |
return new Observable<DataQueryResponse>((subscriber) => { |
||||||
return live |
let data: StreamingDataFrame | undefined = undefined; |
||||||
.getChannel<LiveMeasurements>(addr) |
let state = LoadingState.Loading; |
||||||
.getStream() |
const { key, filter } = options; |
||||||
.pipe( |
|
||||||
map((evt) => { |
const process = (msg: DataFrameJSON) => { |
||||||
if (isLiveChannelMessageEvent(evt)) { |
if (!data) { |
||||||
rsp.data = evt.message.getData(query); |
data = new StreamingDataFrame(msg, options.buffer); |
||||||
if (!rsp.data.length) { |
} else { |
||||||
// ?? skip when data is empty ???
|
data.push(msg); |
||||||
|
} |
||||||
|
state = LoadingState.Streaming; |
||||||
|
|
||||||
|
// TODO? this *coud* happen only when the schema changes
|
||||||
|
let filtered = data as DataFrame; |
||||||
|
if (filter?.fields && filter.fields.length) { |
||||||
|
filtered = { |
||||||
|
...data, |
||||||
|
fields: data.fields.filter((f) => filter.fields!.includes(f.name)), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
subscriber.next({ state, data: [filtered], key }); |
||||||
|
}; |
||||||
|
|
||||||
|
const sub = live |
||||||
|
.getChannel<DataFrameJSON>(options.addr) |
||||||
|
.getStream() |
||||||
|
.subscribe({ |
||||||
|
error: (err: any) => { |
||||||
|
state = LoadingState.Error; |
||||||
|
subscriber.next({ state, data: [data], key }); |
||||||
|
sub.unsubscribe(); // close after error
|
||||||
|
}, |
||||||
|
complete: () => { |
||||||
|
if (state !== LoadingState.Error) { |
||||||
|
state = LoadingState.Done; |
||||||
|
} |
||||||
|
subscriber.next({ state, data: [data], key }); |
||||||
|
subscriber.complete(); |
||||||
|
sub.unsubscribe(); |
||||||
|
}, |
||||||
|
next: (evt: LiveChannelEvent) => { |
||||||
|
if (isLiveChannelMessageEvent(evt)) { |
||||||
|
process(evt.message); |
||||||
|
return; |
||||||
} |
} |
||||||
delete rsp.error; |
if (isLiveChannelStatusEvent(evt)) { |
||||||
rsp.state = LoadingState.Streaming; |
if ( |
||||||
} else if (isLiveChannelStatusEvent(evt)) { |
evt.state === LiveChannelConnectionState.Connected || |
||||||
if (evt.error != null) { |
evt.state === LiveChannelConnectionState.Pending |
||||||
rsp.error = rsp.error; |
) { |
||||||
rsp.state = LoadingState.Error; |
if (evt.message) { |
||||||
|
process(evt.message); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
console.log('ignore state', evt); |
||||||
} |
} |
||||||
} |
}, |
||||||
return { ...rsp }; // send event on all status messages
|
}); |
||||||
}) |
|
||||||
); |
return () => { |
||||||
|
sub.unsubscribe(); |
||||||
|
}; |
||||||
|
}); |
||||||
} |
} |
||||||
|
@ -1,32 +0,0 @@ |
|||||||
import { DataFrame, DataFrameJSON } from '@grafana/data'; |
|
||||||
|
|
||||||
/** |
|
||||||
* List of Measurements sent in a batch |
|
||||||
* |
|
||||||
* @alpha -- experimental |
|
||||||
*/ |
|
||||||
export interface MeasurementBatch { |
|
||||||
/** |
|
||||||
* List of measurements to process |
|
||||||
*/ |
|
||||||
batch: DataFrameJSON[]; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* @alpha -- experimental |
|
||||||
*/ |
|
||||||
export interface MeasurementsQuery { |
|
||||||
key?: string; |
|
||||||
fields?: string[]; // only include the fields with these names
|
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Channels that receive Measurements can collect them into frames |
|
||||||
* |
|
||||||
* @alpha -- experimental |
|
||||||
*/ |
|
||||||
export interface LiveMeasurements { |
|
||||||
getData(query?: MeasurementsQuery): DataFrame[]; |
|
||||||
getKeys(): string[]; |
|
||||||
ensureCapacity(size: number): void; |
|
||||||
} |
|
Loading…
Reference in new issue