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'; |
||||
|
@ -1,77 +1,114 @@ |
||||
import { |
||||
DataFrame, |
||||
DataFrameJSON, |
||||
DataQueryResponse, |
||||
isLiveChannelMessageEvent, |
||||
isLiveChannelStatusEvent, |
||||
isValidLiveChannelAddress, |
||||
LiveChannelAddress, |
||||
LiveChannelConnectionState, |
||||
LiveChannelEvent, |
||||
LoadingState, |
||||
StreamingDataFrame, |
||||
StreamingFrameOptions, |
||||
} from '@grafana/data'; |
||||
import { LiveMeasurements, MeasurementsQuery } from './types'; |
||||
import { getGrafanaLiveSrv } from '../services/live'; |
||||
|
||||
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 { |
||||
if (!isValidLiveChannelAddress(addr)) { |
||||
return undefined; |
||||
} |
||||
|
||||
const live = getGrafanaLiveSrv(); |
||||
if (!live) { |
||||
return undefined; |
||||
} |
||||
|
||||
const channel = live.getChannel<LiveMeasurements>(addr); |
||||
const getController = channel?.config?.getController; |
||||
return getController ? getController() : undefined; |
||||
export interface LiveDataStreamOptions { |
||||
key?: string; |
||||
addr: LiveChannelAddress; |
||||
buffer?: StreamingFrameOptions; |
||||
filter?: LiveDataFilter; |
||||
} |
||||
|
||||
/** |
||||
* 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( |
||||
addr: LiveChannelAddress, |
||||
requestId: string, |
||||
query?: MeasurementsQuery |
||||
): Observable<DataQueryResponse> { |
||||
const rsp: DataQueryResponse = { data: [] }; |
||||
if (!addr || !addr.path) { |
||||
return of(rsp); // Address not configured yet
|
||||
export function getLiveDataStream(options: LiveDataStreamOptions): Observable<DataQueryResponse> { |
||||
if (!isValidLiveChannelAddress(options.addr)) { |
||||
return of({ error: toDataQueryError('invalid address'), data: [] }); |
||||
} |
||||
|
||||
const live = getGrafanaLiveSrv(); |
||||
if (!live) { |
||||
// This will only happen with the feature flag is not enabled
|
||||
rsp.error = { message: 'Grafana live is not initalized' }; |
||||
return of(rsp); |
||||
return of({ error: toDataQueryError('grafana live is not initalized'), data: [] }); |
||||
} |
||||
|
||||
rsp.key = requestId; |
||||
return live |
||||
.getChannel<LiveMeasurements>(addr) |
||||
.getStream() |
||||
.pipe( |
||||
map((evt) => { |
||||
if (isLiveChannelMessageEvent(evt)) { |
||||
rsp.data = evt.message.getData(query); |
||||
if (!rsp.data.length) { |
||||
// ?? skip when data is empty ???
|
||||
return new Observable<DataQueryResponse>((subscriber) => { |
||||
let data: StreamingDataFrame | undefined = undefined; |
||||
let state = LoadingState.Loading; |
||||
const { key, filter } = options; |
||||
|
||||
const process = (msg: DataFrameJSON) => { |
||||
if (!data) { |
||||
data = new StreamingDataFrame(msg, options.buffer); |
||||
} else { |
||||
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; |
||||
rsp.state = LoadingState.Streaming; |
||||
} else if (isLiveChannelStatusEvent(evt)) { |
||||
if (evt.error != null) { |
||||
rsp.error = rsp.error; |
||||
rsp.state = LoadingState.Error; |
||||
if (isLiveChannelStatusEvent(evt)) { |
||||
if ( |
||||
evt.state === LiveChannelConnectionState.Connected || |
||||
evt.state === LiveChannelConnectionState.Pending |
||||
) { |
||||
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