Add network/java/go metrics

This commit is contained in:
cuigh 2018-03-12 16:48:16 +08:00
parent 6fff495cda
commit c600bcb0dd
8 changed files with 363 additions and 272 deletions

View File

@ -1857,6 +1857,156 @@ var Swirl;
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Service;
(function (Service) {
class MetricChartOptions {
constructor() {
this.type = "line";
this.height = 50;
}
}
class MetricChart {
constructor(elem, opts) {
this.colors = [
'rgb(255, 99, 132)',
'rgb(75, 192, 192)',
'rgb(255, 159, 64)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(255, 205, 86)',
'rgb(201, 203, 207)',
];
opts = $.extend(new MetricChartOptions(), opts);
this.config = {
type: opts.type,
data: {},
options: {
title: {
text: opts.title || 'NONE'
},
animation: {
duration: 0,
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'YYYY/MM/DD HH:mm:ss',
displayFormats: {
minute: 'HH:mm'
}
},
}],
yAxes: [{}]
},
}
};
if (opts.labelX) {
this.config.options.scales.xAxes[0].scaleLabel = {
display: true,
labelString: opts.labelX,
};
}
if (opts.labelY) {
this.config.options.scales.yAxes[0].scaleLabel = {
display: true,
labelString: opts.labelY,
};
}
if (opts.tickY) {
this.config.options.scales.yAxes[0].ticks = {
callback: opts.tickY,
};
}
let ctx = $(elem).get(0).getContext('2d');
if (opts.height) {
ctx.canvas.height = opts.height;
}
this.chart = new Chart(ctx, this.config);
}
setData(datasets) {
datasets.forEach((ds, i) => {
let color = (i < this.colors.length) ? this.colors[i] : this.colors[0];
ds.backgroundColor = Chart.helpers.color(color).alpha(0.3).rgbString();
ds.borderColor = color;
ds.borderWidth = 2;
ds.pointRadius = 2;
});
this.config.data.datasets = datasets;
this.chart.update();
}
}
class StatsPage {
constructor() {
this.chartOptions = {
"cpu": { tickY: (value) => value + '%' },
"memory": { tickY: StatsPage.formatSize },
"network_in": { tickY: StatsPage.formatSize },
"network_out": { tickY: StatsPage.formatSize },
"threads": {},
"goroutines": {},
"gc_duration": { tickY: (value) => value * 1000 + 'ms' },
};
this.charts = {};
let $cb_time = $("#cb-time");
if ($cb_time.length == 0) {
return;
}
$cb_time.change(this.loadData.bind(this));
$("#cb-refresh").change(() => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
else {
this.refreshData();
}
});
$.each(this.chartOptions, (name, opt) => {
let $el = $("#canvas_" + name);
if ($el.length > 0) {
this.charts[name] = new MetricChart($el, opt);
}
});
this.refreshData();
}
refreshData() {
this.loadData();
if ($("#cb-refresh").prop("checked")) {
this.timer = setTimeout(this.refreshData.bind(this), 15000);
}
}
loadData() {
let time = $("#cb-time").val();
$ajax.get(`metrics`, { time: time }).json((d) => {
$.each(this.charts, (name, chart) => {
if (d[name]) {
chart.setData(d[name]);
}
});
});
}
static formatSize(size) {
if (size < 1024) {
return size.toString() + 'B';
}
else if (size < 1048576) {
return (size / 1024).toFixed(2) + 'K';
}
else if (size < 1073741824) {
return (size / 1048576).toFixed(2) + 'M';
}
else {
return (size / 1073741824).toFixed(2) + 'G';
}
}
}
Service.StatsPage = StatsPage;
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Service;
(function (Service) {
@ -2155,137 +2305,4 @@ var Swirl;
Volume.NewPage = NewPage;
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
})(Swirl || (Swirl = {}));
var Swirl;
(function (Swirl) {
var Service;
(function (Service) {
class MetricChartOptions {
constructor() {
this.type = "line";
this.height = 50;
}
}
class MetricData {
}
class MetricChart {
constructor(elem, opts) {
this.colors = [
'rgb(255, 99, 132)',
'rgb(75, 192, 192)',
'rgb(255, 159, 64)',
'rgb(54, 162, 235)',
'rgb(153, 102, 255)',
'rgb(255, 205, 86)',
'rgb(201, 203, 207)',
];
opts = $.extend(new MetricChartOptions(), opts);
this.config = {
type: opts.type,
data: {},
options: {
title: {
text: opts.title || 'NONE'
},
animation: {
duration: 0,
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: 'minute',
tooltipFormat: 'YYYY/MM/DD HH:mm:ss',
displayFormats: {
minute: 'HH:mm'
}
},
}],
yAxes: [{}]
},
}
};
if (opts.labelX) {
this.config.options.scales.xAxes[0].scaleLabel = {
display: true,
labelString: opts.labelX,
};
}
if (opts.labelY) {
this.config.options.scales.yAxes[0].scaleLabel = {
display: true,
labelString: opts.labelY,
};
}
if (opts.tickY) {
this.config.options.scales.yAxes[0].ticks = {
callback: opts.tickY,
};
}
let ctx = $(elem).get(0).getContext('2d');
if (opts.height) {
ctx.canvas.height = opts.height;
}
this.chart = new Chart(ctx, this.config);
}
setData(datasets) {
datasets.forEach((ds, i) => {
let color = (i < this.colors.length) ? this.colors[i] : this.colors[0];
ds.backgroundColor = Chart.helpers.color(color).alpha(0.3).rgbString();
ds.borderColor = color;
ds.borderWidth = 2;
ds.pointRadius = 2;
});
this.config.data.datasets = datasets;
this.chart.update();
}
}
class StatsPage {
constructor() {
let $cb_time = $("#cb-time");
if ($cb_time.length == 0) {
return;
}
$cb_time.change(this.loadData.bind(this));
$("#cb-refresh").change(() => {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
else {
this.refreshData();
}
});
this.cpu = new MetricChart("#canvas-cpu", {
tickY: function (value) {
return value + '%';
},
});
this.memory = new MetricChart("#canvas-memory", {
tickY: function (value) {
return value < 1024 ? (value + 'M') : (value / 1024) + 'G';
},
});
this.refreshData();
}
refreshData() {
this.loadData();
if ($("#cb-refresh").prop("checked")) {
this.timer = setTimeout(this.refreshData.bind(this), 15000);
}
}
loadData() {
let time = $("#cb-time").val();
$ajax.get(`metrics`, { time: time }).json((d) => {
if (d.cpu) {
this.cpu.setData(d.cpu);
}
if (d.memory) {
this.memory.setData(d.memory);
}
});
}
}
Service.StatsPage = StatsPage;
})(Service = Swirl.Service || (Swirl.Service = {}));
})(Swirl || (Swirl = {}));
//# sourceMappingURL=swirl.js.map

