diff --git a/controller/metrics.go b/controller/metrics.go index a5091b0f..0197dbce 100644 --- a/controller/metrics.go +++ b/controller/metrics.go @@ -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 } diff --git a/rest_model_zrok/metrics.go b/rest_model_zrok/metrics.go index dfb9bf08..1aa24172 100644 --- a/rest_model_zrok/metrics.go +++ b/rest_model_zrok/metrics.go @@ -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 } diff --git a/rest_model_zrok/metrics_sample.go b/rest_model_zrok/metrics_sample.go new file mode 100644 index 00000000..84869de4 --- /dev/null +++ b/rest_model_zrok/metrics_sample.go @@ -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 +} diff --git a/rest_server_zrok/embedded_spec.go b/rest_server_zrok/embedded_spec.go index 2d8d53fc..d268d47c 100644 --- a/rest_server_zrok/embedded_spec.go +++ b/rest_server_zrok/embedded_spec.go @@ -1147,20 +1147,28 @@ 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" - } + "type": "number" } } }, @@ -2563,20 +2571,28 @@ 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" - } + "type": "number" } } }, diff --git a/specs/zrok.yml b/specs/zrok.yml index a73ab913..fe129fc8 100644 --- a/specs/zrok.yml +++ b/specs/zrok.yml @@ -743,14 +743,20 @@ definitions: type: string period: type: number + samples: + type: array + items: + $ref: "#/definitions/metricsSample" + + metricsSample: + type: object + properties: rx: - type: array - items: - type: number + type: number tx: - type: array - items: - type: number + type: number + timestamp: + type: number principal: type: object diff --git a/ui/src/api/types.js b/ui/src/api/types.js index 6f5a6317..01c83b83 100644 --- a/ui/src/api/types.js +++ b/ui/src/api/types.js @@ -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 */ /** diff --git a/ui/src/console/detail/account/AccountDetail.js b/ui/src/console/detail/account/AccountDetail.js index 679b51aa..bd39617d 100644 --- a/ui/src/console/detail/account/AccountDetail.js +++ b/ui/src/console/detail/account/AccountDetail.js @@ -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) => { + + + ); } +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 ( +
+
+

RX: {bytesToSize(rx)}, TX: {bytesToSize(tx)}

+
+ + + + new Date(v.timestamp)} /> + + + + + + +
+ ); +} + +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; \ No newline at end of file