|
|
|
@ -1,118 +1,15 @@ |
|
|
|
|
import { |
|
|
|
|
CircularDataFrame, |
|
|
|
|
Labels, |
|
|
|
|
formatLabels, |
|
|
|
|
FieldType, |
|
|
|
|
DataFrame, |
|
|
|
|
matchAllLabels, |
|
|
|
|
parseLabels, |
|
|
|
|
CircularVector, |
|
|
|
|
ArrayVector, |
|
|
|
|
} from '@grafana/data'; |
|
|
|
|
import { Measurement, MeasurementBatch, LiveMeasurements, MeasurementsQuery, MeasurementAction } from './types'; |
|
|
|
|
|
|
|
|
|
interface MeasurementCacheConfig { |
|
|
|
|
append?: 'head' | 'tail'; |
|
|
|
|
capacity?: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** This is a cache scoped to a the measurement name |
|
|
|
|
* |
|
|
|
|
* @alpha -- experimental |
|
|
|
|
*/ |
|
|
|
|
export class MeasurementCache { |
|
|
|
|
readonly frames: Record<string, CircularDataFrame> = {}; // key is the labels
|
|
|
|
|
|
|
|
|
|
constructor(public name: string, private config: MeasurementCacheConfig) { |
|
|
|
|
if (!this.config) { |
|
|
|
|
this.config = { |
|
|
|
|
append: 'tail', |
|
|
|
|
capacity: 600, // Default capacity 10min @ 1hz
|
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getFrames(match?: Labels): DataFrame[] { |
|
|
|
|
const frames = Object.values(this.frames); |
|
|
|
|
if (!match) { |
|
|
|
|
return frames; |
|
|
|
|
} |
|
|
|
|
return frames.filter((f) => { |
|
|
|
|
return matchAllLabels(match, f.meta?.custom?.labels); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
addMeasurement(m: Measurement, action: MeasurementAction): DataFrame { |
|
|
|
|
const key = m.labels ? formatLabels(m.labels) : ''; |
|
|
|
|
let frame = this.frames[key]; |
|
|
|
|
|
|
|
|
|
if (!frame) { |
|
|
|
|
frame = new CircularDataFrame(this.config); |
|
|
|
|
frame.name = this.name; |
|
|
|
|
frame.addField({ |
|
|
|
|
name: 'time', |
|
|
|
|
type: FieldType.time, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(m.values)) { |
|
|
|
|
frame.addFieldFor(value, key).labels = m.labels; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
frame.meta = { |
|
|
|
|
custom: { |
|
|
|
|
labels: m.labels, |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
this.frames[key] = frame; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Clear existing values
|
|
|
|
|
if (action === MeasurementAction.Replace) { |
|
|
|
|
for (const field of frame.fields) { |
|
|
|
|
(field.values as ArrayVector).buffer.length = 0; // same buffer, but reset to empty length
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Add the timestamp
|
|
|
|
|
frame.fields[0].values.add(m.time || Date.now()); |
|
|
|
|
|
|
|
|
|
// Attach field config to the current fields
|
|
|
|
|
if (m.config) { |
|
|
|
|
for (const [key, value] of Object.entries(m.config)) { |
|
|
|
|
const f = frame.fields.find((f) => f.name === key); |
|
|
|
|
if (f) { |
|
|
|
|
f.config = value; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Append all values (a row)
|
|
|
|
|
for (const [key, value] of Object.entries(m.values)) { |
|
|
|
|
const existingField = frame.fields.find((v) => v.name === key); |
|
|
|
|
if (!existingField) { |
|
|
|
|
const f = frame.addFieldFor(value, key); |
|
|
|
|
f.labels = m.labels; |
|
|
|
|
f.values.add(value); |
|
|
|
|
} else { |
|
|
|
|
existingField.values.add(value); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Make sure all fields have the same length
|
|
|
|
|
frame.validate(); |
|
|
|
|
return frame; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
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, MeasurementCache>(); |
|
|
|
|
config: MeasurementCacheConfig = { |
|
|
|
|
append: 'tail', |
|
|
|
|
capacity: 600, // Default capacity 10min @ 1hz
|
|
|
|
|
measurements = new Map<string, StreamingDataFrame>(); |
|
|
|
|
config: StreamingFrameOptions = { |
|
|
|
|
maxLength: 600, // Default capacity 10min @ 1hz
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
//------------------------------------------------------
|
|
|
|
@ -120,93 +17,68 @@ export class MeasurementCollector implements LiveMeasurements { |
|
|
|
|
//------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
getData(query?: MeasurementsQuery): DataFrame[] { |
|
|
|
|
const { name, labels, fields } = query || {}; |
|
|
|
|
|
|
|
|
|
let data: DataFrame[] = []; |
|
|
|
|
if (name) { |
|
|
|
|
// for now we only match exact names
|
|
|
|
|
const m = this.measurements.get(name); |
|
|
|
|
if (m) { |
|
|
|
|
data = m.getFrames(labels); |
|
|
|
|
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.apply(data, f.getFrames(labels)); |
|
|
|
|
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 }); // Copy the frame with fewer fields
|
|
|
|
|
filtered.push({ ...frame, fields: match, length: frame.length }); // Copy the frame with fewer fields
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if (filtered.length) { |
|
|
|
|
return filtered; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return data; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getDistinctNames(): string[] { |
|
|
|
|
getKeys(): string[] { |
|
|
|
|
return Object.keys(this.measurements); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getDistinctLabels(name: string): Labels[] { |
|
|
|
|
const m = this.measurements.get(name); |
|
|
|
|
if (m) { |
|
|
|
|
return Object.keys(m.frames).map((k) => parseLabels(k)); |
|
|
|
|
} |
|
|
|
|
return []; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setCapacity(size: number) { |
|
|
|
|
this.config.capacity = size; |
|
|
|
|
|
|
|
|
|
// Now update all the circular buffers
|
|
|
|
|
for (const wrap of this.measurements.values()) { |
|
|
|
|
for (const frame of Object.values(wrap.frames)) { |
|
|
|
|
for (const field of frame.fields) { |
|
|
|
|
(field.values as CircularVector).setCapacity(size); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getCapacity() { |
|
|
|
|
return this.config.capacity!; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
clear() { |
|
|
|
|
this.measurements.clear(); |
|
|
|
|
ensureCapacity(size: number) { |
|
|
|
|
// TODO...
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//------------------------------------------------------
|
|
|
|
|
// Collector
|
|
|
|
|
//------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
addBatch = (batch: MeasurementBatch) => { |
|
|
|
|
let action = batch.action ?? MeasurementAction.Append; |
|
|
|
|
if (action === MeasurementAction.Clear) { |
|
|
|
|
this.measurements.clear(); |
|
|
|
|
action = MeasurementAction.Append; |
|
|
|
|
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'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Change the local buffer size
|
|
|
|
|
if (batch.capacity && batch.capacity !== this.config.capacity) { |
|
|
|
|
this.setCapacity(batch.capacity); |
|
|
|
|
} |
|
|
|
|
for (const measure of msg.batch) { |
|
|
|
|
const key = measure.key ?? measure.schema?.name ?? ''; |
|
|
|
|
|
|
|
|
|
for (const measure of batch.measurements) { |
|
|
|
|
const name = measure.name || ''; |
|
|
|
|
let m = this.measurements.get(name); |
|
|
|
|
if (!m) { |
|
|
|
|
m = new MeasurementCache(name, this.config); |
|
|
|
|
this.measurements.set(name, m); |
|
|
|
|
} |
|
|
|
|
if (measure.values) { |
|
|
|
|
m.addMeasurement(measure, action); |
|
|
|
|
let s = this.measurements.get(key); |
|
|
|
|
if (s) { |
|
|
|
|
s.update(measure); |
|
|
|
|
} else { |
|
|
|
|
console.log('invalid measurement', measure); |
|
|
|
|
s = new StreamingDataFrame(measure, this.config); //
|
|
|
|
|
this.measurements.set(key, s); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return this; |
|
|
|
|