Chart support multiple metrics

This commit is contained in:
cuigh 2018-07-02 17:33:14 +08:00
parent 79ebf6937f
commit d47ab7974d
10 changed files with 244 additions and 90 deletions

View File

@ -1198,6 +1198,14 @@ var Swirl;
config(opt) {
}
formatValue(value) {
if (value >= 0) {
return this.formatPositiveValue(value);
}
else {
return "-" + this.formatPositiveValue(-value);
}
}
formatPositiveValue(value) {
switch (this.opts.unit) {
case "percent:100":
return value.toFixed(1) + "%";
@ -2956,4 +2964,36 @@ var Swirl;
Service.DetailPage = DetailPage;
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Metric;
(function (Metric) {
var EditTable = Swirl.Core.EditTable;
class MetricTable extends EditTable {
render() {
return `<tr>
<td>
<input name="metrics[${this.index}].legend" class="input is-small" placeholder="Legend expression for dataset, e.g. ${name}">
</td>
<td>
<input name="metrics[${this.index}].query" class="input is-small" placeholder="Prometheus query expression, for service dashboard, you can use '$\{service\}' variable">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-metric">
<span class="icon is-small">
<i class="far fa-trash-alt"></i>
</span>
</a>
</td>
</tr>`;
}
}
class EditPage {
constructor() {
new MetricTable("#table-metrics");
}
}
Metric.EditPage = EditPage;
})(Metric = Swirl.Metric || (Swirl.Metric = {}));
})(Swirl || (Swirl = {}));
//# sourceMappingURL=swirl.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
///<reference path="../core/core.ts" />
namespace Swirl.Metric {
import EditTable = Swirl.Core.EditTable;
class MetricTable extends EditTable {
protected render(): string {
return `<tr>
<td>
<input name="metrics[${this.index}].legend" class="input is-small" placeholder="Legend expression for dataset, e.g. ${name}">
</td>
<td>
<input name="metrics[${this.index}].query" class="input is-small" placeholder="Prometheus query expression, for service dashboard, you can use '$\{service\}' variable">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-metric">
<span class="icon is-small">
<i class="far fa-trash-alt"></i>
</span>
</a>
</td>
</tr>`;
}
}
export class EditPage {
constructor() {
new MetricTable("#table-metrics");
}
}
}

View File