File diff suppressed because one or more lines are too long

View File

@ -7,11 +7,7 @@ namespace Swirl.Service {
labelX?: string;
labelY?: string;
tickY?: (value: number) => string;
}
class MetricData {
cpu?: Chart.ChartDataSets[];
memory?: Chart.ChartDataSets[];
// tooltipLabel?: (tooltipItem: Chart.ChartTooltipItem, data: Chart.ChartData) => string | string[];
}
class MetricChart {
@ -27,7 +23,7 @@ namespace Swirl.Service {
'rgb(201, 203, 207)', // grey
];
constructor(elem: string | Element, opts: MetricChartOptions) {
constructor(elem: string | Element | JQuery, opts?: MetricChartOptions) {
opts = $.extend(new MetricChartOptions(), opts);
this.config = {
type: opts.type,
@ -59,6 +55,11 @@ namespace Swirl.Service {
}],
yAxes: [{}]
},
// tooltips: {
// callbacks: {
// label: opts.tooltipLabel,
// },
// }
}
};
if (opts.labelX) {
@ -101,8 +102,16 @@ namespace Swirl.Service {
}
export class StatsPage {
private cpu: MetricChart;
private memory: MetricChart;
private chartOptions: { [index: string]: MetricChartOptions } = {
"cpu": {tickY: (value: number): string => value + '%'},
"memory": {tickY: StatsPage.formatSize},
"network_in": {tickY: StatsPage.formatSize},
"network_out": {tickY: StatsPage.formatSize},
"threads": {},
"goroutines": {},
"gc_duration": {tickY: (value: number): string => value * 1000 + 'ms'},
};
private charts: { [index: string]: MetricChart } = {};
private timer: number;
constructor() {
@ -121,15 +130,11 @@ namespace Swirl.Service {
}
});
this.cpu = new MetricChart("#canvas-cpu", {
tickY: function (value: number): string {
return value + '%';
},
});
this.memory = new MetricChart("#canvas-memory", {
tickY: function (value: number): string {
return value < 1024 ? (value + 'M') : (value / 1024) + 'G';
},
$.each(this.chartOptions, (name, opt) => {
let $el = $("#canvas_" + name);
if ($el.length > 0) {
this.charts[name] = new MetricChart($el, opt);
}
});
this.refreshData();
}
@ -143,14 +148,25 @@ namespace Swirl.Service {
private loadData() {
let time = $("#cb-time").val();
$ajax.get(`metrics`, {time: time}).json((d: MetricData) => {
if (d.cpu) {
this.cpu.setData(d.cpu);
}
if (d.memory) {
this.memory.setData(d.memory);
}
$ajax.get(`metrics`, {time: time}).json((d: {[index: string]: Chart.ChartDataSets[]}) => {
$.each(this.charts, (name: string, chart: MetricChart) => {
if (d[name]) {
chart.setData(d[name]);
}
});
});
}
private static formatSize(size: number): string {
if (size < 1024) { // 1K
return size.toString() + 'B';
} else if (size < 1048576) { // 1M
return (size / 1024).toFixed(2) + 'K';
} else if (size < 1073741824) { // 1G
return (size / 1048576).toFixed(2) + 'M';
} else {
return (size / 1073741824).toFixed(2) + 'G';
}
}
}
}

112
biz/metric.go Normal file
View File

@ -0,0 +1,112 @@
package biz
import (
"context"
"fmt"
"time"
"github.com/cuigh/auxo/ext/times"
"github.com/cuigh/auxo/util/lazy"
"github.com/cuigh/swirl/model"
pclient "github.com/prometheus/client_golang/api"
papi "github.com/prometheus/client_golang/api/prometheus/v1"
pmodel "github.com/prometheus/common/model"
)
// Metric return a metric biz instance.
var Metric = &metricBiz{
api: lazy.Value{
New: func() (interface{}, error) {
setting, err := Setting.Get()
if err != nil {
return nil, err
}
client, err := pclient.NewClient(pclient.Config{Address: setting.Metrics.Prometheus})
if err != nil {
return nil, err
}
return papi.NewAPI(client), nil
},
},
}
type metricBiz struct {
api lazy.Value
}
func (b *metricBiz) GetServiceCharts(service string, categories []string) (charts []model.ChartInfo) {
charts = append(charts, model.NewChartInfo("cpu", "CPU", "name", `rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`))
charts = append(charts, model.NewChartInfo("memory", "Memory", "name", `container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`))
charts = append(charts, model.NewChartInfo("network_in", "Network Receive", "name", `sum(irate(container_network_receive_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
charts = append(charts, model.NewChartInfo("network_out", "Network Send", "name", `sum(irate(container_network_transmit_bytes_total{container_label_com_docker_swarm_service_name="%s"}[5m])) by(name)`))
for _, c := range categories {
if c == "java" {
charts = append(charts, model.NewChartInfo("threads", "Threads", "instance", `jvm_threads_current{service="%s"}`))
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "instance", `rate(jvm_gc_collection_seconds_sum{service="%s"}[1m])`))
} else if c == "go" {
charts = append(charts, model.NewChartInfo("threads", "Threads", "instance", `go_threads{service="%s"}`))
charts = append(charts, model.NewChartInfo("goroutines", "Goroutines", "instance", `go_goroutines{service="%s"}`))
charts = append(charts, model.NewChartInfo("gc_duration", "GC Duration", "instance", `sum(go_gc_duration_seconds{service="%s"}) by (instance)`))
}
}
for i, c := range charts {
charts[i].Query = fmt.Sprintf(c.Query, service)
}
return
}
func (b *metricBiz) GetMatrix(query, label string, start, end time.Time) (lines []model.ChartLine, err error) {
api, err := b.getAPI()
if err != nil {
return nil, err
}
period := end.Sub(start)
value, err := api.QueryRange(context.Background(), query, papi.Range{
Start: start,
End: end,
Step: b.calcStep(period),
})
if err != nil {
return nil, err
}
matrix := value.(pmodel.Matrix)
for _, stream := range matrix {
line := model.ChartLine{Label: string(stream.Metric[pmodel.LabelName(label)])}
for _, v := range stream.Values {
p := model.ChartPoint{
X: int64(v.Timestamp),
Y: float64(v.Value),
}
line.Data = append(line.Data, p)
}
lines = append(lines, line)
}
return
}
func (b *metricBiz) calcStep(period time.Duration) (step time.Duration) {
if period >= times.Day {
step = 20 * time.Minute
} else if period >= 12*time.Hour {
step = 10 * time.Minute
} else if period >= 6*time.Hour {
step = 5 * time.Minute
} else if period >= 3*time.Hour {
step = 3 * time.Minute
} else {
step = time.Minute
}
return
}
func (b *metricBiz) getAPI() (api papi.API, err error) {
v, err := b.api.Get()
if err != nil {
return nil, err
}
return v.(papi.API), nil
}

View File

@ -1,8 +1,6 @@
package controller
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@ -10,16 +8,12 @@ import (
"github.com/cuigh/auxo/data"
"github.com/cuigh/auxo/data/set"
"github.com/cuigh/auxo/errors"
"github.com/cuigh/auxo/ext/times"
"github.com/cuigh/auxo/net/web"
"github.com/cuigh/auxo/util/cast"
"github.com/cuigh/swirl/biz"
"github.com/cuigh/swirl/biz/docker"
"github.com/cuigh/swirl/misc"
"github.com/cuigh/swirl/model"
"github.com/prometheus/client_golang/api"
prometheus "github.com/prometheus/client_golang/api/prometheus/v1"
pm "github.com/prometheus/common/model"
)
// ServiceController is a controller of docker service
@ -299,112 +293,46 @@ func serviceStats(ctx web.Context) error {
return err
}
var charts []model.ChartInfo
if setting.Metrics.Prometheus != "" {
var categories []string
if label := service.Spec.Labels["swirl.metrics"]; label != "" {
categories = strings.Split(label, ",")
}
charts = biz.Metric.GetServiceCharts(name, categories)
}
period := cast.ToDuration(ctx.Q("time"), time.Hour)
refresh := cast.ToBool(ctx.Q("refresh"), true)
m := newModel(ctx).Set("Service", service).Set("Tasks", tasks).
Set("Time", period.String()).Set("Refresh", refresh).Set("Metrics", setting.Metrics.Prometheus != "")
m := newModel(ctx).Set("Service", service).Set("Tasks", tasks).Set("Time", period.String()).
Set("Refresh", refresh).Set("Charts", charts)
return ctx.Render("service/stats", m)
}
// nolint: gocyclo
func serviceMetrics(ctx web.Context) error {
type chartPoint struct {
X int64 `json:"x"`
Y float64 `json:"y"`
}
type chartDataset struct {
Label string `json:"label"`
Data []chartPoint `json:"data"`
}
name := ctx.P("name")
service, _, err := docker.ServiceInspect(name)
if err != nil {
return err
}
var categories []string
if label := service.Spec.Labels["swirl.metrics"]; label != "" {
categories = strings.Split(label, ",")
}
charts := biz.Metric.GetServiceCharts(name, categories)
period := cast.ToDuration(ctx.Q("time"), time.Hour)
var step time.Duration
if period >= times.Day {
step = 10 * time.Minute
} else if period >= 12*time.Hour {
step = 5 * time.Minute
} else if period >= 6*time.Hour {
step = 3 * time.Minute
} else if period >= 3*time.Hour {
step = 2 * time.Minute
} else {
step = time.Minute
}
setting, err := biz.Setting.Get()
if err != nil {
return err
}
client, err := api.NewClient(api.Config{Address: setting.Metrics.Prometheus})
if err != nil {
return err
}
papi := prometheus.NewAPI(client)
// cpu
query := fmt.Sprintf(`rate(container_cpu_user_seconds_total{container_label_com_docker_swarm_service_name="%s"}[5m]) * 100`, name)
end := time.Now()
start := end.Add(-period)
value, err := papi.QueryRange(context.Background(), query, prometheus.Range{
Start: start,
End: end,
Step: step,
})
if err != nil {
return err
}
matrix := value.(pm.Matrix)
var cpuDatas []chartDataset
for _, stream := range matrix {
ds := chartDataset{
Label: string(stream.Metric["name"]),
}
for _, v := range stream.Values {
p := chartPoint{
X: int64(v.Timestamp),
Y: float64(v.Value),
}
ds.Data = append(ds.Data, p)
}
cpuDatas = append(cpuDatas, ds)
}
// memory
query = fmt.Sprintf(`container_memory_usage_bytes{container_label_com_docker_swarm_service_name="%s"}`, name)
value, err = papi.QueryRange(context.Background(), query, prometheus.Range{
Start: start,
End: end,
Step: step,
})
if err != nil {
return err
}
matrix = value.(pm.Matrix)
var memoryDatas []chartDataset
for _, stream := range matrix {
ds := chartDataset{
Label: string(stream.Metric["name"]),
m := data.Map{}
for _, c := range charts {
matrix, err := biz.Metric.GetMatrix(c.Query, c.Label, start, end)
if err != nil {
return err
}
for _, v := range stream.Values {
p := chartPoint{
X: int64(v.Timestamp),
Y: float64(v.Value) / 1024 / 1024,
}
ds.Data = append(ds.Data, p)
}
memoryDatas = append(memoryDatas, ds)
}
// start time
//query = fmt.Sprintf(`container_start_time_seconds{container_label_com_docker_swarm_service_name="%s"}`, name)
//value, err = papi.Query(context.Background(), query, end)
//scalar := value.(*pm.Scalar)
m := data.Map{
"cpu": cpuDatas,
"memory": memoryDatas,
m[c.Name] = matrix
}
return ctx.JSON(m)
}

View File

@ -26,7 +26,7 @@ func main() {
misc.BindOptions()
app.Name = "Swirl"
app.Version = "0.6.9"
app.Version = "0.7.0"
app.Desc = "A web management UI for Docker, focused on swarm cluster"
app.Action = func(ctx *app.Context) {
misc.LoadOptions()

View File

@ -33,3 +33,29 @@ type TemplateListArgs struct {
PageIndex int `bind:"page"`
PageSize int `bind:"size"`
}
type ChartPoint struct {
X int64 `json:"x"`
Y float64 `json:"y"`
}
type ChartLine struct {
Label string `json:"label"`
Data []ChartPoint `json:"data"`
}
type ChartInfo struct {
Name string `json:"name"`
Title string `json:"title"`
Label string `json:"label"`
Query string `json:"query"`
}
func NewChartInfo(name, title, label, query string) ChartInfo {
return ChartInfo{
Name: name,
Title: title,
Label: label,
Query: query,
}
}

View File

@ -43,7 +43,7 @@
<section class="section">
<div class="container">
{{ if .Metrics }}
{{ if .Charts }}
<nav class="level">
<form>
<div class="level-left">
@ -75,24 +75,16 @@
</div>
</form>
</nav>
{{ range .Charts }}
<div class="block">
<div class="block-header">
<p>CPU</p>
<p>{{ .Title }}</p>
</div>
<div class="block-body is-bordered">
<canvas id="canvas-cpu"></canvas>
</div>
</div>
<div class="block">
<div class="block-header">
<p>Memory</p>
</div>
<div class="block-body is-bordered">
<canvas id="canvas-memory"></canvas>
<canvas id="canvas_{{ .Name }}"></canvas>
</div>
</div>
{{ end }}
{{ else }}
<div class="notification is-info">
NOTICE: To enable this feature, you must set <b>Metrics</b> option on <a href="/system/setting/">Setting</a> page first.