alternate metrics model with sample objects (#319)

This commit is contained in:
Michael Quigley 2023-05-09 16:22:30 -04:00
parent 6b078abcd7
commit 9f29bb59c7
No known key found for this signature in database
GPG Key ID: 9B60314A9DD20A62
7 changed files with 277 additions and 34 deletions

View File

@ -42,7 +42,7 @@ func (h *getAccountMetricsHandler) Handle(params metadata.GetAccountMetricsParam
slice := duration / 200
query := fmt.Sprintf("from(bucket: \"%v\")\n", h.cfg.Bucket) +
fmt.Sprintf("|> range(start -%v)\n", duration) +
fmt.Sprintf("|> range(start: -%v)\n", duration) +
"|> filter(fn: (r) => r[\"_measurement\"] == \"xfer\")\n" +
"|> filter(fn: (r) => r[\"_field\"] == \"rx\" or r[\"_field\"] == \"tx\")\n" +
"|> filter(fn: (r) => r[\"namespace\"] == \"backend\")\n" +
@ -50,35 +50,42 @@ func (h *getAccountMetricsHandler) Handle(params metadata.GetAccountMetricsParam
"|> drop(columns: [\"share\", \"envId\"])\n" +
fmt.Sprintf("|> aggregateWindow(every: %v, fn: sum, createEmpty: true)", slice)
rx, tx, err := runFluxForRxTxArray(query, h.queryApi)
rx, tx, timestamps, err := runFluxForRxTxArray(query, h.queryApi)
if err != nil {
logrus.Errorf("error running account metrics query for '%v': %v", principal.Email, err)
return metadata.NewGetAccountMetricsInternalServerError()
}
response := &rest_model_zrok.Metrics{
Scope: "account",
ID: fmt.Sprintf("%d", principal.ID),
Period: duration.Seconds(),
Rx: rx,
Tx: tx,
}
for i := 0; i < len(rx) && i < len(tx) && i < len(timestamps); i++ {
response.Samples = append(response.Samples, &rest_model_zrok.MetricsSample{
Rx: rx[i],
Tx: tx[i],
Timestamp: timestamps[i],
})
}
return metadata.NewGetAccountMetricsOK().WithPayload(response)
}
func runFluxForRxTxArray(query string, queryApi api.QueryAPI) (rx, tx []float64, err error) {
func runFluxForRxTxArray(query string, queryApi api.QueryAPI) (rx, tx, timestamps []float64, err error) {
result, err := queryApi.Query(context.Background(), query)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
for result.Next() {
if v, ok := result.Record().Value().(int64); ok {
switch result.Record().Field() {
case "rx":
rx = append(rx, float64(v))
timestamps = append(timestamps, float64(result.Record().Time().UnixMilli()))
case "tx":
tx = append(tx, float64(v))
}
}
}
return rx, tx, nil
return rx, tx, timestamps, nil
}

View File

@ -7,7 +7,9 @@ package rest_model_zrok
import (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
@ -23,23 +25,84 @@ type Metrics struct {
// period
Period float64 `json:"period,omitempty"`
// rx
Rx []float64 `json:"rx"`
// samples
Samples []*MetricsSample `json:"samples"`
// scope
Scope string `json:"scope,omitempty"`
// tx
Tx []float64 `json:"tx"`
}
// Validate validates this metrics
func (m *Metrics) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateSamples(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
// ContextValidate validates this metrics based on context it is used
func (m *Metrics) validateSamples(formats strfmt.Registry) error {
if swag.IsZero(m.Samples) { // not required
return nil
}
for i := 0; i < len(m.Samples); i++ {
if swag.IsZero(m.Samples[i]) { // not required
continue
}
if m.Samples[i] != nil {
if err := m.Samples[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("samples" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("samples" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this metrics based on the context it is used
func (m *Metrics) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateSamples(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *Metrics) contextValidateSamples(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Samples); i++ {
if m.Samples[i] != nil {
if err := m.Samples[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("samples" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("samples" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}

View File

@ -0,0 +1,56 @@
// Code generated by go-swagger; DO NOT EDIT.
package rest_model_zrok
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// MetricsSample metrics sample
//
// swagger:model metricsSample
type MetricsSample struct {
// rx
Rx float64 `json:"rx,omitempty"`
// timestamp
Timestamp float64 `json:"timestamp,omitempty"`
// tx
Tx float64 `json:"tx,omitempty"`
}
// Validate validates this metrics sample
func (m *MetricsSample) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this metrics sample based on context it is used
func (m *MetricsSample) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *MetricsSample) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *MetricsSample) UnmarshalBinary(b []byte) error {
var res MetricsSample
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@ -1147,22 +1147,30 @@ func init() {
"period": {
"type": "number"
},
"rx": {
"samples": {
"type": "array",
"items": {
"type": "number"
"$ref": "#/definitions/metricsSample"
}
},
"scope": {
"type": "string"
}
}
},
"metricsSample": {
"type": "object",
"properties": {
"rx": {
"type": "number"
},
"timestamp": {
"type": "number"
},
"tx": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"principal": {
"type": "object",
@ -2563,22 +2571,30 @@ func init() {
"period": {
"type": "number"
},
"rx": {
"samples": {
"type": "array",
"items": {
"type": "number"
"$ref": "#/definitions/metricsSample"
}
},
"scope": {
"type": "string"
}
}
},
"metricsSample": {
"type": "object",
"properties": {
"rx": {
"type": "number"
},
"timestamp": {
"type": "number"
},
"tx": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"principal": {
"type": "object",

View File

@ -743,13 +743,19 @@ definitions:
type: string
period:
type: number
rx:
samples:
type: array
items:
$ref: "#/definitions/metricsSample"
metricsSample:
type: object
properties:
rx:
type: number
tx:
type: array
items:
type: number
timestamp:
type: number
principal:

View File

@ -130,8 +130,16 @@
* @property {string} scope
* @property {string} id
* @property {number} period
* @property {number[]} rx
* @property {number[]} tx
* @property {module:types.metricsSample[]} samples
*/
/**
* @typedef metricsSample
* @memberof module:types
*
* @property {number} rx
* @property {number} tx
* @property {number} timestamp
*/
/**

View File

@ -1,8 +1,11 @@
import {mdiAccountBox} from "@mdi/js";
import Icon from "@mdi/react";
import PropertyTable from "../../PropertyTable";
import {Tab, Tabs} from "react-bootstrap";
import {Tab, Tabs, Tooltip} from "react-bootstrap";
import SecretToggle from "../../SecretToggle";
import React, {useEffect, useState} from "react";
import * as metadata from "../../../api/metadata";
import {Area, AreaChart, CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis, YAxis} from "recharts";
const AccountDetail = (props) => {
const customProperties = {
@ -16,9 +19,93 @@ const AccountDetail = (props) => {
<Tab eventKey={"detail"} title={"Detail"}>
<PropertyTable object={props.user} custom={customProperties}/>
</Tab>
<Tab eventKey={"metrics"} title={"Metrics"}>
<MetricsTab />
</Tab>
</Tabs>
</div>
);
}
const MetricsTab = (props) => {
const [metrics, setMetrics] = useState({});
const [tx, setTx] = useState(0);
const [rx, setRx] = useState(0)
useEffect(() => {
metadata.getAccountMetrics()
.then(resp => {
setMetrics(resp.data);
});
}, []);
useEffect(() => {
let mounted = true;
let interval = setInterval(() => {
metadata.getAccountMetrics()
.then(resp => {
if(mounted) {
setMetrics(resp.data);
}
});
}, 1000);
return () => {
mounted = false;
clearInterval(interval);
}
}, []);
useEffect(() => {
let txAccum = 0
let rxAccum = 0
if(metrics.samples) {
metrics.samples.forEach(sample => {
txAccum += sample.tx
rxAccum += sample.rx
})
}
setTx(txAccum);
setRx(rxAccum);
}, [metrics])
console.log(metrics);
return (
<div>
<div>
<h1>RX: {bytesToSize(rx)}, TX: {bytesToSize(tx)}</h1>
</div>
<ResponsiveContainer width={"100%"} height={300}>
<LineChart data={metrics.samples}>
<CartesianGrid strokeDasharay={"3 3"} />
<XAxis dataKey={(v) => new Date(v.timestamp)} />
<YAxis />
<Line type={"linear"} stroke={"red"} dataKey={"rx"} activeDot={{ r: 8 }}/>
<Line type={"linear"} stroke={"green"} dataKey={"tx"} />
<Tooltip />
</LineChart>
</ResponsiveContainer>
</div>
);
}
const bytesToSize = (sz) => {
let absSz = sz;
if(absSz < 0) {
absSz *= -1;
}
const unit = 1000
if(absSz < unit) {
return '' + absSz + ' B';
}
let div = unit
let exp = 0
for(let n = absSz / unit; n >= unit; n /= unit) {
div *= unit;
exp++;
}
return '' + (sz / div).toFixed(2) + "kMGTPE"[exp];
}
export default AccountDetail;