@ -99,6 +99,14 @@ namespace Swirl.Core {
}
protected formatValue(value: number): string {
if (value >= 0) {
return this.formatPositiveValue(value)
} else {
return "-" + this.formatPositiveValue(-value);
}
}
private formatPositiveValue(value: number): string {
switch (this.opts.unit) {
case "percent:100":
return value.toFixed(1) + "%";

View File

@ -2,6 +2,7 @@ package biz
import (
"os"
"sync"
"time"
"github.com/cuigh/auxo/data"
@ -25,19 +26,6 @@ func newChartBiz() *chartBiz {
b.builtin = append(b.builtin, model.NewChart("service", "$memory", "Memory", "${name}", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="${service}"}`, "size:bytes"))
b.builtin = append(b.builtin, model.NewChart("service", "$network_in", "Network Receive", "${name}", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes"))
b.builtin = append(b.builtin, model.NewChart("service", "$network_out", "Network Send", "${name}", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="${service}"}[5m])) by(name)`, "size:bytes"))
// for _, c := range categories {
// if c == "java" {
// charts = append(charts, model.NewChart("threads", "Threads", "${instance}", `jvm_threads_current{service="%s"}`, ""))
// charts = append(charts, model.NewChart("gc_duration", "GC Duration", "${instance}", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`, "time:s"))
// } else if c == "go" {
// charts = append(charts, model.NewChart("threads", "Threads", "${instance}", `go_threads{service="%s"}`, ""))
// charts = append(charts, model.NewChart("goroutines", "Goroutines", "${instance}", `go_goroutines{service="%s"}`, ""))
// charts = append(charts, model.NewChart("gc_duration", "GC Duration", "${instance}", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`, "time:s"))
// }
// }
// for i, c := range charts {
// charts[i].Query = fmt.Sprintf(c.Query, name)
// }
return b
}
@ -67,6 +55,9 @@ func (b *chartBiz) Delete(id string, user web.User) (err error) {
func (b *chartBiz) Get(name string) (chart *model.Chart, err error) {
do(func(d dao.Interface) {
chart, err = d.ChartGet(name)
if len(chart.Metrics) == 0 {
chart.Metrics = append(chart.Metrics, model.ChartMetric{Legend: chart.Legend, Query: chart.Query})
}
})
return
}
@ -74,6 +65,13 @@ func (b *chartBiz) Get(name string) (chart *model.Chart, err error) {
func (b *chartBiz) Batch(names ...string) (charts []*model.Chart, err error) {
do(func(d dao.Interface) {
charts, err = d.ChartBatch(names...)
if err == nil {
for _, c := range charts {
if len(c.Metrics) == 0 {
c.Metrics = append(c.Metrics, model.ChartMetric{Legend: c.Legend, Query: c.Query})
}
}
}
})
return
}
@ -164,58 +162,99 @@ func (b *chartBiz) FetchDatas(key string, names []string, period time.Duration)
datas := data.Map{}
end := time.Now()
start := end.Add(-period)
var wg sync.WaitGroup
for _, chart := range charts {
query, err := b.formatQuery(chart, key)
wg.Add(1)
go func(c *model.Chart) {
defer wg.Done()
switch c.Type {
case "line", "bar":
d, err := b.fetchMatrixData(c, key, start, end)
if err != nil {
log.Get("metric").Error(err)
} else {
datas[c.Name] = d
}
case "pie":
d, err := b.fetchVectorData(c, key, end)
if err != nil {
log.Get("metric").Error(err)
} else {
datas[c.Name] = d
}
case "gauge":
d, err := b.fetchScalarData(c, key, end)
if err != nil {
log.Get("metric").Error(err)
} else {
datas[c.Name] = &model.ChartValue{
//Name: "",
Value: d,
}
}
}
}(chart)
}
wg.Wait()
return datas, nil
}
func (b *chartBiz) fetchMatrixData(chart *model.Chart, key string, start, end time.Time) (*model.ChartMatrixData, error) {
var cmd *model.ChartMatrixData
for i, m := range chart.Metrics {
q, err := b.formatQuery(m.Query, chart.Dashboard, key)
if err != nil {
return nil, err
}
switch chart.Type {
case "line", "bar":
d, err := Metric.GetMatrix(query, chart.Legend, start, end)
if err != nil {
log.Get("metric").Error(err)
} else {
datas[chart.Name] = d
}
case "pie":
d, err := Metric.GetVector(query, chart.Legend, end)
if err != nil {
log.Get("metric").Error(err)
} else {
datas[chart.Name] = d
}
case "gauge":
d, err := Metric.GetScalar(query, end)
if err != nil {
log.Get("metric").Error(err)
} else {
datas[chart.Name] = &model.ChartValue{
//Name: "",
Value: d,
}
}
d, err := Metric.GetMatrix(q, m.Legend, start, end)
if err != nil {
log.Get("metric").Error(err)
} else if i == 0 {
cmd = d
} else {
cmd.Legend = append(cmd.Legend, d.Legend...)
cmd.Series = append(cmd.Series, d.Series...)
}
}
return datas, nil
return cmd, nil
}
func (b *chartBiz) formatQuery(chart *model.Chart, key string) (string, error) {
if chart.Dashboard == "home" {
return chart.Query, nil
func (b *chartBiz) fetchVectorData(chart *model.Chart, key string, end time.Time) (*model.ChartVectorData, error) {
query, err := b.formatQuery(chart.Metrics[0].Query, chart.Dashboard, key)
if err != nil {
return nil, err
}
return Metric.GetVector(query, chart.Legend, end)
}
func (b *chartBiz) fetchScalarData(chart *model.Chart, key string, end time.Time) (float64, error) {
query, err := b.formatQuery(chart.Metrics[0].Query, chart.Dashboard, key)
if err != nil {
return 0, err
}
return Metric.GetScalar(query, end)
}
func (b *chartBiz) formatQuery(query, dashboard, key string) (string, error) {
if dashboard == "home" {
return query, nil
}
var errs []error
m := map[string]string{chart.Dashboard: key}
query := os.Expand(chart.Query, func(k string) string {
m := map[string]string{dashboard: key}
q := os.Expand(query, func(k string) string {
if v, ok := m[k]; ok {
return v
}
errs = append(errs, errors.New("invalid argument in query: "+chart.Query))
errs = append(errs, errors.New("invalid argument in query: "+query))
return ""
})
if len(errs) == 0 {
return query, nil
return q, nil
}
return "", errs[0]
}

View File

@ -115,6 +115,13 @@ func chartUpdate(ctx web.Context) error {
chart := &model.Chart{}
err := ctx.Bind(chart)
if err == nil {
var metrics []model.ChartMetric
for _, m := range chart.Metrics {
if m.Query != "" {
metrics = append(metrics, m)
}
}
chart.Metrics = metrics
err = biz.Chart.Update(chart, ctx.User())
}
return ajaxResult(ctx, err)

View File

@ -45,12 +45,6 @@ func (d *Dao) ChartBatch(names ...string) (charts []*model.Chart, err error) {
func (d *Dao) ChartUpdate(chart *model.Chart) (err error) {
d.do(func(db *database) {
//update := bson.M{
// "$set": bson.M{
// "name": chart.Name,
// "desc": chart.Description,
// },
//}
err = db.C("chart").UpdateId(chart.Name, chart)
})
return

View File

@ -9,18 +9,19 @@ import (
// Chart represents a dashboard chart.
type Chart struct {
Name string `json:"name" bson:"_id" valid:"required"` // unique, the name of build-in charts has '$' prefix.
Title string `json:"title" valid:"required"`
Description string `json:"desc"`
Legend string `json:"legend"` // ${name} - ${instance}
Query string `json:"query" valid:"required"`
Kind string `json:"kind"` // builtin/custom
Dashboard string `json:"dashboard"` // home/service/task...
Type string `json:"type"` // pie/line...
Unit string `json:"unit"` // bytes/milliseconds/percent:100...
Width int32 `json:"width"` // 1-12(12 columns total)
Height int32 `json:"height"` // default 50
Options data.Map `json:"options"`
Name string `json:"name" bson:"_id" valid:"required"` // unique, the name of build-in charts has '$' prefix.
Title string `json:"title" valid:"required"`
Description string `json:"desc"`
Legend string `json:"-"`
Query string `json:"-"`
Metrics []ChartMetric `json:"metrics" valid:"required"`
Kind string `json:"kind"` // builtin/custom
Dashboard string `json:"dashboard"` // home/service/task...
Type string `json:"type"` // pie/line...
Unit string `json:"unit"` // bytes/milliseconds/percent:100...
Width int32 `json:"width"` // 1-12(12 columns total)
Height int32 `json:"height"` // default 50
Options data.Map `json:"options"`
//Colors []string `json:"colors"`
}
@ -29,16 +30,22 @@ func NewChart(dashboard, name, title, legend, query, unit string) *Chart {
Name: name,
Title: title,
Description: title,
Legend: legend,
Query: query,
Dashboard: dashboard,
Type: "line",
Unit: unit,
Width: 12,
Height: 200,
Metrics: []ChartMetric{
{Legend: legend, Query: query},
},
Dashboard: dashboard,
Type: "line",
Unit: unit,
Width: 12,
Height: 200,
}
}
type ChartMetric struct {
Legend string `json:"legend"`
Query string `json:"query"`
}
type ChartOption struct {
Name string `json:"name"`
Width int32 `json:"width"`

View File

@ -34,9 +34,9 @@
</div>
</div>
</form>
<p class="has-text-centered">
<a href="/password_reset">{{ i18n("login.forgot-password") }}</a>
</p>
{*<p class="has-text-centered">*}
{*<a href="/password_reset">{{ i18n("login.forgot-password") }}</a>*}
{*</p>*}
</div>
</div>
</div>

View File

@ -1,6 +1,10 @@
{{ extends "_base" }}
{{ import "../../_modules/form" }}
{{ block script() }}
<script>$(() => new Swirl.Metric.EditPage())</script>
{{ end }}
{{ block body_content() }}
<section class="section">
<div class="container">
@ -22,9 +26,9 @@
</div>
</div>
<div class="field">
<label class="label">{{ i18n("field.legend") }}</label>
<label class="label">{{ i18n("field.desc") }}</label>
<div class="control">
<input name="legend" value="{{ .Chart.Legend }}" class="input" placeholder="Legend expression for dataset, e.g. ${name}">
<input name="desc" value="{{ .Chart.Description }}" class="input" placeholder="Chart description">
</div>
</div>
</div>
@ -88,18 +92,6 @@
</div>
</div>
</div>
<div class="field">
<label class="label">{{ i18n("field.desc") }}</label>
<div class="control">
<input name="desc" value="{{ .Chart.Description }}" class="input" placeholder="Chart description">
</div>
</div>
<div class="field">
<label class="label">Query expression</label>
<div class="control">
<input name="query" value="{{ .Chart.Query }}" class="input" placeholder="Prometheus query expression, for service dashboard, you can use '${service}' variable" data-v-rule="native" required>
</div>
</div>
<div class="field">
<label class="label">{{ i18n("field.type") }}</label>
<div class="control">
@ -119,6 +111,43 @@
{*{{ yield radio(name="dashboard", value="task", label="Task", checked=.Chart.Dashboard) }}*}
</div>
</div>
<fieldset>
<legend class="lead is-5">Metrics</legend>
<table id="table-metrics" class="table is-bordered is-narrow is-fullwidth is-marginless" data-name="metric">
<thead>
<tr>
<th width="25%">Legend</th>
<th>Query</th>
<th width="50">
<a class="button is-small is-outlined is-success" data-action="add-metric">
<span class="icon is-small">
<i class="fas fa-plus"></i>
</span>
</a>
</th>
</tr>
</thead>
<tbody>
{{ range i, c := .Chart.Metrics }}
<tr>
<td>
<input name="metrics[{{i}}].legend" value="{{c.Legend}}" class="input is-small" placeholder="Legend expression for dataset, e.g. ${name}">
</td>
<td>
<input name="metrics[{{i}}].query" value="{{c.Query}}" class="input is-small" placeholder="Prometheus query expression, for service dashboard, you can use '${service}' variable">
</td>
<td>
<a class="button is-small is-outlined is-danger" data-action="delete-metric">
<span class="icon is-small">
<i class="far fa-trash-alt"></i>
</span>
</a>
</td>
</tr>
{{ end }}
</tbody>
</table>
</fieldset>
{{ yield form_submit(url="/system/chart/") }}
</form>
</div>