Add service stats page based on Prometheus

This commit is contained in:
cuigh 2018-03-09 19:00:03 +08:00
parent 5b60a024c5
commit 6fff495cda
23 changed files with 20108 additions and 8 deletions

19
Gopkg.lock generated
View File

@ -58,7 +58,7 @@
"util/i18n",
"util/lazy"
]
revision = "ac7f3a5d4d43fd755008c87525a95698544843ec"
revision = "772f5a654db9ee95a5dc851e9562253bc0b2f11d"
[[projects]]
branch = "master"
@ -169,6 +169,21 @@
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
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]]
branch = "master"
name = "golang.org/x/crypto"
@ -209,6 +224,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "aae5ea1cf0fb5fd50cb9659fb0abe58fc9546231ec91f9df9148c5df3e3cc194"
inputs-digest = "c829e2bd1f0dc356225caad79652e6732d0572e0e59580a5e6c5b6baab134755"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -9,6 +9,7 @@ Swirl is a web management tool for Docker, focused on swarm cluster.
* Swarm components management
* Image and container management
* Compose management with deployment support
* Service monitoring based on Prometheus
* LDAP authentication support
* Full permission control based on RBAC model
* Scale out as you want

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

File diff suppressed because one or more lines are too long

View File

@ -2155,4 +2155,137 @@ 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

@ -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
View 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;

View File

@ -20,7 +20,7 @@ var mgr = &manager{}
type manager struct {
client *client.Client
locker sync.Mutex
logger *log.Logger
logger log.Logger
}
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
}
func (m *manager) Logger() *log.Logger {
func (m *manager) Logger() log.Logger {
if m.logger == nil {
m.locker.Lock()
defer m.locker.Unlock()

View File

@ -77,6 +77,7 @@ menu.raw: Raw
menu.edit: Edit
menu.log: Logs
menu.perm: Permission
menu.stats: Stats
# login page
login.title: Sign in to Swirl

View File

@ -77,6 +77,7 @@ menu.raw: 原始
menu.edit: 编辑
menu.log: 日志
menu.perm: 权限
menu.stats: 状态
# login page
login.title: 登录到 Swirl

View File

@ -1,17 +1,25 @@
package controller
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"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
@ -29,6 +37,8 @@ type ServiceController struct {
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"`
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
@ -47,6 +57,8 @@ func Service() (c *ServiceController) {
Rollback: serviceRollback,
PermEdit: servicePermEdit,
PermUpdate: permUpdate("service", "name"),
Stats: serviceStats,
Metrics: serviceMetrics,
}
}
@ -269,3 +281,130 @@ func servicePermEdit(ctx web.Context) error {
m := newModel(ctx).Set("Name", name)
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)
}

View File

@ -57,7 +57,7 @@ func (d *database) Run(cmd, result interface{}) error {
}
type Dao struct {
logger *log.Logger
logger log.Logger
session *mgo.Session
}

View File

@ -26,7 +26,7 @@ func main() {
misc.BindOptions()
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.Action = func(ctx *app.Context) {
misc.LoadOptions()

View File

@ -43,7 +43,10 @@ type Setting struct {
Name string `bson:"name" json:"name,omitempty"` // Asia/Shanghai
Offset int32 `bson:"offset" json:"offset,omitempty"` // seconds east of UTC
} `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"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at,omitempty"`
}

View File

@ -85,6 +85,7 @@ var Perms = []PermGroup{
{Key: "service.detail", Text: "View detail"},
{Key: "service.raw", Text: "View raw"},
{Key: "service.logs", Text: "View logs"},
{Key: "service.stats", Text: "View stats"},
{Key: "service.edit", Text: "View edit"},
{Key: "service.create", Text: "Create"},
{Key: "service.delete", Text: "Delete"},

View File

@ -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}}/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}}/stats">{{ i18n("menu.stats") }}</a>
</div>
</div>
</nav>

View File

@ -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 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}}/stats">{{ i18n("menu.stats") }}</a>
</div>
</div>
</nav>

View File

@ -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" 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}}/stats">{{ i18n("menu.stats") }}</a>
</div>
</div>
</nav>

View File

@ -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}}/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" href="/service/{{.Name}}/stats">{{ i18n("menu.stats") }}</a>
</div>
</div>
</nav>

View File

@ -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}}/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}}/stats">{{ i18n("menu.stats") }}</a>
</div>
</div>
</nav>

108
views/service/stats.jet Normal file
View 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 }}

View File

@ -219,12 +219,30 @@
</div>
</div>
</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>
<div class="field">
<p class="control">
<button class="button is-primary" type="submit">{{ i18n("button.save") }}</button>
</p>
</div>
<div class="notification is-info">
NOTICE: To activate modifications, you must restart swirl.
</div>
</form>
</div>
</section>