diff --git a/internal/rpc/rpc_client.go b/internal/rpc/rpc_client.go index 8a568a5a..1eeff2f2 100644 --- a/internal/rpc/rpc_client.go +++ b/internal/rpc/rpc_client.go @@ -115,6 +115,10 @@ func (this *RPCClient) ServerRegionProviderMonthlyStatRPC() pb.ServerRegionProvi return pb.NewServerRegionProviderMonthlyStatServiceClient(this.pickConn()) } +func (this *RPCClient) ServerHTTPFirewallDailyStatRPC() pb.ServerHTTPFirewallDailyStatServiceClient { + return pb.NewServerHTTPFirewallDailyStatServiceClient(this.pickConn()) +} + func (this *RPCClient) ServerGroupRPC() pb.ServerGroupServiceClient { return pb.NewServerGroupServiceClient(this.pickConn()) } diff --git a/internal/web/actions/default/servers/server/stat/init.go b/internal/web/actions/default/servers/server/stat/init.go index d1f5942c..4cb64f65 100644 --- a/internal/web/actions/default/servers/server/stat/init.go +++ b/internal/web/actions/default/servers/server/stat/init.go @@ -16,6 +16,7 @@ func init() { Get("", new(IndexAction)). Get("/providers", new(ProvidersAction)). Get("/clients", new(ClientsAction)). + Get("/waf", new(WafAction)). EndAll() }) } diff --git a/internal/web/actions/default/servers/server/stat/waf.go b/internal/web/actions/default/servers/server/stat/waf.go new file mode 100644 index 00000000..16d496a0 --- /dev/null +++ b/internal/web/actions/default/servers/server/stat/waf.go @@ -0,0 +1,78 @@ +package stat + +import ( + "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils" + "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" + "github.com/iwind/TeaGo/maps" + timeutil "github.com/iwind/TeaGo/utils/time" +) + +type WafAction struct { + actionutils.ParentAction +} + +func (this *WafAction) Init() { + this.Nav("", "stat", "") + this.SecondMenu("waf") +} + +func (this *WafAction) RunGet(params struct { + ServerId int64 +}) { + // 统计数据 + resp, err := this.RPC().ServerHTTPFirewallDailyStatRPC().ComposeServerHTTPFirewallDashboard(this.AdminContext(), &pb.ComposeServerHTTPFirewallDashboardRequest{ + Day: timeutil.Format("Ymd"), + ServerId: params.ServerId, + }) + if err != nil { + this.ErrorPage(err) + return + } + + this.Data["countDailyLog"] = resp.CountDailyLog + this.Data["countDailyBlock"] = resp.CountDailyBlock + this.Data["countDailyCaptcha"] = resp.CountDailyCaptcha + this.Data["countWeeklyBlock"] = resp.CountWeeklyBlock + this.Data["countMonthlyBlock"] = resp.CountMonthlyBlock + + // 分组 + groupStatMaps := []maps.Map{} + for _, group := range resp.HttpFirewallRuleGroups { + groupStatMaps = append(groupStatMaps, maps.Map{ + "group": maps.Map{ + "id": group.HttpFirewallRuleGroup.Id, + "name": group.HttpFirewallRuleGroup.Name, + }, + "count": group.Count, + }) + } + this.Data["groupStats"] = groupStatMaps + + // 每日趋势 + logStatMaps := []maps.Map{} + blockStatMaps := []maps.Map{} + captchaStatMaps := []maps.Map{} + for _, stat := range resp.LogDailyStats { + logStatMaps = append(logStatMaps, maps.Map{ + "day": stat.Day, + "count": stat.Count, + }) + } + for _, stat := range resp.BlockDailyStats { + blockStatMaps = append(blockStatMaps, maps.Map{ + "day": stat.Day, + "count": stat.Count, + }) + } + for _, stat := range resp.CaptchaDailyStats { + captchaStatMaps = append(captchaStatMaps, maps.Map{ + "day": stat.Day, + "count": stat.Count, + }) + } + this.Data["logDailyStats"] = logStatMaps + this.Data["blockDailyStats"] = blockStatMaps + this.Data["captchaDailyStats"] = captchaStatMaps + + this.Show() +} diff --git a/internal/web/actions/default/servers/serverutils/server_helper.go b/internal/web/actions/default/servers/serverutils/server_helper.go index 4fb81990..93cd98d9 100644 --- a/internal/web/actions/default/servers/serverutils/server_helper.go +++ b/internal/web/actions/default/servers/serverutils/server_helper.go @@ -171,6 +171,11 @@ func (this *ServerHelper) createStatMenu(secondMenuItem string, serverIdString s "url": "/servers/server/stat/clients?serverId=" + serverIdString, "isActive": secondMenuItem == "client", }) + menuItems = append(menuItems, maps.Map{ + "name": "WAF", + "url": "/servers/server/stat/waf?serverId=" + serverIdString, + "isActive": secondMenuItem == "waf", + }) return menuItems } diff --git a/web/views/@default/servers/server/stat/waf.css b/web/views/@default/servers/server/stat/waf.css new file mode 100644 index 00000000..346ec7de --- /dev/null +++ b/web/views/@default/servers/server/stat/waf.css @@ -0,0 +1,26 @@ +.grid { + margin-top: 2em !important; + margin-left: 2em !important; +} +.grid .column { + margin-bottom: 2em; + border-right: 1px #eee solid; +} +.grid .column div.value { + margin-top: 1.5em; +} +.grid .column div.value span { + font-size: 2em; + margin-right: 0.2em; +} +.grid .column.no-border { + border-right: 0; +} +.chart-box { + height: 20em; +} +h4 .color-span { + font-size: 0.6em; + padding: 2px 4px; +} +/*# sourceMappingURL=waf.css.map */ \ No newline at end of file diff --git a/web/views/@default/servers/server/stat/waf.css.map b/web/views/@default/servers/server/stat/waf.css.map new file mode 100644 index 00000000..b87862d2 --- /dev/null +++ b/web/views/@default/servers/server/stat/waf.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["waf.less"],"names":[],"mappings":"AAAA;EACC,0BAAA;EACA,2BAAA;;AAFD,KAIC;EACC,kBAAA;EACA,4BAAA;;AANF,KAIC,QAIC,IAAG;EACF,iBAAA;;AATH,KAIC,QAIC,IAAG,MAGF;EACC,cAAA;EACA,mBAAA;;AAbJ,KAkBC,QAAO;EACN,eAAA;;AAIF;EACC,YAAA;;AAGD,EAAG;EACF,gBAAA;EACA,gBAAA","file":"waf.css"} \ No newline at end of file diff --git a/web/views/@default/servers/server/stat/waf.html b/web/views/@default/servers/server/stat/waf.html new file mode 100644 index 00000000..a3d96991 --- /dev/null +++ b/web/views/@default/servers/server/stat/waf.html @@ -0,0 +1,47 @@ +{$layout} + +{$var "header"} + + +{$end} + +{$template "/left_menu"} +
+
+
+

今日拦截

+
{{countDailyBlock}}
+
+ +
+

今日验证码验证

+
{{countDailyCaptcha}}
+
+ +
+

今日记录

+
{{countDailyLog}}
+
+ +
+

本周拦截

+
{{countWeeklyBlock}}
+
+ +
+

本月拦截

+
{{countMonthlyBlock}}
+
+
+ +

近15日趋势 + 全部 + 拦截 + 验证码 + 记录 +

+
+ +

拦截类型分布

+
+
\ No newline at end of file diff --git a/web/views/@default/servers/server/stat/waf.js b/web/views/@default/servers/server/stat/waf.js new file mode 100644 index 00000000..63e374e1 --- /dev/null +++ b/web/views/@default/servers/server/stat/waf.js @@ -0,0 +1,198 @@ +Tea.context(function () { + this.$delay(function () { + let that = this + + this.totalDailyStats = this.logDailyStats.map(function (v, k) { + return { + day: v.day, + count: that.logDailyStats[k].count + that.blockDailyStats[k].count + that.captchaDailyStats[k].count + } + }) + let dailyUnit = this.processMaxUnit(this.totalDailyStats) + this.reloadLineChart("daily-chart", "规则分组", this.totalDailyStats, function (v) { + return v.day.substring(4, 6) + "-" + v.day.substring(6) + }, function (args) { + + return that.logDailyStats[args.dataIndex].day.substring(4, 6) + "-" + that.logDailyStats[args.dataIndex].day.substring(6) + ": 拦截: " + + teaweb.formatNumber(that.blockDailyStats[args.dataIndex].count) + ", 验证码: " + teaweb.formatNumber(that.captchaDailyStats[args.dataIndex].count) + ", 记录: " + teaweb.formatNumber(that.logDailyStats[args.dataIndex].count) + }, dailyUnit) + + let groupUnit = this.processMaxUnit(this.groupStats) + let total = this.groupStats.$sum(function (k, v) { + return v.rawCount + }) + this.reloadBarChart("group-chart", "规则分组", this.groupStats, function (v) { + return v.group.name + }, function (args) { + let percent = "" + if (total > 0) { + percent = ", 占比: " + (Math.round(that.groupStats[args.dataIndex].rawCount * 100 * 100 / total) / 100) + "%" + } + return that.groupStats[args.dataIndex].group.name + ": " + teaweb.formatNumber(that.groupStats[args.dataIndex].rawCount) + percent + }, groupUnit) + + window.addEventListener("resize", function () { + that.resizeChart("daily-chart") + that.resizeChart("group-chart") + }) + }) + + this.reloadLineChart = function (chartId, name, stats, xFunc, tooltipFunc, unit) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let that = this + let chart = echarts.init(chartBox) + let option = { + xAxis: { + data: stats.map(xFunc) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: tooltipFunc + }, + grid: { + left: 40, + top: 10, + right: 20, + bottom: 20 + }, + series: [ + { + name: name, + type: "line", + data: this.totalDailyStats.map(function (v, index) { + return that.totalDailyStats[index].count; + }), + areaStyle: {}, + itemStyle: { + color: "#9DD3E8" + } + }, + { + name: name, + type: "line", + data: this.logDailyStats.map(function (v) { + return v.count; + }), + itemStyle: { + color: "#879BD7" + } + }, + { + name: name, + type: "line", + data: this.blockDailyStats.map(function (v) { + return v.count; + }), + itemStyle: { + color: "#F39494" + } + }, + { + name: name, + type: "line", + data: this.captchaDailyStats.map(function (v) { + return v.count; + }), + itemStyle: { + color: "#FBD88A" + } + } + ], + animation: true + } + chart.setOption(option) + chart.resize() + } + + this.reloadBarChart = function (chartId, name, stats, xFunc, tooltipFunc, unit) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let chart = echarts.init(chartBox) + let option = { + xAxis: { + data: stats.map(xFunc) + }, + yAxis: { + axisLabel: { + formatter: function (value) { + return value + unit + } + } + }, + tooltip: { + show: true, + trigger: "item", + formatter: tooltipFunc + }, + grid: { + left: 40, + top: 10, + right: 20, + bottom: 20 + }, + series: [ + { + name: name, + type: "bar", + data: stats.map(function (v) { + return v.count; + }), + itemStyle: { + color: "#9DD3E8" + }, + barWidth: "20em" + } + ], + animation: true + } + chart.setOption(option) + chart.resize() + } + + this.resizeChart = function (chartId) { + let chartBox = document.getElementById(chartId) + if (chartBox == null) { + return + } + let chart = echarts.init(chartBox) + chart.resize() + } + + this.processMaxUnit = function (stats) { + let max = stats.$map(function (k, v) { + return v.count + }).$max() + let divider = 0 + let unit = "" + if (max >= 1000 * 1000 * 1000) { + unit = "B" + divider = 1000 * 1000 * 1000 + } else if (max >= 1000 * 1000) { + unit = "M" + divider = 1000 * 1000 + } else if (max >= 1000) { + unit = "K" + divider = 1000 + } + stats.forEach(function (v) { + v.rawCount = v.count + if (divider > 0) { + v.count /= divider + } + }) + return unit + } +}) \ No newline at end of file diff --git a/web/views/@default/servers/server/stat/waf.less b/web/views/@default/servers/server/stat/waf.less new file mode 100644 index 00000000..4978e44a --- /dev/null +++ b/web/views/@default/servers/server/stat/waf.less @@ -0,0 +1,31 @@ +.grid { + margin-top: 2em !important; + margin-left: 2em !important; + + .column { + margin-bottom: 2em; + border-right: 1px #eee solid; + + div.value { + margin-top: 1.5em; + + span { + font-size: 2em; + margin-right: 0.2em; + } + } + } + + .column.no-border { + border-right: 0; + } +} + +.chart-box { + height: 20em; +} + +h4 .color-span { + font-size: 0.6em; + padding: 2px 4px; +} \ No newline at end of file