mirror of
https://github.com/cuigh/swirl
synced 2024-12-28 23:02:02 +00:00
Add service stats page based on Prometheus
This commit is contained in:
parent
5b60a024c5
commit
6fff495cda
19
Gopkg.lock
generated
19
Gopkg.lock
generated
@ -58,7 +58,7 @@
|
|||||||
"util/i18n",
|
"util/i18n",
|
||||||
"util/lazy"
|
"util/lazy"
|
||||||
]
|
]
|
||||||
revision = "ac7f3a5d4d43fd755008c87525a95698544843ec"
|
revision = "772f5a654db9ee95a5dc851e9562253bc0b2f11d"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
@ -169,6 +169,21 @@
|
|||||||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||||
version = "v0.8.0"
|
version = "v0.8.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/prometheus/client_golang"
|
||||||
|
packages = [
|
||||||
|
"api",
|
||||||
|
"api/prometheus/v1"
|
||||||
|
]
|
||||||
|
revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
|
||||||
|
version = "v0.9.0-pre1"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/prometheus/common"
|
||||||
|
packages = ["model"]
|
||||||
|
revision = "6fb6fce6f8b75884b92e1889c150403fc0872c5e"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
@ -209,6 +224,6 @@
|
|||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "aae5ea1cf0fb5fd50cb9659fb0abe58fc9546231ec91f9df9148c5df3e3cc194"
|
inputs-digest = "c829e2bd1f0dc356225caad79652e6732d0572e0e59580a5e6c5b6baab134755"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
@ -9,6 +9,7 @@ Swirl is a web management tool for Docker, focused on swarm cluster.
|
|||||||
* Swarm components management
|
* Swarm components management
|
||||||
* Image and container management
|
* Image and container management
|
||||||
* Compose management with deployment support
|
* Compose management with deployment support
|
||||||
|
* Service monitoring based on Prometheus
|
||||||
* LDAP authentication support
|
* LDAP authentication support
|
||||||
* Full permission control based on RBAC model
|
* Full permission control based on RBAC model
|
||||||
* Scale out as you want
|
* Scale out as you want
|
||||||
|
18919
assets/chart/chart.bundle.js
Normal file
18919
assets/chart/chart.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
10
assets/chart/chart.bundle.min.js
vendored
Normal file
10
assets/chart/chart.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -2155,4 +2155,137 @@ var Swirl;
|
|||||||
Volume.NewPage = NewPage;
|
Volume.NewPage = NewPage;
|
||||||
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
|
})(Volume = Swirl.Volume || (Swirl.Volume = {}));
|
||||||
})(Swirl || (Swirl = {}));
|
})(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
|
//# sourceMappingURL=swirl.js.map
|
File diff suppressed because one or more lines are too long
156
assets/swirl/ts/service/stats.ts
Normal file
156
assets/swirl/ts/service/stats.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
///<reference path="../core/core.ts" />
|
||||||
|
namespace Swirl.Service {
|
||||||
|
class MetricChartOptions {
|
||||||
|
type?: string = "line";
|
||||||
|
height?: number = 50;
|
||||||
|
title?: string;
|
||||||
|
labelX?: string;
|
||||||
|
labelY?: string;
|
||||||
|
tickY?: (value: number) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetricData {
|
||||||
|
cpu?: Chart.ChartDataSets[];
|
||||||
|
memory?: Chart.ChartDataSets[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class MetricChart {
|
||||||
|
private chart: any;
|
||||||
|
private readonly config: any;
|
||||||
|
private colors = [
|
||||||
|
'rgb(255, 99, 132)', // red
|
||||||
|
'rgb(75, 192, 192)', // green
|
||||||
|
'rgb(255, 159, 64)', // orange
|
||||||
|
'rgb(54, 162, 235)', // blue
|
||||||
|
'rgb(153, 102, 255)', // purple
|
||||||
|
'rgb(255, 205, 86)', // yellow
|
||||||
|
'rgb(201, 203, 207)', // grey
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(elem: string | Element, opts: MetricChartOptions) {
|
||||||
|
opts = $.extend(new MetricChartOptions(), opts);
|
||||||
|
this.config = {
|
||||||
|
type: opts.type,
|
||||||
|
data: {},
|
||||||
|
options: {
|
||||||
|
title: {
|
||||||
|
// display: true,
|
||||||
|
text: opts.title || 'NONE'
|
||||||
|
},
|
||||||
|
// legend: {
|
||||||
|
// position: "bottom"
|
||||||
|
// },
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
// easing: 'easeOutBounce',
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
// min: new Date(),
|
||||||
|
// max: new Date(),
|
||||||
|
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 = (<HTMLCanvasElement>$(elem).get(0)).getContext('2d');
|
||||||
|
if (opts.height) {
|
||||||
|
ctx.canvas.height = opts.height;
|
||||||
|
}
|
||||||
|
this.chart = new Chart(ctx, this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setData(datasets: Chart.ChartDataSets[]) {
|
||||||
|
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;
|
||||||
|
// ds.fill = false;
|
||||||
|
});
|
||||||
|
this.config.data.datasets = datasets;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StatsPage {
|
||||||
|
private cpu: MetricChart;
|
||||||
|
private memory: MetricChart;
|
||||||
|
private timer: number;
|
||||||
|
|
||||||
|
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: number): string {
|
||||||
|
return value + '%';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.memory = new MetricChart("#canvas-memory", {
|
||||||
|
tickY: function (value: number): string {
|
||||||
|
return value < 1024 ? (value + 'M') : (value / 1024) + 'G';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshData() {
|
||||||
|
this.loadData();
|
||||||
|
if ($("#cb-refresh").prop("checked")) {
|
||||||
|
this.timer = setTimeout(this.refreshData.bind(this), 15000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
590
assets/swirl/typings/chart.d.ts
vendored
Normal file
590
assets/swirl/typings/chart.d.ts
vendored
Normal file
@ -0,0 +1,590 @@
|
|||||||
|
// Type definitions for Chart.js 2.7
|
||||||
|
// Project: https://github.com/nnnick/Chart.js
|
||||||
|
// Definitions by: Alberto Nuti <https://github.com/anuti>
|
||||||
|
// Fabien Lavocat <https://github.com/FabienLavocat>
|
||||||
|
// KentarouTakeda <https://github.com/KentarouTakeda>
|
||||||
|
// Larry Bahr <https://github.com/larrybahr>
|
||||||
|
// Daniel Luz <https://github.com/mernen>
|
||||||
|
// Joseph Page <https://github.com/josefpaij>
|
||||||
|
// Dan Manastireanu <https://github.com/danmana>
|
||||||
|
// Guillaume Rodriguez <https://github.com/guillaume-ro-fr>
|
||||||
|
// Sergey Rubanov <https://github.com/chicoxyzzy>
|
||||||
|
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||||
|
// TypeScript Version: 2.3
|
||||||
|
|
||||||
|
declare class Chart {
|
||||||
|
static readonly Chart: typeof Chart;
|
||||||
|
constructor(
|
||||||
|
context: string | CanvasRenderingContext2D | HTMLCanvasElement | ArrayLike<CanvasRenderingContext2D | HTMLCanvasElement>,
|
||||||
|
options: Chart.ChartConfiguration
|
||||||
|
);
|
||||||
|
config: Chart.ChartConfiguration;
|
||||||
|
data: Chart.ChartData;
|
||||||
|
destroy: () => {};
|
||||||
|
update: (duration?: any, lazy?: any) => {};
|
||||||
|
render: (duration?: any, lazy?: any) => {};
|
||||||
|
stop: () => {};
|
||||||
|
resize: () => {};
|
||||||
|
clear: () => {};
|
||||||
|
toBase64: () => string;
|
||||||
|
generateLegend: () => {};
|
||||||
|
getElementAtEvent: (e: any) => {};
|
||||||
|
getElementsAtEvent: (e: any) => Array<{}>;
|
||||||
|
getDatasetAtEvent: (e: any) => Array<{}>;
|
||||||
|
ctx: CanvasRenderingContext2D|null;
|
||||||
|
canvas: HTMLCanvasElement|null;
|
||||||
|
chartArea: Chart.ChartArea;
|
||||||
|
static pluginService: PluginServiceStatic;
|
||||||
|
|
||||||
|
static defaults: {
|
||||||
|
global: Chart.ChartOptions & Chart.ChartFontOptions;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
static controllers: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tooltip Static Options
|
||||||
|
static Tooltip: Chart.ChartTooltipsStaticConfiguration;
|
||||||
|
static helpers: HelperStatic;
|
||||||
|
}
|
||||||
|
declare class PluginServiceStatic {
|
||||||
|
register(plugin: PluginServiceRegistrationOptions): void;
|
||||||
|
unregister(plugin: PluginServiceRegistrationOptions): void;
|
||||||
|
}
|
||||||
|
declare class HelperStatic {
|
||||||
|
color(color: string): Color;
|
||||||
|
}
|
||||||
|
declare class Color {
|
||||||
|
alpha(alpha: number): Color;
|
||||||
|
rgbString(): string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginServiceRegistrationOptions {
|
||||||
|
beforeInit?(chartInstance: Chart): void;
|
||||||
|
afterInit?(chartInstance: Chart): void;
|
||||||
|
|
||||||
|
resize?(chartInstance: Chart, newChartSize: Size): void;
|
||||||
|
|
||||||
|
beforeUpdate?(chartInstance: Chart): void;
|
||||||
|
afterScaleUpdate?(chartInstance: Chart): void;
|
||||||
|
beforeDatasetsUpdate?(chartInstance: Chart): void;
|
||||||
|
afterDatasetsUpdate?(chartInstance: Chart): void;
|
||||||
|
afterUpdate?(chartInstance: Chart): void;
|
||||||
|
|
||||||
|
// This is called at the start of a render. It is only called once, even if the animation will run for a number of frames. Use beforeDraw or afterDraw
|
||||||
|
// to do something on each animation frame
|
||||||
|
beforeRender?(chartInstance: Chart): void;
|
||||||
|
|
||||||
|
// Easing is for animation
|
||||||
|
beforeDraw?(chartInstance: Chart, easing: string): void;
|
||||||
|
afterDraw?(chartInstance: Chart, easing: string): void;
|
||||||
|
// Before the datasets are drawn but after scales are drawn
|
||||||
|
beforeDatasetsDraw?(chartInstance: Chart, easing: string): void;
|
||||||
|
afterDatasetsDraw?(chartInstance: Chart, easing: string): void;
|
||||||
|
|
||||||
|
// Called before drawing the `tooltip`. If any plugin returns `false`,
|
||||||
|
// the tooltip drawing is cancelled until another `render` is triggered.
|
||||||
|
beforeTooltipDraw?(chartInstance: Chart): void;
|
||||||
|
// Called after drawing the `tooltip`. Note that this hook will not,
|
||||||
|
// be called if the tooltip drawing has been previously cancelled.
|
||||||
|
afterTooltipDraw?(chartInstance: Chart): void;
|
||||||
|
|
||||||
|
destroy?(chartInstance: Chart): void;
|
||||||
|
|
||||||
|
// Called when an event occurs on the chart
|
||||||
|
beforeEvent?(chartInstance: Chart, event: Event): void;
|
||||||
|
afterEvent?(chartInstance: Chart, event: Event): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Size {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace Chart {
|
||||||
|
type ChartType = 'line' | 'bar' | 'radar' | 'doughnut' | 'polarArea' | 'bubble' | 'pie';
|
||||||
|
|
||||||
|
type TimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
type ScaleType = 'category' | 'linear' | 'logarithmic' | 'time' | 'radialLinear';
|
||||||
|
|
||||||
|
type PointStyle = 'circle' | 'cross' | 'crossRot' | 'dash' | 'line' | 'rect' | 'rectRounded' | 'rectRot' | 'star' | 'triangle';
|
||||||
|
|
||||||
|
type PositionType = 'left' | 'right' | 'top' | 'bottom';
|
||||||
|
|
||||||
|
interface ChartArea {
|
||||||
|
top: number;
|
||||||
|
right: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartLegendItem {
|
||||||
|
text?: string;
|
||||||
|
fillStyle?: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
lineCap?: string;
|
||||||
|
lineDash?: number[];
|
||||||
|
lineDashOffset?: number;
|
||||||
|
lineJoin?: string;
|
||||||
|
lineWidth?: number;
|
||||||
|
strokeStyle?: string;
|
||||||
|
pointStyle?: PointStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTooltipItem {
|
||||||
|
xLabel?: string;
|
||||||
|
yLabel?: string;
|
||||||
|
datasetIndex?: number;
|
||||||
|
index?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTooltipLabelColor {
|
||||||
|
borderColor: ChartColor;
|
||||||
|
backgroundColor: ChartColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTooltipCallback {
|
||||||
|
beforeTitle?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
title?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
afterTitle?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
beforeBody?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
beforeLabel?(tooltipItem: ChartTooltipItem, data: ChartData): string | string[];
|
||||||
|
label?(tooltipItem: ChartTooltipItem, data: ChartData): string | string[];
|
||||||
|
labelColor?(tooltipItem: ChartTooltipItem, chart: Chart): ChartTooltipLabelColor;
|
||||||
|
labelTextColor?(tooltipItem: ChartTooltipItem, chart: Chart): string;
|
||||||
|
afterLabel?(tooltipItem: ChartTooltipItem, data: ChartData): string | string[];
|
||||||
|
afterBody?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
beforeFooter?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
footer?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
afterFooter?(item: ChartTooltipItem[], data: ChartData): string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartAnimationParameter {
|
||||||
|
chartInstance?: any;
|
||||||
|
animationObject?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartPoint {
|
||||||
|
x?: number | string | Date;
|
||||||
|
y?: number;
|
||||||
|
r?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartConfiguration {
|
||||||
|
type?: ChartType | string;
|
||||||
|
data?: ChartData;
|
||||||
|
options?: ChartOptions;
|
||||||
|
// Plugins can require any options
|
||||||
|
plugins?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartData {
|
||||||
|
labels?: Array<string | string[]>;
|
||||||
|
datasets?: ChartDataSets[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartOptions {
|
||||||
|
responsive?: boolean;
|
||||||
|
responsiveAnimationDuration?: number;
|
||||||
|
aspectRatio?: number;
|
||||||
|
maintainAspectRatio?: boolean;
|
||||||
|
events?: string[];
|
||||||
|
onClick?(event?: MouseEvent, activeElements?: Array<{}>): any;
|
||||||
|
title?: ChartTitleOptions;
|
||||||
|
legend?: ChartLegendOptions;
|
||||||
|
tooltips?: ChartTooltipOptions;
|
||||||
|
hover?: ChartHoverOptions;
|
||||||
|
animation?: ChartAnimationOptions;
|
||||||
|
elements?: ChartElementsOptions;
|
||||||
|
layout?: ChartLayoutOptions;
|
||||||
|
scales?: ChartScales;
|
||||||
|
showLines?: boolean;
|
||||||
|
spanGaps?: boolean;
|
||||||
|
cutoutPercentage?: number;
|
||||||
|
circumference?: number;
|
||||||
|
rotation?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartFontOptions {
|
||||||
|
defaultFontColor?: ChartColor;
|
||||||
|
defaultFontFamily?: string;
|
||||||
|
defaultFontSize?: number;
|
||||||
|
defaultFontStyle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTitleOptions {
|
||||||
|
display?: boolean;
|
||||||
|
position?: PositionType;
|
||||||
|
fullWdith?: boolean;
|
||||||
|
fontSize?: number;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontColor?: ChartColor;
|
||||||
|
fontStyle?: string;
|
||||||
|
padding?: number;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartLegendOptions {
|
||||||
|
display?: boolean;
|
||||||
|
position?: PositionType;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
onClick?(event: MouseEvent, legendItem: ChartLegendItem): void;
|
||||||
|
onHover?(event: MouseEvent, legendItem: ChartLegendItem): void;
|
||||||
|
labels?: ChartLegendLabelOptions;
|
||||||
|
reverse?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartLegendLabelOptions {
|
||||||
|
boxWidth?: number;
|
||||||
|
fontSize?: number;
|
||||||
|
fontStyle?: string;
|
||||||
|
fontColor?: ChartColor;
|
||||||
|
fontFamily?: string;
|
||||||
|
padding?: number;
|
||||||
|
generateLabels?(chart: any): any;
|
||||||
|
filter?(item: ChartLegendItem, data: ChartData): any;
|
||||||
|
usePointStyle?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTooltipOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
custom?(a: any): void;
|
||||||
|
mode?: string;
|
||||||
|
intersect?: boolean;
|
||||||
|
backgroundColor?: ChartColor;
|
||||||
|
titleFontFamily?: string;
|
||||||
|
titleFontSize?: number;
|
||||||
|
titleFontStyle?: string;
|
||||||
|
titleFontColor?: ChartColor;
|
||||||
|
titleSpacing?: number;
|
||||||
|
titleMarginBottom?: number;
|
||||||
|
bodyFontFamily?: string;
|
||||||
|
bodyFontSize?: number;
|
||||||
|
bodyFontStyle?: string;
|
||||||
|
bodyFontColor?: ChartColor;
|
||||||
|
bodySpacing?: number;
|
||||||
|
footerFontFamily?: string;
|
||||||
|
footerFontSize?: number;
|
||||||
|
footerFontStyle?: string;
|
||||||
|
footerFontColor?: ChartColor;
|
||||||
|
footerSpacing?: number;
|
||||||
|
footerMarginTop?: number;
|
||||||
|
xPadding?: number;
|
||||||
|
yPadding?: number;
|
||||||
|
caretSize?: number;
|
||||||
|
cornerRadius?: number;
|
||||||
|
multiKeyBackground?: string;
|
||||||
|
callbacks?: ChartTooltipCallback;
|
||||||
|
filter?(item: ChartTooltipItem): boolean;
|
||||||
|
itemSort?(itemA: ChartTooltipItem, itemB: ChartTooltipItem): number;
|
||||||
|
position?: string;
|
||||||
|
caretPadding?: number;
|
||||||
|
displayColors?: boolean;
|
||||||
|
borderColor?: ChartColor;
|
||||||
|
borderWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartTooltipsStaticConfiguration {
|
||||||
|
positioners: {[mode: string]: ChartTooltipPositioner};
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartTooltipPositioner = (elements: any[], eventPosition: Point) => Point;
|
||||||
|
|
||||||
|
interface ChartHoverOptions {
|
||||||
|
mode?: string;
|
||||||
|
animationDuration?: number;
|
||||||
|
intersect?: boolean;
|
||||||
|
onHover?(active: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartAnimationObject {
|
||||||
|
currentStep?: number;
|
||||||
|
numSteps?: number;
|
||||||
|
easing?: string;
|
||||||
|
render?(arg: any): void;
|
||||||
|
onAnimationProgress?(arg: any): void;
|
||||||
|
onAnimationComplete?(arg: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartAnimationOptions {
|
||||||
|
duration?: number;
|
||||||
|
easing?: string;
|
||||||
|
onProgress?(chart: any): void;
|
||||||
|
onComplete?(chart: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartElementsOptions {
|
||||||
|
point?: ChartPointOptions;
|
||||||
|
line?: ChartLineOptions;
|
||||||
|
arc?: ChartArcOptions;
|
||||||
|
rectangle?: ChartRectangleOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartArcOptions {
|
||||||
|
backgroundColor?: ChartColor;
|
||||||
|
borderColor?: ChartColor;
|
||||||
|
borderWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartLineOptions {
|
||||||
|
tension?: number;
|
||||||
|
backgroundColor?: ChartColor;
|
||||||
|
borderWidth?: number;
|
||||||
|
borderColor?: ChartColor;
|
||||||
|
borderCapStyle?: string;
|
||||||
|
borderDash?: any[];
|
||||||
|
borderDashOffset?: number;
|
||||||
|
borderJoinStyle?: string;
|
||||||
|
capBezierPoints?: boolean;
|
||||||
|
fill?: 'zero' | 'top' | 'bottom' | boolean;
|
||||||
|
stepped?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartPointOptions {
|
||||||
|
radius?: number;
|
||||||
|
pointStyle?: PointStyle;
|
||||||
|
backgroundColor?: ChartColor;
|
||||||
|
borderWidth?: number;
|
||||||
|
borderColor?: ChartColor;
|
||||||
|
hitRadius?: number;
|
||||||
|
hoverRadius?: number;
|
||||||
|
hoverBorderWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartRectangleOptions {
|
||||||
|
backgroundColor?: ChartColor;
|
||||||
|
borderWidth?: number;
|
||||||
|
borderColor?: ChartColor;
|
||||||
|
borderSkipped?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartLayoutOptions {
|
||||||
|
padding?: ChartLayoutPaddingObject | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartLayoutPaddingObject {
|
||||||
|
top?: number;
|
||||||
|
right?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GridLineOptions {
|
||||||
|
display?: boolean;
|
||||||
|
color?: ChartColor;
|
||||||
|
borderDash?: number[];
|
||||||
|
borderDashOffset?: number;
|
||||||
|
lineWidth?: number;
|
||||||
|
drawBorder?: boolean;
|
||||||
|
drawOnChartArea?: boolean;
|
||||||
|
drawTicks?: boolean;
|
||||||
|
tickMarkLength?: number;
|
||||||
|
zeroLineWidth?: number;
|
||||||
|
zeroLineColor?: ChartColor;
|
||||||
|
zeroLineBorderDash?: number[];
|
||||||
|
zeroLineBorderDashOffset?: number;
|
||||||
|
offsetGridLines?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScaleTitleOptions {
|
||||||
|
display?: boolean;
|
||||||
|
labelString?: string;
|
||||||
|
fontColor?: ChartColor;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontStyle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TickOptions {
|
||||||
|
autoSkip?: boolean;
|
||||||
|
autoSkipPadding?: number;
|
||||||
|
callback?(value: any, index: any, values: any): string|number;
|
||||||
|
display?: boolean;
|
||||||
|
fontColor?: ChartColor;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontStyle?: string;
|
||||||
|
labelOffset?: number;
|
||||||
|
maxRotation?: number;
|
||||||
|
minRotation?: number;
|
||||||
|
mirror?: boolean;
|
||||||
|
padding?: number;
|
||||||
|
reverse?: boolean;
|
||||||
|
min?: any;
|
||||||
|
max?: any;
|
||||||
|
}
|
||||||
|
interface AngleLineOptions {
|
||||||
|
display?: boolean;
|
||||||
|
color?: ChartColor;
|
||||||
|
lineWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PointLabelOptions {
|
||||||
|
callback?(arg: any): any;
|
||||||
|
fontColor?: ChartColor;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: number;
|
||||||
|
fontStyle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TickOptions {
|
||||||
|
backdropColor?: ChartColor;
|
||||||
|
backdropPaddingX?: number;
|
||||||
|
backdropPaddingY?: number;
|
||||||
|
maxTicksLimit?: number;
|
||||||
|
showLabelBackdrop?: boolean;
|
||||||
|
}
|
||||||
|
interface LinearTickOptions extends TickOptions {
|
||||||
|
beginAtZero?: boolean;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
maxTicksLimit?: number;
|
||||||
|
stepSize?: number;
|
||||||
|
suggestedMin?: number;
|
||||||
|
suggestedMax?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogarithmicTickOptions extends TickOptions {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartColor = string | CanvasGradient | CanvasPattern | string[];
|
||||||
|
|
||||||
|
interface ChartDataSets {
|
||||||
|
cubicInterpolationMode?: 'default' | 'monotone';
|
||||||
|
backgroundColor?: ChartColor | ChartColor[];
|
||||||
|
borderWidth?: number | number[];
|
||||||
|
borderColor?: ChartColor | ChartColor[];
|
||||||
|
borderCapStyle?: string;
|
||||||
|
borderDash?: number[];
|
||||||
|
borderDashOffset?: number;
|
||||||
|
borderJoinStyle?: string;
|
||||||
|
borderSkipped?: PositionType;
|
||||||
|
data?: number[] | ChartPoint[];
|
||||||
|
fill?: boolean | number | string;
|
||||||
|
hoverBackgroundColor?: string | string[];
|
||||||
|
hoverBorderColor?: string | string[];
|
||||||
|
hoverBorderWidth?: number | number[];
|
||||||
|
label?: string;
|
||||||
|
lineTension?: number;
|
||||||
|
steppedLine?: 'before' | 'after' | boolean;
|
||||||
|
pointBorderColor?: ChartColor | ChartColor[];
|
||||||
|
pointBackgroundColor?: ChartColor | ChartColor[];
|
||||||
|
pointBorderWidth?: number | number[];
|
||||||
|
pointRadius?: number | number[];
|
||||||
|
pointHoverRadius?: number | number[];
|
||||||
|
pointHitRadius?: number | number[];
|
||||||
|
pointHoverBackgroundColor?: ChartColor | ChartColor[];
|
||||||
|
pointHoverBorderColor?: ChartColor | ChartColor[];
|
||||||
|
pointHoverBorderWidth?: number | number[];
|
||||||
|
pointStyle?: PointStyle | HTMLImageElement | Array<PointStyle | HTMLImageElement>;
|
||||||
|
xAxisID?: string;
|
||||||
|
yAxisID?: string;
|
||||||
|
type?: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
hideInLegendAndTooltip?: boolean;
|
||||||
|
showLine?: boolean;
|
||||||
|
stack?: string;
|
||||||
|
spanGaps?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartScales {
|
||||||
|
type?: ScaleType | string;
|
||||||
|
display?: boolean;
|
||||||
|
position?: PositionType | string;
|
||||||
|
gridLines?: GridLineOptions;
|
||||||
|
scaleLabel?: ScaleTitleOptions;
|
||||||
|
ticks?: TickOptions;
|
||||||
|
xAxes?: ChartXAxe[];
|
||||||
|
yAxes?: ChartYAxe[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommonAxe {
|
||||||
|
type?: ScaleType | string;
|
||||||
|
display?: boolean;
|
||||||
|
id?: string;
|
||||||
|
stacked?: boolean;
|
||||||
|
position?: string;
|
||||||
|
ticks?: TickOptions;
|
||||||
|
gridLines?: GridLineOptions;
|
||||||
|
barThickness?: number;
|
||||||
|
scaleLabel?: ScaleTitleOptions;
|
||||||
|
beforeUpdate?(scale?: any): void;
|
||||||
|
beforeSetDimension?(scale?: any): void;
|
||||||
|
beforeDataLimits?(scale?: any): void;
|
||||||
|
beforeBuildTicks?(scale?: any): void;
|
||||||
|
beforeTickToLabelConversion?(scale?: any): void;
|
||||||
|
beforeCalculateTickRotation?(scale?: any): void;
|
||||||
|
beforeFit?(scale?: any): void;
|
||||||
|
afterUpdate?(scale?: any): void;
|
||||||
|
afterSetDimension?(scale?: any): void;
|
||||||
|
afterDataLimits?(scale?: any): void;
|
||||||
|
afterBuildTicks?(scale?: any): void;
|
||||||
|
afterTickToLabelConversion?(scale?: any): void;
|
||||||
|
afterCalculateTickRotation?(scale?: any): void;
|
||||||
|
afterFit?(scale?: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChartXAxe extends CommonAxe {
|
||||||
|
categoryPercentage?: number;
|
||||||
|
barPercentage?: number;
|
||||||
|
time?: TimeScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line no-empty-interface
|
||||||
|
interface ChartYAxe extends CommonAxe {
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LinearScale extends ChartScales {
|
||||||
|
ticks?: LinearTickOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogarithmicScale extends ChartScales {
|
||||||
|
ticks?: LogarithmicTickOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimeDisplayFormat {
|
||||||
|
millisecond?: string;
|
||||||
|
second?: string;
|
||||||
|
minute?: string;
|
||||||
|
hour?: string;
|
||||||
|
day?: string;
|
||||||
|
week?: string;
|
||||||
|
month?: string;
|
||||||
|
quarter?: string;
|
||||||
|
year?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TimeScale extends ChartScales {
|
||||||
|
displayFormats?: TimeDisplayFormat;
|
||||||
|
isoWeekday?: boolean;
|
||||||
|
max?: string;
|
||||||
|
min?: string;
|
||||||
|
parser?: string | ((arg: any) => any);
|
||||||
|
round?: TimeUnit;
|
||||||
|
tooltipFormat?: string;
|
||||||
|
unit?: TimeUnit;
|
||||||
|
unitStepSize?: number;
|
||||||
|
stepSize?: number;
|
||||||
|
minUnit?: TimeUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RadialLinearScale {
|
||||||
|
lineArc?: boolean;
|
||||||
|
angleLines?: AngleLineOptions;
|
||||||
|
pointLabels?: PointLabelOptions;
|
||||||
|
ticks?: TickOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Point {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export = Chart;
|
||||||
|
export as namespace Chart;
|
@ -20,7 +20,7 @@ var mgr = &manager{}
|
|||||||
type manager struct {
|
type manager struct {
|
||||||
client *client.Client
|
client *client.Client
|
||||||
locker sync.Mutex
|
locker sync.Mutex
|
||||||
logger *log.Logger
|
logger log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) Do(fn func(ctx context.Context, cli *client.Client) error) (err error) {
|
func (m *manager) Do(fn func(ctx context.Context, cli *client.Client) error) (err error) {
|
||||||
@ -51,7 +51,7 @@ func (m *manager) Client() (ctx context.Context, cli *client.Client, err error)
|
|||||||
return context.TODO(), m.client, nil
|
return context.TODO(), m.client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manager) Logger() *log.Logger {
|
func (m *manager) Logger() log.Logger {
|
||||||
if m.logger == nil {
|
if m.logger == nil {
|
||||||
m.locker.Lock()
|
m.locker.Lock()
|
||||||
defer m.locker.Unlock()
|
defer m.locker.Unlock()
|
||||||
|
@ -77,6 +77,7 @@ menu.raw: Raw
|
|||||||
menu.edit: Edit
|
menu.edit: Edit
|
||||||
menu.log: Logs
|
menu.log: Logs
|
||||||
menu.perm: Permission
|
menu.perm: Permission
|
||||||
|
menu.stats: Stats
|
||||||
|
|
||||||
# login page
|
# login page
|
||||||
login.title: Sign in to Swirl
|
login.title: Sign in to Swirl
|
||||||
|
@ -77,6 +77,7 @@ menu.raw: 原始
|
|||||||
menu.edit: 编辑
|
menu.edit: 编辑
|
||||||
menu.log: 日志
|
menu.log: 日志
|
||||||
menu.perm: 权限
|
menu.perm: 权限
|
||||||
|
menu.stats: 状态
|
||||||
|
|
||||||
# login page
|
# login page
|
||||||
login.title: 登录到 Swirl
|
login.title: 登录到 Swirl
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cuigh/auxo/data"
|
||||||
"github.com/cuigh/auxo/data/set"
|
"github.com/cuigh/auxo/data/set"
|
||||||
"github.com/cuigh/auxo/errors"
|
"github.com/cuigh/auxo/errors"
|
||||||
|
"github.com/cuigh/auxo/ext/times"
|
||||||
"github.com/cuigh/auxo/net/web"
|
"github.com/cuigh/auxo/net/web"
|
||||||
"github.com/cuigh/auxo/util/cast"
|
"github.com/cuigh/auxo/util/cast"
|
||||||
"github.com/cuigh/swirl/biz"
|
"github.com/cuigh/swirl/biz"
|
||||||
"github.com/cuigh/swirl/biz/docker"
|
"github.com/cuigh/swirl/biz/docker"
|
||||||
"github.com/cuigh/swirl/misc"
|
"github.com/cuigh/swirl/misc"
|
||||||
"github.com/cuigh/swirl/model"
|
"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
|
// ServiceController is a controller of docker service
|
||||||
@ -29,6 +37,8 @@ type ServiceController struct {
|
|||||||
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"service.update" authorize:"!" perm:"write,service,name"`
|
Update web.HandlerFunc `path:"/:name/edit" method:"post" name:"service.update" authorize:"!" perm:"write,service,name"`
|
||||||
PermEdit web.HandlerFunc `path:"/:name/perm" name:"service.perm.edit" authorize:"!" perm:"write,service,name"`
|
PermEdit web.HandlerFunc `path:"/:name/perm" name:"service.perm.edit" authorize:"!" perm:"write,service,name"`
|
||||||
PermUpdate web.HandlerFunc `path:"/:name/perm" method:"post" name:"service.perm.update" authorize:"!" perm:"write,service,name"`
|
PermUpdate web.HandlerFunc `path:"/:name/perm" method:"post" name:"service.perm.update" authorize:"!" perm:"write,service,name"`
|
||||||
|
Stats web.HandlerFunc `path:"/:name/stats" name:"service.stats" authorize:"!" perm:"read,service,name"`
|
||||||
|
Metrics web.HandlerFunc `path:"/:name/metrics" name:"service.metrics" authorize:"?"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service creates an instance of ServiceController
|
// Service creates an instance of ServiceController
|
||||||
@ -47,6 +57,8 @@ func Service() (c *ServiceController) {
|
|||||||
Rollback: serviceRollback,
|
Rollback: serviceRollback,
|
||||||
PermEdit: servicePermEdit,
|
PermEdit: servicePermEdit,
|
||||||
PermUpdate: permUpdate("service", "name"),
|
PermUpdate: permUpdate("service", "name"),
|
||||||
|
Stats: serviceStats,
|
||||||
|
Metrics: serviceMetrics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,3 +281,130 @@ func servicePermEdit(ctx web.Context) error {
|
|||||||
m := newModel(ctx).Set("Name", name)
|
m := newModel(ctx).Set("Name", name)
|
||||||
return permEdit(ctx, "service", name, "service/perm", m)
|
return permEdit(ctx, "service", name, "service/perm", m)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serviceStats(ctx web.Context) error {
|
||||||
|
name := ctx.P("name")
|
||||||
|
service, _, err := docker.ServiceInspect(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, _, err := docker.TaskList(&model.TaskListArgs{Service: name})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setting, err := biz.Setting.Get()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "")
|
||||||
|
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")
|
||||||
|
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"]),
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
return ctx.JSON(m)
|
||||||
|
}
|
||||||
|
@ -57,7 +57,7 @@ func (d *database) Run(cmd, result interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Dao struct {
|
type Dao struct {
|
||||||
logger *log.Logger
|
logger log.Logger
|
||||||
session *mgo.Session
|
session *mgo.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
main.go
2
main.go
@ -26,7 +26,7 @@ func main() {
|
|||||||
misc.BindOptions()
|
misc.BindOptions()
|
||||||
|
|
||||||
app.Name = "Swirl"
|
app.Name = "Swirl"
|
||||||
app.Version = "0.6.8"
|
app.Version = "0.6.9"
|
||||||
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
app.Desc = "A web management UI for Docker, focused on swarm cluster"
|
||||||
app.Action = func(ctx *app.Context) {
|
app.Action = func(ctx *app.Context) {
|
||||||
misc.LoadOptions()
|
misc.LoadOptions()
|
||||||
|
@ -43,7 +43,10 @@ type Setting struct {
|
|||||||
Name string `bson:"name" json:"name,omitempty"` // Asia/Shanghai
|
Name string `bson:"name" json:"name,omitempty"` // Asia/Shanghai
|
||||||
Offset int32 `bson:"offset" json:"offset,omitempty"` // seconds east of UTC
|
Offset int32 `bson:"offset" json:"offset,omitempty"` // seconds east of UTC
|
||||||
} `bson:"tz" json:"tz,omitempty"`
|
} `bson:"tz" json:"tz,omitempty"`
|
||||||
Language string `bson:"lang" json:"lang,omitempty"`
|
Language string `bson:"lang" json:"lang,omitempty"`
|
||||||
|
Metrics struct {
|
||||||
|
Prometheus string `bson:"prometheus" json:"prometheus"`
|
||||||
|
} `bson:"metrics" json:"metrics"`
|
||||||
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
|
UpdatedBy string `bson:"updated_by" json:"updated_by,omitempty"`
|
||||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
|
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,7 @@ var Perms = []PermGroup{
|
|||||||
{Key: "service.detail", Text: "View detail"},
|
{Key: "service.detail", Text: "View detail"},
|
||||||
{Key: "service.raw", Text: "View raw"},
|
{Key: "service.raw", Text: "View raw"},
|
||||||
{Key: "service.logs", Text: "View logs"},
|
{Key: "service.logs", Text: "View logs"},
|
||||||
|
{Key: "service.stats", Text: "View stats"},
|
||||||
{Key: "service.edit", Text: "View edit"},
|
{Key: "service.edit", Text: "View edit"},
|
||||||
{Key: "service.create", Text: "Create"},
|
{Key: "service.create", Text: "Create"},
|
||||||
{Key: "service.delete", Text: "Delete"},
|
{Key: "service.delete", Text: "Delete"},
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/logs">{{ i18n("menu.log") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/stats">{{ i18n("menu.stats") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/logs">{{ i18n("menu.log") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||||
<a class="navbar-item is-tab is-active" href="/service/{{.Service.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
<a class="navbar-item is-tab is-active" href="/service/{{.Service.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Name}}/stats">{{ i18n("menu.stats") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
<a class="navbar-item is-tab is-active" href="/service/{{.Service}}/logs">{{ i18n("menu.log") }}</a>
|
<a class="navbar-item is-tab is-active" href="/service/{{.Service}}/logs">{{ i18n("menu.log") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/edit">{{ i18n("menu.edit") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/edit">{{ i18n("menu.edit") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/stats">{{ i18n("menu.stats") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
<a class="navbar-item is-tab" href="/service/{{.Name}}/logs">{{ i18n("menu.log") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||||
<a class="navbar-item is-tab is-active" href="/service/{{.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
<a class="navbar-item is-tab is-active" href="/service/{{.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Name}}/stats">{{ i18n("menu.stats") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/logs">{{ i18n("menu.log") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/logs">{{ i18n("menu.log") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/edit">{{ i18n("menu.edit") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/edit">{{ i18n("menu.edit") }}</a>
|
||||||
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/perm">{{ i18n("menu.perm") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service}}/stats">{{ i18n("menu.stats") }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
108
views/service/stats.jet
Normal file
108
views/service/stats.jet
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{{ extends "_base" }}
|
||||||
|
{{ import "../_modules/detail" }}
|
||||||
|
{{ import "../_modules/form" }}
|
||||||
|
|
||||||
|
{{ block script() }}
|
||||||
|
<script src="/assets/chart/chart.bundle.min.js?v=2.7.2"></script>
|
||||||
|
<script>$(() => new Swirl.Service.StatsPage())</script>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ block body_content() }}
|
||||||
|
<div class="container">
|
||||||
|
<nav class="breadcrumb has-succeeds-separator is-small is-marginless" aria-label="breadcrumbs">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">{{ i18n("menu.dashboard") }}</a></li>
|
||||||
|
<li><a href="/service/">{{ i18n("menu.service") }}</a></li>
|
||||||
|
<li class="is-active"><a>{{ i18n("menu.stats") }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="hero is-small is-light">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="title is-2">
|
||||||
|
{{ .Service.Spec.Name }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="navbar has-shadow">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/detail">{{ i18n("menu.detail") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/raw">{{ i18n("menu.raw") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/logs">{{ i18n("menu.log") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/edit">{{ i18n("menu.edit") }}</a>
|
||||||
|
<a class="navbar-item is-tab" href="/service/{{.Service.Spec.Name}}/perm">{{ i18n("menu.perm") }}</a>
|
||||||
|
<a class="navbar-item is-tab is-active" href="/service/{{.Service.Spec.Name}}/stats">{{ i18n("menu.stats") }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
{{ if .Metrics }}
|
||||||
|
<nav class="level">
|
||||||
|
<form>
|
||||||
|
<div class="level-left">
|
||||||
|
<div class="level-item">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control">
|
||||||
|
<a class="button is-static">Time</a>
|
||||||
|
</p>
|
||||||
|
<p class="control">
|
||||||
|
<div class="select">
|
||||||
|
<select id="cb-time" name="time">
|
||||||
|
{{ yield option(value="30m", label="Last 30 minutes", selected=.Time) }}
|
||||||
|
{{ yield option(value="1h", label="Last 1 hour", selected=.Time) }}
|
||||||
|
{{ yield option(value="3h", label="Last 3 hours", selected=.Time) }}
|
||||||
|
{{ yield option(value="6h", label="Last 6 hours", selected=.Time) }}
|
||||||
|
{{ yield option(value="12h", label="Last 12 hours", selected=.Time) }}
|
||||||
|
{{ yield option(value="24h", label="Last 24 hours", selected=.Time) }}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="level-item">
|
||||||
|
<div class="field">
|
||||||
|
<input id="cb-refresh" name="refresh" value="true" type="checkbox" class="switch is-success is-rounded"{{if .Refresh}} checked{{end}}>
|
||||||
|
<label for="cb-refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<div class="block-header">
|
||||||
|
<p>CPU</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ 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.
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<a href="/service/" class="button is-primary">
|
||||||
|
<span class="icon"><i class="fas fa-reply"></i></span>
|
||||||
|
<span>{{ i18n("button.return") }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
@ -219,12 +219,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset id="fs-metrics">
|
||||||
|
<legend class="lead is-5 is-bordered">Metrics</legend>
|
||||||
|
<div class="field is-horizontal">
|
||||||
|
<div class="field-label is-normal">
|
||||||
|
<label class="label">Prometheus address</label>
|
||||||
|
</div>
|
||||||
|
<div class="field-body">
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<input name="metrics.prometheus" value="{{ .Setting.Metrics.Prometheus }}" class="input" type="text" placeholder="e.g. http://prometheus.xxx.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<hr>
|
<hr>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button class="button is-primary" type="submit">{{ i18n("button.save") }}</button>
|
<button class="button is-primary" type="submit">{{ i18n("button.save") }}</button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="notification is-info">
|
||||||
|
NOTICE: To activate modifications, you must restart swirl.
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
Loading…
Reference in New Issue
Block a user