├── __init__.py
├── Src
├── __init__.py
├── ip2region.db
├── ip2Region.py
└── Core.py
├── webServer
├── __init__.py
├── admin
│ ├── __init__.py
│ └── home.py
├── static
│ ├── images
│ │ ├── jt.png
│ │ ├── lbx.png
│ │ ├── map.png
│ │ ├── head_bg.png
│ │ ├── line(1).png
│ │ ├── weather.png
│ │ └── webserver_monitor.png
│ ├── font
│ │ └── DS-DIGIT.TTF
│ ├── js
│ │ ├── flexible.js
│ │ ├── timer.js
│ │ ├── config.js.bak
│ │ ├── config.js
│ │ ├── myMap.js
│ │ └── index.js
│ └── css
│ │ ├── index.css
│ │ └── index.less
├── start.py
├── customer.py
├── templates
│ └── home
│ │ └── index.html
├── divers
│ ├── mongo.py
│ └── mysql.py
└── api_request.http
├── ParserAdapter
├── __init__.py
├── BaseAdapter.py
├── Apache.py
└── Nginx.py
├── QueueAdapter
├── __init__.py
├── BaseAdapter.py
├── Redis.py
└── Mongodb.py
├── StorageAdapter
├── __init__.py
├── BaseAdapter.py
├── Mongodb.py
└── Mysql.py
├── requirements.txt
├── Dockerfile
├── config.ini.example
├── main.py
├── README.MD
└── LICENSE
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Src/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webServer/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ParserAdapter/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/QueueAdapter/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/StorageAdapter/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/webServer/admin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Src/ip2region.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/Src/ip2region.db
--------------------------------------------------------------------------------
/webServer/static/images/jt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/jt.png
--------------------------------------------------------------------------------
/webServer/static/images/lbx.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/lbx.png
--------------------------------------------------------------------------------
/webServer/static/images/map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/map.png
--------------------------------------------------------------------------------
/webServer/static/font/DS-DIGIT.TTF:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/font/DS-DIGIT.TTF
--------------------------------------------------------------------------------
/webServer/static/images/head_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/head_bg.png
--------------------------------------------------------------------------------
/webServer/static/images/line(1).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/line(1).png
--------------------------------------------------------------------------------
/webServer/static/images/weather.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/weather.png
--------------------------------------------------------------------------------
/webServer/static/images/webserver_monitor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jyolo/wLogger/HEAD/webServer/static/images/webserver_monitor.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask == 1.1.1
2 | flask-pymongo == 2.3.0
3 | flask_sqlalchemy == 2.4.4
4 | SQLAlchemy == 1.3
5 | configparser == 4.0.2
6 | redis == 3.5.3
7 | mongo == 0.2.0
8 | pymysql == 0.10.1
9 | click == 7.1.2
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM centos
2 | RUN yum list | grep python \
3 | && yum -y install python38 \
4 | && yum -y install git \
5 | && cd / && git clone https://github.com/jyolo/wLogger \
6 | && cd /wLogger \
7 | && pip3 install -r requirements.txt \
8 | && echo "/usr/bin/python3 /wLogger/main.py \$@" > run.sh
9 |
10 |
11 |
12 |
13 | ENTRYPOINT ["/bin/bash","/wLogger/run.sh"]
14 |
--------------------------------------------------------------------------------
/QueueAdapter/BaseAdapter.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod,ABCMeta
2 |
3 | class Adapter():
4 | __metaclass__ = ABCMeta
5 |
6 |
7 | def __init__(self): pass
8 |
9 | @abstractmethod
10 | def initQueue(self): pass
11 |
12 | @abstractmethod
13 | def pushDataToQueue(self ): pass
14 |
15 | @abstractmethod
16 | def getDataFromQueue(self): pass
17 |
18 | @abstractmethod
19 | def rollBackToQueue(self): pass
20 |
21 | @abstractmethod
22 | def getDataCountNum(self): pass
--------------------------------------------------------------------------------
/webServer/static/js/flexible.js:
--------------------------------------------------------------------------------
1 | (function flexible(window, document) {
2 | var docEl = document.documentElement;
3 | var dpr = window.devicePixelRatio || 1;
4 |
5 | // adjust body font size
6 | function setBodyFontSize() {
7 | if (document.body) {
8 | document.body.style.fontSize = 12 * dpr + "px";
9 | } else {
10 | document.addEventListener("DOMContentLoaded", setBodyFontSize);
11 | }
12 | }
13 | setBodyFontSize();
14 |
15 | // set 1rem = viewWidth / 10
16 | function setRemUnit() {
17 | var rem = docEl.clientWidth / 24;
18 | docEl.style.fontSize = rem + "px";
19 | }
20 |
21 | setRemUnit();
22 |
23 | // reset rem unit on page resize
24 | window.addEventListener("resize", setRemUnit);
25 | window.addEventListener("pageshow", function(e) {
26 | if (e.persisted) {
27 | setRemUnit();
28 | }
29 | });
30 |
31 | // detect 0.5px supports
32 | if (dpr >= 2) {
33 | var fakeBody = document.createElement("body");
34 | var testElement = document.createElement("div");
35 | testElement.style.border = ".5px solid transparent";
36 | fakeBody.appendChild(testElement);
37 | docEl.appendChild(fakeBody);
38 | if (testElement.offsetHeight === 1) {
39 | docEl.classList.add("hairlines");
40 | }
41 | docEl.removeChild(fakeBody);
42 | }
43 | })(window, document);
44 |
--------------------------------------------------------------------------------
/webServer/static/js/timer.js:
--------------------------------------------------------------------------------
1 | // 每个loader 定时设定 ms 毫秒
2 | var each_loader_timmer = {
3 |
4 | total_ip: global_timer_secends,
5 | total_pv: global_timer_secends,
6 |
7 | map_chart: global_timer_secends,
8 | top_ip_chart : global_timer_secends ,
9 |
10 | status_code_chart : global_timer_secends,
11 |
12 | network_traffic_by_minute : global_timer_secends,
13 | request_ip_pv_by_minute : global_timer_secends,
14 |
15 | request_num_by_url : global_timer_secends,
16 | spider_by_ua : global_timer_secends,
17 |
18 | }
19 |
20 | // window.chart_load_func['total_ip']()
21 | // window.chart_load_func['total_pv']()
22 | // window.chart_load_func['map_chart']()
23 | // window.chart_load_func['top_ip_chart']()
24 | // window.chart_load_func['status_code_chart']()
25 | // window.chart_load_func['request_ip_pv_by_minute']()
26 | // window.chart_load_func['network_traffic_by_minute']()
27 | // window.chart_load_func['request_num_by_url']()
28 | // window.chart_load_func['spider_by_ua']()
29 |
30 |
31 |
32 | loader_key = Object.keys(each_loader_timmer)
33 | var timer = []
34 |
35 | for (var i=0 ;i < loader_key.length ; i++){
36 |
37 | let func = window.chart_load_func[loader_key[i]]
38 | // first run
39 | if (!func) continue;
40 | func()
41 | timer = setInterval(function(){
42 | // for timer
43 | func()
44 | },each_loader_timmer[ loader_key[i] ])
45 |
46 | }
47 |
48 |
49 |
--------------------------------------------------------------------------------
/webServer/static/js/config.js.bak:
--------------------------------------------------------------------------------
1 | window.host = 'http://local.monitor.com';
2 | window.Authorization = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJsb2NhbC5rcGkuY29tIiwiaWF0IjoxNTkxNjg1OTA1LCJzdWIiOiJ3ZWIiLCJhdWQiOiJBcGFjaGUtSHR0cENsaWVudFwvNC41LjYgKEphdmFcLzEuOC4wXzIwMi1yZWxlYXNlKSIsImV4cCI6MTYyMzIyMTkwNSwidWlkIjo4NCwibmlja25hbWUiOiJ3ZWljb25nIn0.fX1RrO4aOJ-v0QKQ4lSBcjIWyDzzl4F96yDv7_aySHnacUU-4VZbYeec4804-iJBLBmWcM3YheoO-XFqyY9ffdQTjNfobD9WiYPBNJBJAooSQJMOo2H7mwJrXgPTGlFEds1rpXfGHEH2yl7SidPwa4Hq4itR6B1aJOdEY23-8GU'
3 | window.global_timer_secends = 20000 // 全局定时器 20秒
4 | window.chart_load_func = []
5 |
6 | //total_money
7 | window.chart_load_func['get_total_money_load'] = function(){
8 | $.ajax({
9 | url: host + '/v1/screen/get_tsmoney_total',
10 | type:'GET',
11 | async:true,
12 | headers: { 'Authorization':Authorization,},
13 | success:function(msg){
14 | let money = msg.data.toString().split('.')[0]
15 | $('.total_money').html(money)
16 |
17 | }
18 |
19 | })
20 | }
21 |
22 |
23 | //total_member
24 | window.chart_load_func['get_total_member_load'] = function(){
25 | $.ajax({
26 | url: host + '/v1/screen/get_member_total',
27 | type:'GET',
28 | async:true,
29 | headers: { 'Authorization':Authorization,},
30 | success:function(msg){
31 | let num = msg.data.toString().split('.')[0]
32 | $('.total_member').html(num)
33 |
34 | }
35 | })
36 | }
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/config.ini.example:
--------------------------------------------------------------------------------
1 |
2 | [nginx]
3 | pid_path = /www/server/nginx/logs/nginx.pid
4 | server_conf = /www/server/nginx/conf/nginx.conf
5 |
6 | [apache]
7 | apachectl_bin = /www/server/apache/bin/apachectl
8 | server_conf = /www/server/apache/conf/httpd.conf
9 |
10 |
11 | [mysql]
12 | host = 127.0.0.1
13 | port = 3306
14 | db = logger
15 | username = logger
16 | password = xxxxx
17 | table = logger_watcher
18 | # 输出 日志 切割 到不同的表 或者 集合 支持 [day, week, month ,year]
19 | split_save = day
20 |
21 | [redis]
22 | host = 127.0.0.1
23 | port = 6379
24 | password = xxx
25 | db = 1
26 |
27 |
28 | [mongodb]
29 | host = 127.0.0.1
30 | port = 27017
31 | username = xxxx
32 | password = xxxx
33 | db = logger_watcher
34 | collection = logger
35 | # 输出 日志 切割 到不同的表 或者 集合 支持 [day, week, month ,year]
36 | split_save = day
37 |
38 |
39 | [inputer]
40 | log_debug = True
41 | node_id = server_80
42 | queue = redis
43 | queue_name = logger_watch:logger
44 | max_batch_push_queue_size = 5000
45 | max_retry_open_file_time = 10
46 | max_retry_reconnect_time = 20
47 |
48 |
49 | [inputer.log_file.web1]
50 | server_type = nginx
51 | file_path = /www/wwwlogs/local.test3.com.log
52 | ;log_format_name = combined
53 | log_format_name = online
54 | read_type = tail
55 | cut_file_type = filesize
56 | cut_file_point = 10
57 | cut_file_save_dir = /www/wwwlogs/cut_file/
58 |
59 |
60 | [outputer]
61 | log_debug = True
62 | ;save_engine = mongodb
63 | save_engine = mysql
64 | queue = redis
65 | ;queue = mongodb
66 | queue_name = logger_watch:logger
67 | ;server_type = apache
68 | server_type = nginx
69 | worker_process_num = 1
70 | max_batch_insert_db_size = 1
71 | max_retry_reconnect_time = 200
72 |
73 |
74 | [web]
75 | # development | production
76 | env = development
77 | debug = True
78 | secret_key = asdasdasdsadasd
79 | host = 127.0.0.1
80 | port = 5000
81 | data_engine = mysql
82 | ;data_engine = mysql
83 |
84 |
85 |
--------------------------------------------------------------------------------
/webServer/static/js/config.js:
--------------------------------------------------------------------------------
1 | window.host = '';
2 | window.Authorization = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJsb2NhbC5rcGkuY29tIiwiaWF0IjoxNTkxNjg1OTA1LCJzdWIiOiJ3ZWIiLCJhdWQiOiJBcGFjaGUtSHR0cENsaWVudFwvNC41LjYgKEphdmFcLzEuOC4wXzIwMi1yZWxlYXNlKSIsImV4cCI6MTYyMzIyMTkwNSwidWlkIjo4NCwibmlja25hbWUiOiJ3ZWljb25nIn0.fX1RrO4aOJ-v0QKQ4lSBcjIWyDzzl4F96yDv7_aySHnacUU-4VZbYeec4804-iJBLBmWcM3YheoO-XFqyY9ffdQTjNfobD9WiYPBNJBJAooSQJMOo2H7mwJrXgPTGlFEds1rpXfGHEH2yl7SidPwa4Hq4itR6B1aJOdEY23-8GU'
3 | window.global_timer_secends = 10000 // 全局定时器 20秒
4 | window.chart_load_func = []
5 |
6 |
7 | //total ip today
8 | window.chart_load_func['total_ip'] = function(){
9 | console.log(window.location.href)
10 | $.ajax({
11 | url: host + '/get_total_ip',
12 | type:'GET',
13 | async:true,
14 | headers: { 'Authorization':Authorization,},
15 | success:function(msg){
16 | $('.total_ip').html(msg.data['total_num'])
17 | }
18 |
19 | })
20 | }
21 |
22 |
23 | //total_pv
24 | window.chart_load_func['total_pv'] = function(){
25 |
26 | $.ajax({
27 | url: host + '/get_total_pv',
28 | type:'GET',
29 | async:true,
30 | headers: { 'Authorization':Authorization,},
31 | success:function(msg){
32 | $('.total_pv').html(msg.data['total_num'])
33 |
34 | }
35 | })
36 | }
37 |
38 | function timestampToTime(timestamp) {
39 | var date = new Date(timestamp * 1000) ;//时间戳为10位需*1000,时间戳为13位的话不需乘1000
40 | // var Y = date.getFullYear() + '-';
41 | // var M = (date.getMonth()+1 < 10 ? '0'+(date.getMonth()+1) : date.getMonth()+1) + '-';
42 | // var D = (date.getDate() < 10 ? '0'+date.getDate() : date.getDate()) + ' ';
43 | var h = (date.getHours() < 10 ? '0'+date.getHours() : date.getHours()) + ':';
44 | var m = (date.getMinutes() < 10 ? '0'+date.getMinutes() : date.getMinutes()) + ':';
45 | // var s = (date.getSeconds() < 10 ? '0'+date.getSeconds() : date.getSeconds());
46 |
47 | // let strDate = Y+M+D+h+m+s;
48 | m = m.replace(':','')
49 | let strDate = h+m;
50 |
51 | return strDate;
52 | }
53 |
54 |
55 |
--------------------------------------------------------------------------------
/StorageAdapter/BaseAdapter.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod,ABCMeta
2 | import datetime,time
3 |
4 |
5 | class Adapter():
6 | __metaclass__ = ABCMeta
7 |
8 | save_engine = []
9 | split_save = ['day', 'week', 'month' ,'year']
10 |
11 | def __init__(self): pass
12 |
13 |
14 | def _getTableName(self,table_key_name):
15 |
16 | table_suffix = ''
17 | save_engine_conf = self.conf[self.conf['outputer']['save_engine']]
18 | try:
19 | self.table = save_engine_conf[table_key_name]
20 | except KeyError as e:
21 | raise Exception('配置错误: %s not exists' % e.args)
22 |
23 |
24 | if 'split_save' in save_engine_conf:
25 |
26 | if save_engine_conf['split_save'] not in self.split_save:
27 |
28 | raise Exception('outputer 配置项 split_save 只支持 %s 选项' % ','.join(self.split_save))
29 |
30 | if save_engine_conf['split_save'] == 'day':
31 |
32 | table_suffix = time.strftime( '%Y_%m_%d' , time.localtime())
33 |
34 | elif save_engine_conf['split_save'] == 'week':
35 |
36 | now = datetime.datetime.now()
37 | this_week_start = now - datetime.timedelta(days=now.weekday())
38 | this_week_end = now + datetime.timedelta(days=6 - now.weekday())
39 |
40 | table_suffix = datetime.datetime.strftime(this_week_start,'%Y_%m_%d') + datetime.datetime.strftime(this_week_end,'_%d')
41 |
42 | elif save_engine_conf['split_save'] == 'month':
43 |
44 | table_suffix = time.strftime( '%Y_%m' , time.localtime())
45 | elif save_engine_conf['split_save'] == 'year':
46 |
47 | table_suffix = time.strftime( '%Y' , time.localtime())
48 |
49 | if len(table_suffix) :
50 | self.table = self.table +'_' + table_suffix
51 |
52 |
53 |
54 | @abstractmethod
55 | def initStorage(self): pass
56 |
57 | @abstractmethod
58 | def analysisTraffic(self):pass
59 |
60 | @abstractmethod
61 | def pushDataToStorage(self ): pass
62 |
63 | @abstractmethod
64 | def _handle_queue_data_before_into_storage(self):pass
65 |
66 | @abstractmethod
67 | def _handle_queue_data_after_into_storage(self):pass
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/webServer/admin/home.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | from flask.blueprints import Blueprint
3 | from flask_pymongo import PyMongo
4 | from flask import render_template,request,flash,session,current_app
5 | from webServer.customer import ApiCorsResponse
6 | import time,re
7 |
8 |
9 |
10 | home = Blueprint('home',__name__)
11 |
12 | @home.route('/',methods=['GET','POST'])
13 | def index():
14 | # flash('You were successfully logged in asdasd')
15 | if(request.method == 'GET'):
16 | return render_template('home/index.html')
17 |
18 |
19 |
20 | @home.route('/get_total_ip' , methods=['GET'])
21 | def get_total_ip():
22 | return current_app.driver.get_total_ip()
23 |
24 | @home.route('/get_total_pv' , methods=['GET'])
25 | def get_total_pv():
26 | return current_app.driver.get_total_pv()
27 |
28 | @home.route('/get_request_num_by_url' , methods=['GET'])
29 | def get_request_num_by_url():
30 | return current_app.driver.get_request_num_by_url()
31 |
32 | @home.route('/get_request_urls_by_ip' , methods=['GET'])
33 | def get_request_urls_by_ip():
34 | return current_app.driver.get_request_urls_by_ip()
35 |
36 | @home.route('/get_request_num_by_ip' , methods=['GET'])
37 | def get_request_num_by_ip():
38 | return current_app.driver.get_request_num_by_ip()
39 |
40 | @home.route('/get_request_num_by_secends' , methods=['GET'])
41 | def get_request_num_by_secends():
42 | return current_app.driver.get_request_num_by_secends()
43 |
44 | @home.route('/get_network_traffic_by_minute' , methods=['GET'])
45 | def get_network_traffic_by_minute():
46 | return current_app.driver.get_network_traffic_by_minute()
47 |
48 | @home.route('/get_ip_pv_num_by_minute' , methods=['GET'])
49 | def get_ip_pv_num_by_minute():
50 | return current_app.driver.get_ip_pv_num_by_minute()
51 |
52 | @home.route('/get_request_num_by_province' , methods=['GET'])
53 | def get_request_num_by_province():
54 | return current_app.driver.get_request_num_by_province()
55 |
56 | @home.route('/get_request_num_by_status' , methods=['GET'])
57 | def get_request_num_by_status():
58 | return current_app.driver.get_request_num_by_status()
59 |
60 |
61 | @home.route('/get_request_num_by_status_code' , methods=['GET'])
62 | def get_request_num_by_status_code():
63 | return current_app.driver.get_request_num_by_status_code()
64 |
65 | @home.route('/get_spider_by_ua' , methods=['GET'])
66 | def get_spider_by_ua():
67 | return current_app.driver.get_spider_by_ua()
68 |
69 |
70 | @home.route('/get_device_type_by_ua' , methods=['GET'])
71 | def get_device_type_by_ua():
72 | return current_app.driver.get_device_type_by_ua()
73 |
74 |
75 |
76 |
77 | if __name__ == "__main__":
78 | pass
--------------------------------------------------------------------------------
/webServer/start.py:
--------------------------------------------------------------------------------
1 | from flask import Flask
2 | import os,sys
3 |
4 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5 | from webServer.admin.home import home
6 | from webServer.divers.mysql import MysqlDb
7 | from webServer.divers.mongo import MongoDb
8 | from webServer.customer import Func
9 |
10 |
11 | app = Flask(__name__)
12 |
13 |
14 | # start this server from the main.py
15 | def start_web(conf_dict = {}):
16 | """
17 | when run in subprocess need conf_dict of the flask config
18 | :param conf_dict:
19 | :return:
20 | """
21 | if(not conf_dict ):
22 | raise ValueError('miss flask config of args conf_dict')
23 |
24 | app.env = conf_dict['env']
25 | if conf_dict['debug'] == 'True':
26 | app.debug = True
27 | elif conf_dict['debug'] == 'False':
28 | app.debug = False
29 |
30 | app.secret_key = conf_dict['secret_key']
31 | if 'host' in conf_dict:
32 | _host = conf_dict['host']
33 | else:
34 | _host = '0.0.0.0'
35 |
36 | if 'port' in conf_dict:
37 | _port = conf_dict['port']
38 | else:
39 | _port = 5000
40 |
41 | if 'server_name' in conf_dict:
42 | app.config['SERVER_NAME'] = conf_dict['server_name']
43 |
44 | app.config.from_mapping(conf_dict)
45 | app.register_blueprint(home, url_prefix='/')
46 |
47 |
48 | # init flask db engine
49 | setAppDataEngine(conf_dict)
50 |
51 | app.run(
52 | host=_host,
53 | port=_port
54 | )
55 |
56 |
57 |
58 | def setAppDataEngine(conf_dict):
59 | args = conf_dict[conf_dict['data_engine']]
60 | app.db_engine_table = Func.getTableName(args ,data_engine = conf_dict['data_engine'])
61 |
62 | if conf_dict['data_engine'] == 'mongodb':
63 | from flask_pymongo import PyMongo
64 |
65 | if args['username'] and args['password']:
66 | mongourl = 'mongodb://%s:%s@%s:%s/%s' % (
67 | args['username'], args['password'], args['host'], args['port'], args['db'])
68 | else:
69 | mongourl = 'mongodb://%s:%s/%s' % (args['host'], args['port'], args['db'])
70 |
71 | app.db = PyMongo(app,mongourl).db
72 | app.driver = MongoDb
73 |
74 |
75 | if conf_dict['data_engine'] == 'mysql':
76 | from flask_sqlalchemy import SQLAlchemy
77 | # from sqlalchemy import create_engine
78 | # from sqlalchemy.engine.result
79 | import pymysql
80 |
81 | #sqlalchemy docs https://docs.sqlalchemy.org/en/13/core/pooling.html
82 | pymysql.install_as_MySQLdb()
83 |
84 | sql_url = 'mysql+pymysql://%s:%s@%s:%s/%s?charset=utf8' % (args['username'],args['password'],args['host'],args['port'],args['db'])
85 |
86 | app.config['SQLALCHEMY_DATABASE_URI'] = sql_url
87 | app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
88 |
89 | db = SQLAlchemy(app)
90 | app.db = db.engine
91 | app.driver = MysqlDb
92 |
93 |
94 |
95 |
96 |
97 |
98 | if __name__ == "__main__":
99 |
100 | pass
101 |
102 | # app.run()
103 |
--------------------------------------------------------------------------------
/webServer/customer.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | from flask import Response,current_app,request,session
3 | from sqlalchemy import text
4 | from decimal import Decimal
5 | import json,time,datetime
6 |
7 |
8 | # 自定义跨域 接口 返回 class
9 | class ApiCorsResponse():
10 |
11 | @staticmethod
12 | def response(data ,success = True,status_code= 200):
13 | if success :
14 | re_data = {'msg':'ok','data':data}
15 | else:
16 | re_data = {'msg': 'fail', 'error_info': data}
17 |
18 |
19 | rep = Response(
20 | response=json.dumps(re_data) + "\n" ,
21 | status=status_code,
22 | mimetype= current_app.config["JSONIFY_MIMETYPE"],
23 | headers={
24 | 'Access-Control-Allow-Origin':'*',
25 | 'Access-Control-Allow-Method':'GET,POST,OPTIONS,PUT,DELETE',
26 | 'Access-Control-Allow-Headers':'*',
27 | }
28 | )
29 |
30 |
31 | return rep
32 | # 自定义函数class
33 | class Func():
34 |
35 | split_save = ['day', 'week', 'month', 'year']
36 | @classmethod
37 | def getTableName(cls,conf,data_engine):
38 | table_suffix = ''
39 |
40 | try:
41 | if data_engine == 'mysql':
42 | table = conf['table']
43 | if data_engine == 'mongodb':
44 | table = conf['collection']
45 |
46 | except KeyError as e:
47 | raise Exception('配置错误: %s not exists' % e.args)
48 |
49 |
50 |
51 | if 'split_save' in conf:
52 |
53 | if conf['split_save'] not in cls.split_save:
54 | raise Exception('webserver 配置项 split_save 只支持 %s 选项' % ','.join(cls.split_save))
55 |
56 | if conf['split_save'] == 'day':
57 |
58 | table_suffix = time.strftime('%Y_%m_%d', time.localtime())
59 |
60 | elif conf['split_save'] == 'week':
61 |
62 | now = datetime.datetime.now()
63 | this_week_start = now - datetime.timedelta(days=now.weekday())
64 | this_week_end = now + datetime.timedelta(days=6 - now.weekday())
65 |
66 | table_suffix = datetime.datetime.strftime(this_week_start, '%Y_%m_%d') + datetime.datetime.strftime(
67 | this_week_end, '_%d')
68 |
69 | elif conf['split_save'] == 'month':
70 |
71 | table_suffix = time.strftime('%Y_%m', time.localtime())
72 | elif conf['split_save'] == 'year':
73 |
74 | table_suffix = time.strftime('%Y', time.localtime())
75 |
76 | if len(table_suffix):
77 | table = table + '_' + table_suffix
78 |
79 |
80 | return table
81 | @classmethod
82 | def fetchone(cls,resultObj):
83 | return cls.fetchall(resultObj)[0]
84 |
85 | @classmethod
86 | def fetchall(cls, resultObj):
87 | _list = []
88 | for i in resultObj:
89 | _dict = {}
90 | item = i.items()
91 |
92 | for j in item:
93 | if isinstance(j[1],Decimal):
94 | vl = float(Decimal(j[1]).quantize(Decimal('.001')))
95 | _dict[j[0]] = vl
96 | else:
97 | _dict[j[0]] = j[1]
98 |
99 | _list.append(_dict)
100 |
101 | return _list
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/webServer/templates/home/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 | web流量数据可视化实时监控系统
14 | 当前时间:
15 |
16 |
17 |
18 |
19 |
20 |
21 | 请求量最高的IP TOP50
22 |
23 |
24 |
25 |
26 |
27 |
28 |
最近10分钟每分钟流量情况
29 |
30 |
31 |
32 |
33 |
热门接口URL请求TOP 10
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
48 |
49 |
50 | - 今日IP数
51 | - 今日PV数
52 |
53 |
54 |
55 |
61 |
62 |
63 |
64 |
65 |
66 |
非200状态码
67 |
68 |
69 |
70 |
71 |
最近10分钟每分钟IP/PV
72 |
73 |
74 |
75 |
76 |
搜索引擎蜘蛛占比
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | import time
3 |
4 | from Src.Core import OutputCustomer,Reader,Base
5 | from multiprocessing import Process
6 | from threading import Thread
7 | from webServer.start import start_web
8 | import multiprocessing,os,click
9 |
10 |
11 |
12 |
13 | def runReader(log_files_conf,config_name):
14 |
15 | r = Reader(log_file_conf=log_files_conf ,config_name=config_name)
16 |
17 | pushQueue = ['pushDataToQueue'] * multiprocessing.cpu_count()
18 | jobs = ['readLog','cutFile'] + pushQueue
19 |
20 | t = []
21 | for i in jobs:
22 | th = Thread(target=r.runMethod, args=(i, ))
23 | t.append(th)
24 |
25 | for i in t:
26 | i.start()
27 |
28 | for i in t:
29 | i.join()
30 |
31 | def customer( config_name ):
32 |
33 | OutputCustomer(config_name).saveToStorage()
34 |
35 | def analysis(confg_name):
36 |
37 | OutputCustomer(confg_name).watchTraffic()
38 |
39 | def getLogFilsDict(base):
40 | logFiles = []
41 |
42 | for i in list(base.conf):
43 | if 'inputer.log_file' in i:
44 | item = dict(base.conf[i])
45 | item['app_name'] = i.split('.')[-1]
46 | logFiles.append(item)
47 |
48 | return logFiles
49 |
50 |
51 | def startInputer(base , config):
52 |
53 | logFiles = getLogFilsDict(base)
54 |
55 | plist = []
56 | for i in logFiles:
57 | p = Process(target=runReader, args=(i, config,))
58 | plist.append(p)
59 |
60 | for i in plist:
61 | i.start()
62 |
63 | for i in plist:
64 | i.join()
65 |
66 | def startOutputer(base , config):
67 | p_list = []
68 | for start_webi in range(int(base.conf['outputer']['worker_process_num'])):
69 | p = Process(target=customer, args=(config,))
70 | p_list.append(p)
71 |
72 | for i in p_list:
73 | i.start()
74 |
75 | for i in p_list:
76 | i.join()
77 |
78 | @click.command()
79 | @click.option('-r', '--run', help="run type" ,type=click.Choice(['inputer', 'outputer','traffic','web']))
80 | @click.option('-s', '--stop', help="stop the proccess" ,type=click.Choice(['inputer', 'outputer']))
81 | @click.option('-c', '--config', help="config file name" )
82 | def enter(run,stop,config):
83 |
84 | if (config == None):
85 | print('please use "-c" to bind config.ini file')
86 | exit()
87 |
88 | base = Base(config_name=config)
89 |
90 | if (run == 'inputer'):
91 |
92 | pid = os.fork()
93 | if pid > 0 :
94 | exit()
95 | else:
96 | startInputer(base, config)
97 |
98 |
99 |
100 | if (run == 'outputer'):
101 | pid = os.fork()
102 | if pid > 0:
103 | exit()
104 | else:
105 | startOutputer(base, config)
106 |
107 |
108 |
109 | if (run == 'traffic'):
110 |
111 | analysis(config)
112 |
113 | if (run == 'web'):
114 | web_conf = dict(base.conf['web'])
115 | web_conf[web_conf['data_engine']] = dict(base.conf[web_conf['data_engine']])
116 | start_web(web_conf)
117 |
118 |
119 | if (stop ):
120 | if isinstance(config,str) and len(config) :
121 | cmd = 'ps -ax | grep "main.py -r %s -c %s"' % (stop, config)
122 | else:
123 | cmd = 'ps -ax | grep "main.py -r %s"' % stop
124 |
125 | res = os.popen(cmd)
126 | pids = []
127 | print('|============================================================')
128 | for i in res.readlines():
129 | if i.find('grep') != -1:
130 | continue
131 | print('| %s ' % i.strip())
132 | pids.append(i.strip().split(' ')[0])
133 |
134 |
135 | if len(pids) == 0:
136 | print('| %s is not running ' % stop)
137 | print('|============================================================')
138 | exit('nothing happened . bye bye !')
139 |
140 |
141 | print('|============================================================')
142 |
143 | confirm = input('confirm: please enter [ yes | y ] or [ no | n ] : ')
144 |
145 | if confirm in ['yes','y'] and len(pids) > 0:
146 |
147 | os.popen('kill %s' % ' '.join(pids))
148 | exit('pid: %s was killed and %s is stoped. bye bye !' % (' '.join(pids) ,stop) )
149 | else:
150 | exit('nothing happened . bye bye !')
151 |
152 |
153 |
154 |
155 | if __name__ == "__main__":
156 |
157 | enter()
158 |
--------------------------------------------------------------------------------
/ParserAdapter/BaseAdapter.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod,ABCMeta
2 | from Src.ip2Region import Ip2Region
3 | import os,time
4 |
5 | # 解析错误
6 | class ParseError(Exception):pass
7 | # 预编译错误
8 | class ReCompile(Exception):pass
9 |
10 |
11 |
12 | class Adapter():
13 | __metaclass__ = ABCMeta
14 |
15 | LOG_FORMAT_SPLIT_TAG = '<@>'
16 |
17 | ip_parser = None
18 | log_line_pattern_dict = {}
19 |
20 | def __init__(self,*args,**kwargs):
21 |
22 | if self.ip_parser == None:
23 | ip_data_path = os.path.dirname(os.path.dirname(__file__)) + '/Src/ip2region.db'
24 |
25 | if not os.path.exists(ip_data_path):
26 | raise FileNotFoundError('ip2region.db 数据库不存在')
27 |
28 | self.ip_parser = Ip2Region(ip_data_path)
29 |
30 | @abstractmethod
31 | def getLogFormat(self): pass
32 |
33 | @abstractmethod
34 | def parse(self): pass
35 |
36 | @abstractmethod
37 | def getLogFormatByConfStr(self): pass
38 |
39 | def getLoggerFormatByServerConf(self):pass
40 |
41 | @abstractmethod
42 | def rotatelog(self):pass
43 |
44 | # 解析 ip 新增地域字段 isp city city_id province country
45 | @abstractmethod
46 | def parse_ip_to_area(self,ip):
47 | data = {}
48 | try:
49 | res = self.ip_parser.memorySearch(ip)
50 |
51 | _arg = res['region'].decode('utf-8').split('|')
52 |
53 | # _城市Id|国家|区域|省份|城市|ISP_
54 | data['isp'] = _arg[-1]
55 | data['city'] = _arg[-2]
56 | data['city_id'] = int(res['city_id'])
57 | data['province'] = _arg[-3]
58 | data['country'] = _arg[0]
59 |
60 | except Exception as e:
61 | data['isp'] = 0
62 | data['city'] = 0
63 | data['city_id'] = 0
64 | data['province'] = 0
65 | data['country'] = 0
66 |
67 | return data
68 |
69 | # 解析requset字段 变成 request_method ,request_url ,args ,server_protocol
70 | @abstractmethod
71 | def parse_request_to_extend(self,request_data):
72 | data = {}
73 |
74 | try:
75 |
76 |
77 | if len(request_data.strip()) == 0:
78 | data['request_method'] = ''
79 | data['request_url'] = ''
80 | data['args'] = ''
81 | data['server_protocol'] = ''
82 | return data
83 |
84 | _strarr = request_data.split(' ')
85 |
86 | if(len(_strarr) == 1) :
87 | data['request_method'] = ''
88 | data['request_url'] = _strarr[0]
89 | data['args'] = ''
90 | data['server_protocol'] = ''
91 | return data
92 |
93 |
94 |
95 | data['request_method'] = _strarr[0]
96 | _url = _strarr[1].split('?')
97 |
98 | if len(_url) > 1:
99 | data['request_url'] = _url[0]
100 | data['args'] = _url[1]
101 | else:
102 | data['request_url'] = _url[0]
103 | data['args'] = ''
104 |
105 |
106 | data['server_protocol'] = _strarr[2]
107 |
108 |
109 | if 'request_uri' in data:
110 | _strarr = data['request_uri'].split('?')
111 |
112 | data['request_url'] = _url[0]
113 | data['args'] = _url[1]
114 |
115 |
116 | return data
117 | except IndexError as e:
118 | # 异常攻击流量数据 解析错误 不在抛出exception 直接原样返回数据 以供分析
119 | data['request_method'] = ''
120 | data['request_url'] = request_data
121 | data['args'] = ''
122 | data['server_protocol'] = ''
123 |
124 | return data
125 |
126 |
127 | # 解析 time_iso8601 time_local 变成 time_str timestamp
128 | @abstractmethod
129 | def parse_time_to_str(self,time_type , time_data):
130 |
131 | data = {}
132 | time_data = time_data.replace('[', '').replace(']', '')
133 |
134 | if 'time_iso8601' == time_type:
135 | _strarr = time_data.split('+')
136 | ts = time.strptime(_strarr[0], '%Y-%m-%dT%H:%M:%S')
137 |
138 |
139 |
140 | if 'time_local' == time_type:
141 | _strarr = time_data.split('+')
142 | ts = time.strptime(_strarr[0].strip(), '%d/%b/%Y:%H:%M:%S')
143 |
144 |
145 | data['time_str'] = time.strftime('%Y-%m-%d %H:%M:%S', ts)
146 | data['timestamp'] = int(time.mktime(ts))
147 |
148 |
149 |
150 |
151 | return data
--------------------------------------------------------------------------------
/webServer/static/js/myMap.js:
--------------------------------------------------------------------------------
1 | //固定的深圳市
2 | var toCityName = GEO[440300]
3 | // 1. 实例化对象
4 | var map_chart = echarts.init(document.querySelector(".map .chart"));
5 | window.addEventListener("resize", function() {
6 | map_chart.resize();
7 | });
8 | window.chart_load_func['map_chart'] = function() {
9 | $.ajax({
10 | url:host + '/get_request_num_by_province',
11 | type:'GET',
12 | headers: { 'Authorization':Authorization,},
13 | success:function(msg){
14 | var api_data = msg.data
15 | var province_geo = GEO[100000]
16 | var map_lines_data = []
17 | var map_effectScatter_data = []
18 | for (var i= 0 ; i < api_data.length ;i++){
19 | let province_name = api_data[i]['province'].slice(0,2)
20 | for (var j = 0 ; j < province_geo.childrenNum ; j++){
21 | if ( province_geo.children[j]['name'].slice(0,2) == province_name){
22 | let _lines_data = {
23 | fromName: province_geo.children[j]['name'],
24 | toName: toCityName.name,
25 | coords: [province_geo.children[j].center, toCityName.center],
26 | value: api_data[i]['value']
27 | }
28 |
29 | let _effectScatter_data = {
30 | name :province_geo.children[j]['name'],
31 | value: province_geo.children[j].center.concat(api_data[i]['value'])
32 | }
33 | map_lines_data.push(_lines_data)
34 | map_effectScatter_data.push(_effectScatter_data)
35 | break
36 | }
37 | }
38 | }
39 |
40 | var color = ["#fff"]; //航线的颜色
41 | var series = [];
42 | series.push(
43 | {
44 | name: " 运动方向",
45 | type: "lines",
46 | zlevel: 1,
47 | effect: {
48 | show: true,
49 | period: 6,
50 | trailLength: 0.7,
51 | color: "white", //arrow箭头的颜色
52 | symbolSize: 3
53 | },
54 | lineStyle: {
55 | normal: {
56 | color: color[i],
57 | width: 0,
58 | curveness: 0.2
59 | }
60 | },
61 | data: map_lines_data
62 | },
63 | // {
64 | // name: "运动方向 实心线",
65 | // type: "lines",
66 | // zlevel: 2,
67 | // symbol: ["none", "arrow"],
68 | // symbolSize: 10,
69 | // effect: {
70 | // show: true,
71 | // period: 6,
72 | // trailLength: 0,
73 | // symbol: planePath,
74 | // symbolSize: 15
75 | // },
76 | // lineStyle: {
77 | // normal: {
78 | // color: "#fff",
79 | // width: 1,
80 | // opacity: 0.6,
81 | // curveness: 0.2
82 | // }
83 | // },
84 | // data: map_lines_data
85 | // },
86 | {
87 | name: "effectScatter Top3",
88 | type: "effectScatter",
89 | coordinateSystem: "geo",
90 | zlevel: 2,
91 | rippleEffect: {
92 | brushType: "stroke"
93 | },
94 | label: {
95 | color:'#fff',
96 | normal: {
97 | show: true,
98 | position: "right",
99 | formatter: function(params){
100 |
101 | return params.data.name +' :' + params.data.value[2]
102 | }
103 | }
104 | },
105 | symbolSize: function(val) {
106 | return val[2] / 10000;
107 | // return 10;
108 | },
109 | itemStyle: {
110 | normal: {
111 | color: '#fff'
112 | },
113 | emphasis: {
114 | areaColor: "#fff"
115 | }
116 | },
117 | data: map_effectScatter_data
118 | }
119 | );
120 |
121 | var option = {
122 |
123 | geo: {
124 | map: "china",
125 | label: {
126 | show:false,
127 | color:"#fff",
128 | emphasis:{
129 | itemStyle: {
130 | areaColor: null,
131 | shadowOffsetX: 0,
132 | shadowOffsetY: 0,
133 | shadowBlur: 20,
134 | borderWidth: 0,
135 | shadowColor: 'rgba(0, 0, 0, 0.5)'
136 | }
137 | }
138 | },
139 | roam: false,
140 | // 放大我们的地图
141 | zoom: 1.2,
142 | itemStyle: {
143 | normal: {
144 | areaColor: "rgba(43, 196, 243, 0.42)",
145 | borderColor: "rgba(43, 196, 243, 1)",
146 | borderWidth: 1
147 | },
148 | emphasis: {
149 | areaColor: "#2f89cf"
150 | }
151 | }
152 | },
153 | series: series
154 | };
155 | map_chart.setOption(option);
156 |
157 | }
158 |
159 | })
160 |
161 | }
162 |
163 |
--------------------------------------------------------------------------------
/QueueAdapter/Redis.py:
--------------------------------------------------------------------------------
1 | from QueueAdapter.BaseAdapter import Adapter
2 | from redis import Redis,exceptions as redis_exceptions
3 | import time,threading,os,json
4 |
5 |
6 | class QueueAp(Adapter):
7 |
8 | db = None
9 | runner = None
10 |
11 |
12 | @classmethod
13 | def initQueue(cls,runnerObject):
14 | self = cls()
15 | self.runner = runnerObject
16 | self.conf = self.runner.conf
17 | self.logging = self.runner.logging
18 |
19 | try:
20 |
21 | self.db = Redis(
22 | host=self.conf['redis']['host'],
23 | port=int(self.conf['redis']['port']),
24 | password=str(self.conf['redis']['password']),
25 | db=self.conf['redis']['db'],
26 | )
27 |
28 |
29 | except redis_exceptions.RedisError as e:
30 | self.runner.event['stop'] = e.args[0]
31 |
32 |
33 | return self
34 |
35 |
36 | def pushDataToQueue(self ):
37 |
38 | pipe = self.db.pipeline()
39 |
40 | retry_reconnect_time = 0
41 |
42 | while True:
43 | time.sleep(1)
44 | if self.runner.event['stop']:
45 | self.logging.error( '%s ; pushQueue threading stop pid: %s ---- tid: %s ' % (self.runner.event['stop'] ,os.getpid() ,threading.get_ident() ))
46 | return
47 |
48 | try:
49 |
50 | # 重试连接queue的时候; 不再从 dqueue 中拿数据
51 | if retry_reconnect_time == 0:
52 |
53 | start_time = time.perf_counter()
54 | # self.logging.debug("\n pushQueue -------pid: %s -tid: %s- started \n" % ( os.getpid(), threading.get_ident()))
55 |
56 | for i in range(self.runner.max_batch_push_queue_size):
57 | try:
58 | line = self.runner.dqueue.pop()
59 | except IndexError as e:
60 | # self.logging.info("\n pushQueue -------pid: %s -tid: %s- wait for data ;queue len: %s---- start \n" % ( os.getpid(), threading.get_ident(), len(list(self.dqueue))))
61 | break
62 |
63 | data = {}
64 | data['node_id'] = self.runner.node_id
65 | data['app_name'] = self.runner.app_name
66 | data['log_format_name'] = self.runner.log_format_name
67 |
68 | data['line'] = line.strip()
69 |
70 | try:
71 | # 日志的原始字符串
72 | data['log_format_str'] = self.runner.server_conf[self.runner.log_format_name]['log_format_str'].strip()
73 | # 日志中提取出的日志变量
74 | data['log_format_vars'] = self.runner.server_conf[self.runner.log_format_name]['log_format_vars'].strip()
75 |
76 | except KeyError as e:
77 | self.runner.event['stop'] = self.runner.log_format_name + '日志格式不存在'
78 | break
79 |
80 |
81 | data = json.dumps(data)
82 | pipe.lpush(self.runner.queue_key, data)
83 |
84 |
85 | res = pipe.execute()
86 | if len(res):
87 | retry_reconnect_time = 0
88 | end_time = time.perf_counter()
89 | self.logging.debug("\n pushQueue -------pid: %s -tid: %s- push data to queue :%s ; queue_len : %s----耗时:%s \n"
90 | % (os.getpid(), threading.get_ident(), len(res),self.db.llen(self.runner.queue_key), round(end_time - start_time, 2)))
91 |
92 |
93 | except redis_exceptions.RedisError as e:
94 |
95 |
96 | retry_reconnect_time = retry_reconnect_time + 1
97 |
98 | if retry_reconnect_time >= self.runner.max_retry_reconnect_time :
99 | self.runner.event['stop'] = 'pushQueue 重试连接 queue 超出最大次数'
100 | else:
101 | time.sleep(2)
102 | self.logging.debug('pushQueue -------pid: %s -tid: %s- push data fail; reconnect Queue %s times' % (os.getpid() , threading.get_ident() , retry_reconnect_time))
103 |
104 | continue
105 | pass
106 |
107 | def getDataFromQueue(self):
108 | start_time = time.perf_counter()
109 |
110 | pipe = self.db.pipeline()
111 |
112 | db_queue_len = self.db.llen(self.runner.queue_key)
113 |
114 | if db_queue_len >= self.runner.max_batch_insert_db_size:
115 | num = self.runner.max_batch_insert_db_size
116 | else:
117 | num = db_queue_len
118 |
119 | for i in range(num):
120 | # 后进先出
121 | pipe.rpop(self.runner.queue_key)
122 |
123 | queue_list = pipe.execute()
124 |
125 |
126 | # 过滤掉None
127 | if queue_list.count(None):
128 | queue_list = list(filter(None, queue_list))
129 |
130 | end_time = time.perf_counter()
131 | if len(queue_list):
132 | self.logging.debug("\n pid: %s ;tid : %s-- take len: %s ; queue db len : %s ; ----end 耗时: %s \n" %
133 | (os.getpid(), threading.get_ident(), len(queue_list), self.db.llen(self.runner.queue_key),
134 | round(end_time - start_time, 2)))
135 |
136 | return queue_list
137 |
138 |
139 | # 退回队列
140 | def rollBackToQueue(self,data):
141 | pipe = self.db.pipeline()
142 | for i in data:
143 | i = bytes(i,encoding='utf-8')
144 | pipe.rpush(self.runner.queue_key,i)
145 |
146 | pipe.execute()
147 |
148 |
149 | def getDataCountNum(self):
150 | return self.db.llen(self.runner.queue_key)
151 |
152 |
--------------------------------------------------------------------------------
/webServer/static/css/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 | li {
7 | list-style: none;
8 | }
9 | @font-face {
10 | font-family: electronicFont;
11 | src: url(../font/DS-DIGIT.TTF);
12 | }
13 | body {
14 | font-family: Arial, Helvetica, sans-serif;
15 | margin: 0;
16 | padding: 0;
17 | background-repeat: no-repeat;
18 | background-color: #06164A;
19 | background-size: cover;
20 | /* 行高是字体1.15倍 */
21 | line-height: 1.15;
22 | }
23 | header {
24 | position: relative;
25 | height: 1.25rem;
26 | background: url(../images/head_bg.png) no-repeat top center;
27 | background-size: 100% 100%;
28 | }
29 | header h1 {
30 | font-size: 0.475rem;
31 | color: #fff;
32 | text-align: center;
33 | line-height: 1rem;
34 | }
35 | header .showTime {
36 | position: absolute;
37 | top: 0;
38 | right: 0.375rem;
39 | line-height: 0.9375rem;
40 | font-size: 0.25rem;
41 | color: rgba(255, 255, 255, 0.7);
42 | }
43 | .mainbox {
44 | min-width: 1024px;
45 | /*max-width: 1920px;*/
46 | padding: 0.125rem 0.125rem 0;
47 | display: flex;
48 | }
49 | .mainbox .column {
50 | flex: 3;
51 | }
52 | .mainbox .column:nth-child(2) {
53 | flex: 5;
54 | margin: 0 0.125rem 0.1875rem;
55 | overflow: hidden;
56 | }
57 | .panel {
58 | position: relative;
59 | height: 3.875rem;
60 | border: 1px solid rgba(25, 186, 139, 0.17);
61 | background: rgba(255, 255, 255, 0.04) url(../images/line\(1\).png);
62 | padding: 0 0.1875rem 0.5rem;
63 | margin-bottom: 0.1875rem;
64 | }
65 | .panel::before {
66 | position: absolute;
67 | top: 0;
68 | left: 0;
69 | content: "";
70 | width: 10px;
71 | height: 10px;
72 | border-top: 2px solid #02a6b5;
73 | border-left: 2px solid #02a6b5;
74 | border-radius: 20%;
75 | }
76 | .panel::after {
77 | position: absolute;
78 | top: 0;
79 | right: 0;
80 | content: "";
81 | width: 10px;
82 | height: 10px;
83 | border-top: 2px solid #02a6b5;
84 | border-right: 2px solid #02a6b5;
85 | border-radius: 20%;
86 | }
87 | .panel .panel-footer {
88 | position: absolute;
89 | left: 0;
90 | bottom: 0;
91 | width: 100%;
92 | }
93 | .panel .panel-footer::before {
94 | position: absolute;
95 | bottom: 0;
96 | left: 0;
97 | content: "";
98 | width: 10px;
99 | height: 10px;
100 | border-bottom: 2px solid #02a6b5;
101 | border-left: 2px solid #02a6b5;
102 | border-radius: 20%;
103 | }
104 | .panel .panel-footer::after {
105 | position: absolute;
106 | bottom: 0;
107 | right: 0;
108 | content: "";
109 | width: 10px;
110 | height: 10px;
111 | border-bottom: 2px solid #02a6b5;
112 | border-right: 2px solid #02a6b5;
113 | border-radius: 20%;
114 | }
115 | .panel h2 {
116 | height: 0.6rem;
117 | line-height: 0.6rem;
118 | text-align: center;
119 | color: #fff;
120 | font-size: 0.25rem;
121 | font-weight: 400;
122 | }
123 | .panel h2 a {
124 | margin: 0 0.1875rem;
125 | color: #fff;
126 | text-decoration: underline;
127 | }
128 | .panel .chart {
129 | height: 3rem;
130 | }
131 | .no {
132 | background: rgba(101, 132, 226, 0.1);
133 | padding: 0.1875rem;
134 | }
135 | .no .no-hd {
136 | position: relative;
137 | border: 1px solid rgba(25, 186, 139, 0.17);
138 | }
139 | .no .no-hd::before {
140 | content: "";
141 | position: absolute;
142 | width: 30px;
143 | height: 10px;
144 | border-top: 2px solid #02a6b5;
145 | border-left: 2px solid #02a6b5;
146 | top: 0;
147 | left: 0;
148 | }
149 | .no .no-hd::after {
150 | content: "";
151 | position: absolute;
152 | width: 30px;
153 | height: 10px;
154 | border-bottom: 2px solid #02a6b5;
155 | border-right: 2px solid #02a6b5;
156 | right: 0;
157 | bottom: 0;
158 | }
159 | .no .no-hd ul {
160 | display: flex;
161 | }
162 | .no .no-hd ul li {
163 | position: relative;
164 | flex: 1;
165 | text-align: center;
166 | height: 1rem;
167 | line-height: 1rem;
168 | font-size: 0.875rem;
169 | color: #ffeb7b;
170 | padding: 0.05rem 0;
171 | font-family: electronicFont;
172 | font-weight: bold;
173 | }
174 | .no .no-hd ul li:first-child::after {
175 | content: "";
176 | position: absolute;
177 | height: 50%;
178 | width: 1px;
179 | background: rgba(255, 255, 255, 0.2);
180 | right: 0;
181 | top: 25%;
182 | }
183 | .no .no-bd ul {
184 | display: flex;
185 | }
186 | .no .no-bd ul li {
187 | flex: 1;
188 | height: 0.5rem;
189 | line-height: 0.5rem;
190 | text-align: center;
191 | font-size: 0.225rem;
192 | color: rgba(255, 255, 255, 0.7);
193 | padding-top: 0.125rem;
194 | }
195 | .map {
196 | position: relative;
197 | height: 10.125rem;
198 | }
199 | .map .chart {
200 | position: absolute;
201 | top: 0;
202 | left: 0;
203 | z-index: 5;
204 | height: 10.125rem;
205 | width: 100%;
206 | }
207 | .map .map1,
208 | .map .map2,
209 | .map .map3 {
210 | position: absolute;
211 | top: 50%;
212 | left: 50%;
213 | transform: translate(-50%, -50%);
214 | width: 6.475rem;
215 | height: 6.475rem;
216 | background: url(../images/map.png) no-repeat;
217 | background-size: 100% 100%;
218 | opacity: 0.3;
219 | }
220 | .map .map2 {
221 | width: 8.0375rem;
222 | height: 8.0375rem;
223 | background-image: url(../images/lbx.png);
224 | opacity: 0.6;
225 | animation: rotate 15s linear infinite;
226 | z-index: 2;
227 | }
228 | .map .map3 {
229 | width: 7.075rem;
230 | height: 7.075rem;
231 | background-image: url(../images/jt.png);
232 | animation: rotate1 10s linear infinite;
233 | }
234 | @keyframes rotate {
235 | from {
236 | transform: translate(-50%, -50%) rotate(0deg);
237 | }
238 | to {
239 | transform: translate(-50%, -50%) rotate(360deg);
240 | }
241 | }
242 | @keyframes rotate1 {
243 | from {
244 | transform: translate(-50%, -50%) rotate(0deg);
245 | }
246 | to {
247 | transform: translate(-50%, -50%) rotate(-360deg);
248 | }
249 | }
250 | @media screen and (max-width: 1024px) {
251 | html {
252 | font-size: 42px !important;
253 | }
254 | }
255 | @media screen and (min-width: 1920) {
256 | html {
257 | font-size: 80px !important;
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/QueueAdapter/Mongodb.py:
--------------------------------------------------------------------------------
1 | from QueueAdapter.BaseAdapter import Adapter
2 | from pymongo import MongoClient,errors as pyerrors
3 | import time,threading,os,json
4 |
5 | try:
6 | # Python 3.x
7 | from urllib.parse import quote_plus
8 | except ImportError:
9 | # Python 2.x
10 | from urllib import quote_plus
11 |
12 | class QueueAp(Adapter):
13 |
14 | db = None
15 | runner = None
16 |
17 | @classmethod
18 | def initQueue(cls,runnerObject):
19 | self = cls()
20 | self.runner = runnerObject
21 | self.conf = self.runner.conf
22 | self.logging = self.runner.logging
23 |
24 | if self.conf['mongodb']['username'] and self.conf['mongodb']['password']:
25 | mongo_url = 'mongodb://%s:%s@%s:%s/?authSource=%s' % \
26 | (
27 | quote_plus(self.conf['mongodb']['username']),
28 | quote_plus(self.conf['mongodb']['password']),
29 | self.conf['mongodb']['host'],
30 | int(self.conf['mongodb']['port']),
31 | self.conf['mongodb']['db']
32 | )
33 |
34 | else:
35 | mongo_url = 'mongodb://%s:%s/?authSource=%s' % \
36 | (
37 | self.conf['mongodb']['host'],
38 | int(self.conf['mongodb']['port']),
39 | self.conf['mongodb']['db']
40 | )
41 |
42 | mongodb = MongoClient(mongo_url)
43 | self.db = mongodb[self.conf['mongodb']['db']]
44 |
45 | # 创建一个过期索引 过时间
46 | ttl_seconds = 120
47 | self.db[self.runner.queue_key].create_index([("ttl", 1)], expireAfterSeconds=ttl_seconds)
48 | self.db[self.runner.queue_key].create_index([("out_queue", 1)], background=True)
49 | self.db[self.runner.queue_key].create_index([("add_time", -1)], background=True)
50 |
51 | # self.db['asdasd']
52 |
53 | return self
54 |
55 |
56 | def pushDataToQueue(self ):
57 |
58 | retry_reconnect_time = 0
59 |
60 | while True:
61 | time.sleep(1)
62 | if self.runner.event['stop']:
63 | self.logging.error('%s ; pushQueue threading stop pid: %s ---- tid: %s ' % ( self.runner.event['stop'], os.getpid(), threading.get_ident()))
64 | return
65 |
66 | try:
67 |
68 | # 重试连接queue的时候; 不再从 dqueue 中拿数据
69 | if retry_reconnect_time == 0:
70 | _queuedata = []
71 |
72 | start_time = time.perf_counter()
73 |
74 | for i in range(self.runner.max_batch_push_queue_size):
75 | try:
76 | line = self.runner.dqueue.pop()
77 | except IndexError as e:
78 | break
79 |
80 | q_data = {}
81 | data = {}
82 | data['node_id'] = self.runner.node_id
83 | data['app_name'] = self.runner.app_name
84 | data['log_format_name'] = self.runner.log_format_name
85 |
86 | data['line'] = line.strip()
87 |
88 | try:
89 | data['log_format_str'] = self.runner.server_conf[self.runner.log_format_name].strip()
90 | except KeyError as e:
91 | self.runner.event['stop'] = self.runner.log_format_name + '日志格式不存在'
92 | break
93 |
94 | data = json.dumps(data)
95 |
96 | q_data['out_queue'] = 0
97 | q_data['add_time'] = time.time()
98 | q_data['data'] = data
99 |
100 | _queuedata.append(q_data)
101 |
102 |
103 | if len(_queuedata):
104 | res = self.db[self.runner.queue_key].insert_many(_queuedata, ordered=False)
105 |
106 | end_time = time.perf_counter()
107 | self.logging.debug(
108 | "\n pushQueue -------pid: %s -tid: %s- push data to queue :%s ; queue_len : %s----耗时:%s \n"
109 | % (os.getpid(), threading.get_ident(), len(res.inserted_ids), 0,
110 | round(end_time - start_time, 2)))
111 |
112 |
113 | except pyerrors.PyMongoError as e:
114 |
115 | retry_reconnect_time = retry_reconnect_time + 1
116 |
117 | if retry_reconnect_time >= self.runner.max_retry_reconnect_time:
118 | self.logging.error('pushQueue 重试连接 queue 超出最大次数')
119 | raise Exception('pushQueue 重试连接 queue 超出最大次数')
120 | else:
121 | time.sleep(1)
122 | self.logging.debug('pushQueue -------pid: %s -tid: %s- push data fail: %s ; reconnect Queue %s times' % (
123 | os.getpid(), e.args, threading.get_ident(), retry_reconnect_time))
124 |
125 | continue
126 |
127 |
128 | def getDataFromQueue(self):
129 | db_queue_len = self.getDataCountNum()
130 |
131 | if db_queue_len == 0:
132 | return []
133 |
134 | if db_queue_len >= self.runner.max_batch_insert_db_size:
135 | takenum = self.runner.max_batch_insert_db_size
136 | else:
137 | takenum = db_queue_len
138 |
139 |
140 | start_time = time.perf_counter()
141 |
142 | _data = []
143 | for i in range(takenum):
144 | res = self.db[self.runner.queue_key].find_and_modify(
145 | query={'out_queue': 0},
146 | update={'$set': {'out_queue': 1}, '$currentDate': {'ttl': True}},
147 | sort=[('add_time', -1)]
148 | )
149 |
150 | if res:
151 | _data.append(res['data'])
152 |
153 | end_time = time.perf_counter()
154 | self.logging.debug('\n pid: %s take data from queue %s ,queue db len : %s ;.耗时: %s \n' % (
155 | os.getpid(), len(_data),self.getDataCountNum(), round(end_time - start_time ,3)))
156 |
157 | return _data
158 |
159 |
160 |
161 |
162 | def getDataCountNum(self):
163 | return self.db[self.runner.queue_key].count_documents({'out_queue':0})
164 |
--------------------------------------------------------------------------------
/webServer/static/css/index.less:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | }
6 | li {
7 | list-style: none;
8 | }
9 | @font-face {
10 | font-family: electronicFont;
11 | src: url(../font/DS-DIGIT.TTF);
12 | }
13 | body {
14 | font-family: Arial, Helvetica, sans-serif;
15 | margin: 0;
16 | padding: 0;
17 | /* 背景图定位 / 背景图尺寸 cover 完全铺满容器 contain 完整显示在容器内 */
18 | background: url(../images/bg.jpg) no-repeat #000;
19 | background-size: cover;
20 | /* 行高是字体1.15倍 */
21 | line-height: 1.15;
22 | }
23 | header {
24 | position: relative;
25 | height: 1.25rem;
26 | background: url(../images/head_bg.png) no-repeat top center;
27 | background-size: 100% 100%;
28 | h1 {
29 | font-size: 0.475rem;
30 | color: #fff;
31 | text-align: center;
32 | line-height: 1rem;
33 | }
34 | .showTime {
35 | position: absolute;
36 | top: 0;
37 | right: 0.375rem;
38 | line-height: 0.9375rem;
39 | font-size: 0.25rem;
40 | color: rgba(255, 255, 255, 0.7);
41 | }
42 | }
43 | .mainbox {
44 | min-width: 1024px;
45 | max-width: 1920px;
46 | padding: 0.125rem 0.125rem 0;
47 | display: flex;
48 | .column {
49 | flex: 3;
50 | &:nth-child(2) {
51 | flex: 5;
52 | margin: 0 0.125rem 0.1875rem;
53 | overflow: hidden;
54 | }
55 | }
56 | }
57 | .panel {
58 | position: relative;
59 | height: 3.875rem;
60 | border: 1px solid rgba(25, 186, 139, 0.17);
61 | background: rgba(255, 255, 255, 0.04) url(../images/line\(1\).png);
62 | padding: 0 0.1875rem 0.5rem;
63 | margin-bottom: 0.1875rem;
64 | &::before {
65 | position: absolute;
66 | top: 0;
67 | left: 0;
68 | content: "";
69 | width: 10px;
70 | height: 10px;
71 | border-top: 2px solid #02a6b5;
72 | border-left: 2px solid #02a6b5;
73 | border-radius: 20%;
74 | }
75 | &::after {
76 | position: absolute;
77 | top: 0;
78 | right: 0;
79 | content: "";
80 | width: 10px;
81 | height: 10px;
82 | border-top: 2px solid #02a6b5;
83 | border-right: 2px solid #02a6b5;
84 | border-radius: 20%;
85 | }
86 | .panel-footer {
87 | position: absolute;
88 | left: 0;
89 | bottom: 0;
90 | width: 100%;
91 | &::before {
92 | position: absolute;
93 | bottom: 0;
94 | left: 0;
95 | content: "";
96 | width: 10px;
97 | height: 10px;
98 | border-bottom: 2px solid #02a6b5;
99 | border-left: 2px solid #02a6b5;
100 | border-radius: 20%;
101 | }
102 | &::after {
103 | position: absolute;
104 | bottom: 0;
105 | right: 0;
106 | content: "";
107 | width: 10px;
108 | height: 10px;
109 | border-bottom: 2px solid #02a6b5;
110 | border-right: 2px solid #02a6b5;
111 | border-radius: 20%;
112 | }
113 | }
114 |
115 | h2 {
116 | height: 0.6rem;
117 | line-height: 0.6rem;
118 | text-align: center;
119 | color: #fff;
120 | font-size: 0.25rem;
121 | font-weight: 400;
122 | a {
123 | margin: 0 0.1875rem;
124 | color: #fff;
125 | text-decoration: underline;
126 | }
127 | }
128 | .chart {
129 | height: 3rem;
130 | }
131 | }
132 | .no {
133 | background: rgba(101, 132, 226, 0.1);
134 | padding: 0.1875rem;
135 | .no-hd {
136 | position: relative;
137 | border: 1px solid rgba(25, 186, 139, 0.17);
138 | &::before {
139 | content: "";
140 | position: absolute;
141 | width: 30px;
142 | height: 10px;
143 | border-top: 2px solid #02a6b5;
144 | border-left: 2px solid #02a6b5;
145 | top: 0;
146 | left: 0;
147 | }
148 | &::after {
149 | content: "";
150 | position: absolute;
151 | width: 30px;
152 | height: 10px;
153 | border-bottom: 2px solid #02a6b5;
154 | border-right: 2px solid #02a6b5;
155 | right: 0;
156 | bottom: 0;
157 | }
158 | ul {
159 | display: flex;
160 | li {
161 | position: relative;
162 | flex: 1;
163 | text-align: center;
164 | height: 1rem;
165 | line-height: 1rem;
166 | font-size: 0.875rem;
167 | color: #ffeb7b;
168 | padding: 0.05rem 0;
169 | font-family: electronicFont;
170 | font-weight: bold;
171 | &:first-child::after {
172 | content: "";
173 | position: absolute;
174 | height: 50%;
175 | width: 1px;
176 | background: rgba(255, 255, 255, 0.2);
177 | right: 0;
178 | top: 25%;
179 | }
180 | }
181 | }
182 | }
183 | .no-bd ul {
184 | display: flex;
185 | li {
186 | flex: 1;
187 | height: 0.5rem;
188 | line-height: 0.5rem;
189 | text-align: center;
190 | font-size: 0.225rem;
191 | color: rgba(255, 255, 255, 0.7);
192 | padding-top: 0.125rem;
193 | }
194 | }
195 | }
196 | .map {
197 | position: relative;
198 | height: 10.125rem;
199 | .chart {
200 | position: absolute;
201 | top: 0;
202 | left: 0;
203 | z-index: 5;
204 | height: 10.125rem;
205 | width: 100%;
206 | }
207 | .map1,
208 | .map2,
209 | .map3 {
210 | position: absolute;
211 | top: 50%;
212 | left: 50%;
213 | transform: translate(-50%, -50%);
214 | width: 6.475rem;
215 | height: 6.475rem;
216 | background: url(../images/map.png) no-repeat;
217 | background-size: 100% 100%;
218 | opacity: 0.3;
219 | }
220 | .map2 {
221 | width: 8.0375rem;
222 | height: 8.0375rem;
223 | background-image: url(../images/lbx.png);
224 | opacity: 0.6;
225 | animation: rotate 15s linear infinite;
226 | z-index: 2;
227 | }
228 | .map3 {
229 | width: 7.075rem;
230 | height: 7.075rem;
231 | background-image: url(../images/jt.png);
232 | animation: rotate1 10s linear infinite;
233 | }
234 |
235 | @keyframes rotate {
236 | from {
237 | transform: translate(-50%, -50%) rotate(0deg);
238 | }
239 | to {
240 | transform: translate(-50%, -50%) rotate(360deg);
241 | }
242 | }
243 | @keyframes rotate1 {
244 | from {
245 | transform: translate(-50%, -50%) rotate(0deg);
246 | }
247 | to {
248 | transform: translate(-50%, -50%) rotate(-360deg);
249 | }
250 | }
251 | }
252 |
253 | @media screen and (max-width: 1024px) {
254 | html {
255 | font-size: 42px !important;
256 | }
257 | }
258 | @media screen and (min-width: 1920) {
259 | html {
260 | font-size: 80px !important;
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # 介绍
2 |
3 | > wLogger 介绍
4 |
5 | * 介绍
6 |
7 | wLogger 是一款集合 日志采集,日志解析持久化存储,web流量实时监控 。三位一体的web服务流量监控应用。
8 | 三大功能模块均可独立部署启用互不干扰。目前已内置 nginx 和 apache 的日志解析存储器,简单配置一下,开箱即用。
9 | 虽然市面上已经很多类似的开源日志采集监控服务比如goaccess,用了一圈之后始终没有一款用的特别舒心。
10 |
11 | * 它可以在日志采集的时候可以按照日志文件的大小,或者在指定时间内自动对日志进行切割日志,存储到指定的目录 (已测2W并发切割日志不丢数据)
12 | * 它可以不用像goaccess那样必须配置指定格式才能解析到数据,只用指定当前使用的 nginx/apache 日志格式名称 即可解析数据
13 | * 它可以指定不同的项目走不同的队列服务,分别解析存储到不同的数据库,完全可以自己按需灵活配置
14 | * 它天然支持分布式,日志采集服务队列已内置redis LIST结构,可自行拓展kafka ,mq等其它队列服务
15 | * 它支持自定义持久化存储引擎,日志解析持久化存储服务已内置 mongodb 和 mysql ,可自行拓展其它数据库
16 | * 简单配置,开箱即用,无入侵,高拓展,灵活配置,按需应用
17 | * 运行环境:python3+ linux平台
18 |
19 | 如果该项目有帮助到您,请不要吝啬随手给个star
20 |
21 | 您也可以从数据库中取数据自己定义流量监控的UI界面和数据展现方式;
22 |
23 | 大屏实时监控效果图 本人显示器太小,截图略显拥挤;
24 |
25 | QQ交流群 : 862251895
26 |
27 | 
28 |
29 | > 功能说明
30 |
31 | 采集器 inputer
32 |
33 | * 实时日志采集,同时支持多个web日志同时采集
34 | * 可指定按照日志文件大小或指定时间,自动切割文件到指定目录, (日志切割不丢数据.)
35 | * 可自定义队列服务软件,接受采集的日志信息. 已内置redis 如需kafka 等其它mq队列可自行拓展
36 | * 极低的cpu内存占用 ,低配小主机也能愉快的玩耍
37 |
38 | 解析存储器 outputer
39 |
40 | * 实时解析日志并存储到指定的数据库, 已内置 mysql 和 mongodb 如需使用elastic全家桶或其它存储引擎 可自行拓展
41 | * 采集器,解析器,web服务均可 独立分布到不同的服务器节点上运行
42 | * 目前已内置 nginx,apache 解析器, 可随意指定日志格式, 只需在配置文件里面指定格式名称即可正确解析并存储
43 | * 支持按日期天,周,月,年. 自动分表或集合存储日志
44 | * 支持指定工作进程来快速消费队列数据,大流量也能实时解析并存储日志, 虚拟机中ab 实测2W并发延迟小于1秒
45 | * 注: 当海量流量来的时候发现解析存储延迟过高的情况,可将解析器部署到集群中其它多个节点同时消费队列数据,提升解析存储效率
46 |
47 |
48 | web服务 web
49 |
50 | * 已内置大屏监控web面板,流量情况一目了然
51 | * 同时支持 mysql 或者 mongodb 作为 数据源
52 |
53 |
54 |
55 | # 快速开始
56 | > 安装拓展
57 |
58 | sudo pip3 install -r requirements.txt
59 |
60 | > 启动 采集器
61 |
62 | sudo python3 main.py -r inputer -c config.ini
63 |
64 | > 启动 解析存储器
65 |
66 | sudo python3 main.py -r outputer -c config.ini
67 |
68 | > 启动 web服务
69 |
70 | sudo python3 main.py -r web -c config.ini
71 |
72 | > 查看命令行帮助
73 |
74 | python3 main.py --help
75 |
76 | * 以上三个应用均可单独部署和启用
77 |
78 | -r --run ; start ['inputer', 'outputer','web']
79 | -s --stop ; stop ['inputer', 'outputer']
80 | -c --config ; bind config.ini file
81 |
82 | > docker 支持
83 |
84 | docker pull jyolo/wlogger:v1.3 或者 docker build -t yourTagName .
85 |
86 | example:
87 | # 启动 web 服务
88 | docker run jyolo/wlogger:v1.3 -r web -c config.ini # 需要把配置文件复制或者挂载进 容器中/wLogger 目录内
89 |
90 | # 启动 解析存储器 服务
91 | docker run jyolo/wlogger:v1.3 -r outputer -c config.ini # 需要把配置文件复制或者挂载进 容器中/wLogger 目录内
92 |
93 | * 由于采集器 inputer 中切割日志操作,需要操作容器外部 nginx/apache 相关服务器,因此无法在docker中隔离环境下运行 .
94 | * 如果容器中有部署nginx 或者 apache 则可以
95 |
96 | # 配置详解
97 |
98 | > 公共配置
99 |
100 | # 当 inputer 和 outputer 中指定了 server_type = nginx 才需此配置
101 | [nginx]
102 | pid_path = /www/server/nginx/logs/nginx.pid # 指定 nginx.pid 的绝对路径
103 | server_conf = /www/server/nginx/conf/nginx.conf # 指定 nginx 配置文件的绝对路径
104 |
105 | # 当 inputer 和 outputer 中指定了 server_type = apache 才需此配置
106 | [apache]
107 | apachectl_bin = /www/server/apache/bin/apachectl # 指定 apachectl 命令的绝对路径
108 | server_conf = /www/server/apache/conf/httpd.conf # 指定 apache 配置文件的绝对路径
109 |
110 | # 当 inputer 和 outputer 中指定了 queue = redis 才需此配置
111 | [redis]
112 | host = 127.0.0.1
113 | port = 6379
114 | password = xxxxxxxx
115 | db = 1
116 |
117 | # 当 outputer 中 save_engine = mysql 或 web 中 data_engine = mysql 才需此配置
118 | [mysql]
119 | host = 127.0.0.1
120 | port = 3306
121 | username = nginx_logger
122 | password = xxxxxxxx
123 | db = nginx_logger
124 | table = logger
125 | split_save = day # 当有该配置项则代表开启自动分表 目前支持按 天,周,月,年 ;参数:[day, week, month ,year] ,进行存储
126 |
127 | # 当 outputer 中save_engine = mongodb 或 web 中 data_engine = mongodb 需此配置
128 | [mongodb]
129 | host = 127.0.0.1
130 | port = 27017
131 | username = logger_watcher
132 | password = xxxxxxxx
133 | db = nginx_logger
134 | collection = logger
135 | split_save = day # 当有该配置项则代表开启自动分集合 目前支持按 天,周,月,年 ;参数:[day, week, month ,year] ,进行存储
136 |
137 |
138 | > 日志采集端 配置
139 |
140 | [inputer]
141 | log_debug = True # 开启日志debug模式 会在项目中生成日志文件。 类似 : inputer_config.ini.log 名称的日志文件
142 | node_id = server_80 # 当前节点ID 唯一
143 | queue = redis # 队列配置 目前内置了 [redis , mongodb]
144 | queue_name = queue_logger # 队列 key 的名称
145 | max_batch_push_queue_size = 5000 # 每次最多批量插入队列多少条数据
146 | max_retry_open_file_time = 10 # 当文件读取失败之后重新打开日志文件,最多重试多少次
147 | max_retry_reconnect_time = 20 # 连接队列失败的时候,最多重试多少次
148 |
149 | [inputer.log_file.web1] # inputer.log_file.web1 中的 web1 代表应用名称 唯一 app_name
150 | server_type = nginx # 服务器应用 [nginx ,apache]
151 | file_path = /wwwlogs/ww.aaa.com.log # 日志绝对路径
152 | log_format_name = online # 配置文件中 日志名称 example : "access_log /www/wwwlogs/xxx.log online;" 中的 `online` 则代表启用的日志配置名称
153 | read_type = tail # 读取文件方式 支持 tail 从末尾最后一行开始 ; head 从头第一行开始 * 当文件较大的时候 建议使用 tail
154 | cut_file_type = filesize # 切割文件方式 支持 filesize 文件大小单位M ;time 指定当天时间 24:00
155 | cut_file_point = 200 # 切割文件条件节点 当 filesize 时 200 代表200M 切一次 ; 当 time 时 24:00 代表今天该时间 切一次
156 | cut_file_save_dir = /wwwlogs/cut_file/ # 日志切割后存储绝对路径
157 |
158 |
159 | [inputer.log_file.web2] # 支持同时采集多个应用日志 追加配置即可
160 | ..........................
161 |
162 |
163 |
164 | > 日志解析存储端
165 |
166 | [outputer]
167 | log_debug = True # 开启日志debug模式 会在项目中生成日志文件。 类似 : outpuer_config.ini.log 名称的日志文件
168 | save_engine = mongodb # 解析后的日志存储引擎目前支持 [mysql,mongodb]
169 | queue = redis # 队列引擎 此处需要和 inputer 采集端保持一致
170 | queue_name = queue_logger # 队列中 key 或 collection 集合的名称 此处需要和 inputer 采集端保持一致
171 | server_type = nginx # 服务器的类型
172 | worker_process_num = 1 # 指定工作进程数量 根据自己网站流量情况而定,一般4个worker即可
173 | max_batch_insert_db_size = 1 # 最多每次批量写入存储引擎的数量,根据自己应用情况而定,一般5000即可
174 | max_retry_reconnect_time = 200 # 连接存储引擎失败后,最多重试连接次数
175 |
176 | > 大屏监控端
177 |
178 | [web]
179 | env = development # 运行环境 development | production
180 | debug = True # 是否开启 debug
181 | secret_key = xxxx # flask session key
182 | host = 127.0.0.1 # 指定ip
183 | port = 5000 # 指定端口
184 | server_name = 127.0.0.1:5000 # 绑定域名和端口 (不推荐 ,如果是要nginx反代进行访问的话 请不要配置此项.)
185 | data_engine = mysql # 指定读取日志存储数据库引擎 目前内置了 [ mysql , mongodb ]
186 |
187 |
--------------------------------------------------------------------------------
/Src/ip2Region.py:
--------------------------------------------------------------------------------
1 | #-*- coding:utf-8 -*-
2 | """
3 | " ip2region python seacher client module
4 | "
5 | " Author: koma
6 | " Date : 2015-11-06
7 | """
8 | import struct, io, socket, sys
9 |
10 | class Ip2Region(object):
11 | __INDEX_BLOCK_LENGTH = 12
12 | __TOTAL_HEADER_LENGTH = 8192
13 |
14 | __f = None
15 | __headerSip = []
16 | __headerPtr = []
17 | __headerLen = 0
18 | __indexSPtr = 0
19 | __indexLPtr = 0
20 | __indexCount = 0
21 | __dbBinStr = ''
22 |
23 | def __init__(self, dbfile):
24 | self.initDatabase(dbfile)
25 |
26 | def memorySearch(self, ip):
27 | """
28 | " memory search method
29 | " param: ip
30 | """
31 | if not ip.isdigit(): ip = self.ip2long(ip)
32 |
33 | if self.__dbBinStr == '':
34 | self.__dbBinStr = self.__f.read() #read all the contents in file
35 | self.__indexSPtr = self.getLong(self.__dbBinStr, 0)
36 | self.__indexLPtr = self.getLong(self.__dbBinStr, 4)
37 | self.__indexCount = int((self.__indexLPtr - self.__indexSPtr)/self.__INDEX_BLOCK_LENGTH)+1
38 |
39 | l, h, dataPtr = (0, self.__indexCount, 0)
40 | while l <= h:
41 | m = int((l+h) >> 1)
42 | p = self.__indexSPtr + m*self.__INDEX_BLOCK_LENGTH
43 | sip = self.getLong(self.__dbBinStr, p)
44 |
45 | if ip < sip:
46 | h = m -1
47 | else:
48 | eip = self.getLong(self.__dbBinStr, p+4)
49 | if ip > eip:
50 | l = m + 1;
51 | else:
52 | dataPtr = self.getLong(self.__dbBinStr, p+8)
53 | break
54 |
55 | if dataPtr == 0: raise Exception("Data pointer not found")
56 |
57 | return self.returnData(dataPtr)
58 |
59 | def binarySearch(self, ip):
60 | """
61 | " binary search method
62 | " param: ip
63 | """
64 | if not ip.isdigit(): ip = self.ip2long(ip)
65 |
66 | if self.__indexCount == 0:
67 | self.__f.seek(0)
68 | superBlock = self.__f.read(8)
69 | self.__indexSPtr = self.getLong(superBlock, 0)
70 | self.__indexLPtr = self.getLong(superBlock, 4)
71 | self.__indexCount = int((self.__indexLPtr - self.__indexSPtr) / self.__INDEX_BLOCK_LENGTH) + 1
72 |
73 | l, h, dataPtr = (0, self.__indexCount, 0)
74 | while l <= h:
75 | m = int((l+h) >> 1)
76 | p = m*self.__INDEX_BLOCK_LENGTH
77 |
78 | self.__f.seek(self.__indexSPtr+p)
79 | buffer = self.__f.read(self.__INDEX_BLOCK_LENGTH)
80 | sip = self.getLong(buffer, 0)
81 | if ip < sip:
82 | h = m - 1
83 | else:
84 | eip = self.getLong(buffer, 4)
85 | if ip > eip:
86 | l = m + 1
87 | else:
88 | dataPtr = self.getLong(buffer, 8)
89 | break
90 |
91 | if dataPtr == 0: raise Exception("Data pointer not found")
92 |
93 | return self.returnData(dataPtr)
94 |
95 | def btreeSearch(self, ip):
96 | """
97 | " b-tree search method
98 | " param: ip
99 | """
100 | if not ip.isdigit(): ip = self.ip2long(ip)
101 |
102 | if len(self.__headerSip) < 1:
103 | headerLen = 0
104 | #pass the super block
105 | self.__f.seek(8)
106 | #read the header block
107 | b = self.__f.read(self.__TOTAL_HEADER_LENGTH)
108 | #parse the header block
109 | for i in range(0, len(b), 8):
110 | sip = self.getLong(b, i)
111 | ptr = self.getLong(b, i+4)
112 | if ptr == 0:
113 | break
114 | self.__headerSip.append(sip)
115 | self.__headerPtr.append(ptr)
116 | headerLen += 1
117 | self.__headerLen = headerLen
118 |
119 | l, h, sptr, eptr = (0, self.__headerLen, 0, 0)
120 | while l <= h:
121 | m = int((l+h) >> 1)
122 |
123 | if ip == self.__headerSip[m]:
124 | if m > 0:
125 | sptr = self.__headerPtr[m-1]
126 | eptr = self.__headerPtr[m]
127 | else:
128 | sptr = self.__headerPtr[m]
129 | eptr = self.__headerPtr[m+1]
130 | break
131 |
132 | if ip < self.__headerSip[m]:
133 | if m == 0:
134 | sptr = self.__headerPtr[m]
135 | eptr = self.__headerPtr[m+1]
136 | break
137 | elif ip > self.__headerSip[m-1]:
138 | sptr = self.__headerPtr[m-1]
139 | eptr = self.__headerPtr[m]
140 | break
141 | h = m - 1
142 | else:
143 | if m == self.__headerLen - 1:
144 | sptr = self.__headerPtr[m-1]
145 | eptr = self.__headerPtr[m]
146 | break
147 | elif ip <= self.__headerSip[m+1]:
148 | sptr = self.__headerPtr[m]
149 | eptr = self.__headerPtr[m+1]
150 | break
151 | l = m + 1
152 |
153 | if sptr == 0: raise Exception("Index pointer not found")
154 |
155 | indexLen = eptr - sptr
156 | self.__f.seek(sptr)
157 | index = self.__f.read(indexLen + self.__INDEX_BLOCK_LENGTH)
158 |
159 | l, h, dataPrt = (0, int(indexLen/self.__INDEX_BLOCK_LENGTH), 0)
160 | while l <= h:
161 | m = int((l+h) >> 1)
162 | offset = int(m * self.__INDEX_BLOCK_LENGTH)
163 | sip = self.getLong(index, offset)
164 |
165 | if ip < sip:
166 | h = m - 1
167 | else:
168 | eip = self.getLong(index, offset+4)
169 | if ip > eip:
170 | l = m + 1;
171 | else:
172 | dataPrt = self.getLong(index, offset+8)
173 | break
174 |
175 | if dataPrt == 0: raise Exception("Data pointer not found")
176 |
177 | return self.returnData(dataPrt)
178 |
179 | def initDatabase(self, dbfile):
180 | """
181 | " initialize the database for search
182 | " param: dbFile
183 | """
184 | try:
185 | self.__f = io.open(dbfile, "rb")
186 | except IOError as e:
187 | print("[Error]: %s" % e)
188 | sys.exit()
189 |
190 | def returnData(self, dataPtr):
191 | """
192 | " get ip data from db file by data start ptr
193 | " param: dsptr
194 | """
195 | dataLen = (dataPtr >> 24) & 0xFF
196 | dataPtr = dataPtr & 0x00FFFFFF
197 |
198 | self.__f.seek(dataPtr)
199 | data = self.__f.read(dataLen)
200 |
201 | return {
202 | "city_id": self.getLong(data, 0),
203 | "region" : data[4:]
204 | }
205 |
206 | def ip2long(self, ip):
207 | _ip = socket.inet_aton(ip)
208 | return struct.unpack("!L", _ip)[0]
209 |
210 | def isip(self, ip):
211 | p = ip.split(".")
212 |
213 | if len(p) != 4 : return False
214 | for pp in p:
215 | if not pp.isdigit() : return False
216 | if len(pp) > 3 : return False
217 | if int(pp) > 255 : return False
218 |
219 | return True
220 |
221 | def getLong(self, b, offset):
222 | if len(b[offset:offset+4]) == 4:
223 | return struct.unpack('I', b[offset:offset+4])[0]
224 | return 0
225 |
226 | def close(self):
227 | if self.__f != None:
228 | self.__f.close()
229 |
230 | self.__dbBinStr = None
231 | self.__headerPtr = None
232 | self.__headerSip = None
233 |
--------------------------------------------------------------------------------
/StorageAdapter/Mongodb.py:
--------------------------------------------------------------------------------
1 | from StorageAdapter.BaseAdapter import Adapter
2 | from pymongo import MongoClient,errors as pyerrors
3 | from threading import Thread
4 | import time,threading,os,traceback
5 |
6 |
7 | try:
8 | # Python 3.x
9 | from urllib.parse import quote_plus
10 | except ImportError:
11 | # Python 2.x
12 | from urllib import quote_plus
13 |
14 | class StorageAp(Adapter):
15 |
16 | db = None
17 | runner = None
18 |
19 |
20 | @classmethod
21 | def initStorage(cls,runnerObject):
22 | self = cls()
23 | self.runner = runnerObject
24 | self.conf = self.runner.conf
25 | self.logging = self.runner.logging
26 |
27 | if self.conf['mongodb']['username'] and self.conf['mongodb']['password']:
28 | mongo_url = 'mongodb://%s:%s@%s:%s/?authSource=%s' % \
29 | (
30 | quote_plus(self.conf['mongodb']['username']),
31 | quote_plus(self.conf['mongodb']['password']),
32 | self.conf['mongodb']['host'],
33 | int(self.conf['mongodb']['port']),
34 | self.conf['mongodb']['db']
35 | )
36 |
37 | else:
38 | mongo_url = 'mongodb://%s:%s/?authSource=%s' % \
39 | (
40 | self.conf['mongodb']['host'],
41 | int(self.conf['mongodb']['port']),
42 | self.conf['mongodb']['db']
43 | )
44 |
45 | mongodb = MongoClient(mongo_url)
46 | self.db = mongodb[self.conf['mongodb']['db']]
47 |
48 | return self
49 |
50 |
51 | def _parseData(self):
52 |
53 | pass
54 |
55 |
56 | def _intoDb(self):
57 |
58 | if 'max_retry_reconnect_time' in self.conf['outputer']:
59 | max_retry_reconnect_time = int(self.conf['outputer']['max_retry_reconnect_time'])
60 | else:
61 | max_retry_reconnect_time = 3
62 |
63 | retry_reconnect_time = 0
64 |
65 | while True:
66 | time.sleep(1)
67 | # self.logging.debug('\n outputerer -------pid: %s tid: %s parseQeueuData _intoDb len: %s' % (os.getpid(), threading.get_ident(), len(self.runner.dqueue) ))
68 |
69 | mongodb_outputer_client = self.db[self.runner.save_engine_conf['collection']]
70 | try:
71 | start_time = time.perf_counter()
72 |
73 | data = []
74 | for i in range(len(self.runner.dqueue)):
75 | data.append(self.runner.dqueue.pop())
76 |
77 | if len(data) == 0 :
78 | continue
79 | backup_for_push_back_queue = data
80 | # before_into_storage
81 | self._handle_queue_data_before_into_storage()
82 |
83 | res = mongodb_outputer_client.insert_many(data, ordered=False)
84 |
85 | # after_into_storage
86 | self._handle_queue_data_after_into_storage()
87 |
88 | retry_reconnect_time = 0
89 |
90 | end_time = time.perf_counter()
91 | self.logging.debug("\n outputerer -------pid: %s -- insert into mongodb: %s---- end 耗时: %s \n" % (
92 | os.getpid(), len(res.inserted_ids), round(end_time - start_time, 3)))
93 |
94 | # 消费完 dqueue 里面的数据后 取出标识 标识已完成
95 | if hasattr(self.runner,'share_worker_list') and self.runner.share_worker_list != None:
96 | self.runner.share_worker_list.pop()
97 |
98 |
99 | except pyerrors.PyMongoError as e:
100 |
101 | time.sleep(1)
102 | retry_reconnect_time = retry_reconnect_time + 1
103 | if retry_reconnect_time >= max_retry_reconnect_time:
104 | self.runner.push_back_to_queue(backup_for_push_back_queue)
105 | self.logging.error('重试重新链接 mongodb 超出最大次数 %s' % max_retry_reconnect_time)
106 | raise pyerrors.PyMongoError('重试重新链接 mongodb 超出最大次数 %s' % max_retry_reconnect_time)
107 | else:
108 | self.logging.warn("\n outputerer -------pid: %s -- retry_reconnect_mongodb at: %s time---- \n" % (
109 | os.getpid(), retry_reconnect_time))
110 | continue
111 |
112 |
113 |
114 | def pushDataToStorage(self):
115 | retry_reconnect_time = 0
116 |
117 | while True:
118 | time.sleep(1)
119 | self._getTableName('collection')
120 |
121 |
122 | if retry_reconnect_time == 0:
123 |
124 | # 获取队列数据
125 | queue_data = self.runner.getQueueData()
126 | if len(queue_data) == 0:
127 | self.logging.debug('pid: %s 暂无数据 等待 queue数据 ' % os.getpid())
128 | time.sleep(1)
129 | continue
130 |
131 | start_time = time.perf_counter()
132 |
133 | # 错误退回队列 (未解析的原始的数据)
134 | backup_for_push_back_queue = []
135 | _data = []
136 | for item in queue_data:
137 | if isinstance(item, bytes):
138 | item = item.decode(encoding='utf-8')
139 |
140 | backup_for_push_back_queue.append(item)
141 | item = self.runner._parse_line_data(item)
142 | if item:
143 | _data.append(item)
144 |
145 |
146 |
147 | end_time = time.perf_counter()
148 |
149 | take_time = round(end_time - start_time, 3)
150 | self.logging.debug(
151 | '\n outputerer ---pid: %s tid: %s reg data len:%s; take time : %s \n' %
152 | (os.getpid(), threading.get_ident(), len(_data), take_time))
153 |
154 |
155 | if 'max_retry_reconnect_time' in self.conf['outputer']:
156 | max_retry_reconnect_time = int(self.conf['outputer']['max_retry_reconnect_time'])
157 | else:
158 | max_retry_reconnect_time = 3
159 |
160 | mongodb_outputer_client = self.db[self.table]
161 | try:
162 | start_time = time.perf_counter()
163 |
164 | if len(_data) == 0:
165 | continue
166 |
167 | # before_into_storage
168 | self._handle_queue_data_before_into_storage()
169 |
170 | res = mongodb_outputer_client.insert_many(_data, ordered=False)
171 |
172 | # after_into_storage
173 | self._handle_queue_data_after_into_storage()
174 |
175 | # 重置 retry_reconnect_time
176 | retry_reconnect_time = 0
177 |
178 | end_time = time.perf_counter()
179 | self.logging.debug("\n outputerer -------pid: %s -- insert into mongodb: %s---- end 耗时: %s \n" % (
180 | os.getpid(), len(res.inserted_ids), round(end_time - start_time, 3)))
181 |
182 | except pyerrors.PyMongoError as e:
183 | time.sleep(1)
184 | retry_reconnect_time = retry_reconnect_time + 1
185 | if retry_reconnect_time >= max_retry_reconnect_time:
186 | self.runner.rollBackQueue(backup_for_push_back_queue)
187 | self.logging.error('重试重新链接 mongodb 超出最大次数 %s' % max_retry_reconnect_time)
188 | raise Exception('重试重新链接 mongodb 超出最大次数 %s' % max_retry_reconnect_time)
189 | else:
190 | self.logging.warn("\n outputerer -------pid: %s -- retry_reconnect_mongodb at: %s time---- \n" % (
191 | os.getpid(), retry_reconnect_time))
192 | continue
193 |
194 |
195 | # 在持久化存储之前 对 队列中的数据 进行预处理 ,比如 update ,delete 等操作
196 | def _handle_queue_data_before_into_storage(self):
197 | pass
198 |
199 | # 在持久化存储之前 对 队列中的数据 进行预处理 ,比如 update ,delete 等操作
200 | def _handle_queue_data_after_into_storage(self):
201 | # if (hasattr(self.runner, 'queue_data_ids')):
202 | # ids = self.runner.queue_data_ids
203 | # self.db[self.runner.queue_key].update_many(
204 | # {'_id': {'$in': ids}},
205 | # {
206 | # '$set': {'out_queue': 1},
207 | # '$currentDate': {'ttl': True}
208 | # },
209 | #
210 | # )
211 | pass
212 |
213 |
214 |
215 |
216 |
217 |
--------------------------------------------------------------------------------
/ParserAdapter/Apache.py:
--------------------------------------------------------------------------------
1 | from ParserAdapter.BaseAdapter import Adapter,ParseError,ReCompile
2 | import re,os,shutil,json
3 |
4 |
5 | class Handler(Adapter):
6 |
7 | def __init__(self,*args ,**kwargs):
8 | super(Handler,self).__init__(*args,**kwargs)
9 |
10 |
11 | def getLogFormat(self):
12 | # "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %I %O"
13 | return {
14 | ### 非 nginx 日志变量 附加自定义字段
15 | '$node_id': {
16 | 'mysql_field_type': 'varchar(255)',
17 | 'mysql_key_field': True
18 | },
19 | '$app_name': {
20 | 'mysql_field_type': 'varchar(255)',
21 | },
22 | ### 非 nginx 日志变量 附加自定义字段
23 |
24 | '%h':{
25 | 'nickname': 'ip',
26 | 'mysql_field_type': 'varchar(15)',
27 | 'mysql_key_field': [
28 | '%t.timestamp',
29 | ['%>s', '%r.request_url', '%r.request_method']
30 | ],
31 | 'extend_field': {
32 | 'isp': {
33 | 'mysql_field_type': 'varchar(30)',
34 | },
35 | 'city': {
36 | 'mysql_field_type': 'varchar(30)',
37 | 'mysql_key_field': ['%>s'],
38 | },
39 | 'city_id': {
40 | 'mysql_field_type': 'int(10)',
41 | },
42 | 'province': {
43 | 'mysql_field_type': 'varchar(30)',
44 | 'mysql_key_field': [
45 | '%t.timestamp',
46 | '%t.timestamp',
47 | ],
48 | },
49 | 'country': {
50 | 'mysql_field_type': 'varchar(30)',
51 | }
52 | }
53 | },
54 | '%l':{
55 | 'nickname': 'remote_logname',
56 | },
57 | '%u':{
58 | 'nickname': 'remote_user'
59 | },
60 | '%t': {
61 | 'nickname': 'time',
62 | 'extend_field': {
63 | 'time_str': {
64 | 'mysql_field_type': 'datetime',
65 | 'mysql_key_field': True
66 | },
67 | 'timestamp': {
68 | 'mysql_field_type': 'int(10)',
69 | 'mysql_key_field': True
70 | },
71 | }
72 | },
73 | '%r': {
74 | 'nickname': 'request',
75 | 'extend_field': {
76 | 'request_method': {
77 | 'mysql_field_type': 'varchar(10)',
78 | 'mysql_key_field': True,
79 | },
80 | 'request_url': {
81 | 'mysql_field_type': 'varchar(255)',
82 | 'mysql_key_field': ['%t.timestamp', '%t.timestamp'],
83 | },
84 | 'args': {
85 | 'mysql_field_type': 'text',
86 | },
87 | 'server_protocol': {
88 | 'mysql_field_type': 'varchar(10)',
89 | },
90 | }
91 | },
92 | '%>s': {
93 | 'nickname': 'status_code',
94 | },
95 | '%b': {
96 | 'nickname': 'bytes_sent',
97 | },
98 | '%D':{
99 | 'nickname': 'request_time',
100 | },
101 | '%m':{
102 | 'nickname': 'request_method',
103 | },
104 | '%{Referer}i': {
105 | 'nickname': 'referer',
106 | },
107 | '%{User-Agent}i': {
108 | 'nickname': 'ua',
109 | },
110 | '%I': {
111 | 'nickname': 'bytes_received',
112 | 're': '\d+',
113 | },
114 | '%O': {
115 | 'nickname': 'bytes_sent_with_header',
116 | 're': '\d+',
117 | },
118 | }
119 |
120 |
121 | """
122 | 日志解析
123 | """
124 | def parse(self,log_format_name='',log_line=''):
125 |
126 |
127 | log_format_list = self.log_line_pattern_dict[log_format_name]['log_format_list']
128 | log_format_recompile = self.log_line_pattern_dict[log_format_name]['log_format_recompile']
129 |
130 | res = log_format_recompile.match(log_line)
131 |
132 |
133 | if res == None:
134 | raise ParseError('解析日志失败,请检查client 配置中 日志的 格式名称是否一致 log_format_name')
135 |
136 | matched = list(res.groups())
137 |
138 | if len(matched) == len(log_format_list):
139 | data = {}
140 | del_key_name = []
141 |
142 | for i in range(len(list(log_format_list))):
143 | key_name = log_format_list[i]
144 |
145 | # request 参数不支持别名
146 | if 'nickname' in self.getLogFormat()[ log_format_list[i]] :
147 | key_name = self.getLogFormat()[ log_format_list[i]]['nickname']
148 |
149 | # 解析 %h 成对应的 地理位置信息 isp,city,city_id,province,country,ip,
150 | if key_name == '%h':
151 | ip_data = self.parse_ip_to_area(matched[i])
152 | data.update(ip_data)
153 |
154 | # 解析 %r 成 request_method ,request_url ,args ,server_protocol ,
155 | if key_name == '%r':
156 | request_extend_data = self.parse_request_to_extend(matched[i])
157 | data.update(request_extend_data)
158 | del_key_name.append(key_name)
159 |
160 | # 解析 %t 成 timestr , timestamp
161 | if key_name == '%t':
162 | time_data = self.parse_time_to_str('time_local',matched[i])
163 | data.update(time_data)
164 | del_key_name.append(key_name)
165 |
166 | data[key_name] = matched[i]
167 |
168 |
169 |
170 |
171 | # 剔除掉 解析出拓展字符串的 字段
172 | for i in del_key_name:
173 | del data[i]
174 |
175 |
176 | return data
177 |
178 |
179 |
180 | # 从配置获取所有的日志配置项
181 | def getLoggerFormatByServerConf(self,server_conf_path):
182 |
183 | # 根据配置文件 自动获取 log_format 字符串
184 | with open(server_conf_path, 'rb') as fd:
185 | content = fd.read().decode(encoding="utf-8")
186 |
187 | format_list = {}
188 |
189 | log_list = re.findall(r'LogFormat\s?\"([\S\s]*?)\"\s?(\w+)\n' ,content)
190 |
191 |
192 | if len(log_list) == 0:
193 | return format_list
194 |
195 | # 从日志格式字符串中提取日志变量
196 | for i in log_list:
197 | res = re.findall(r'(\%[\>|\{]?[a-zA-Z|\-|\_]+[\}|\^]?\w?)', i[0].strip())
198 | if len(res):
199 | format_list[i[1]] = {
200 | 'log_format_str':i[0].strip(),
201 | 'log_format_vars':self.LOG_FORMAT_SPLIT_TAG.join(res)
202 | }
203 |
204 |
205 | del content
206 |
207 |
208 | return format_list
209 |
210 |
211 |
212 | # 重启日志进程
213 | def rotatelog(self,server_conf,log_path ,target_file ):
214 |
215 |
216 | try:
217 |
218 | if not os.path.exists(server_conf['apachectl_bin']):
219 | raise FileNotFoundError('apachectl_bin : %s 命令不存在' % server_conf['apachectl_bin'])
220 |
221 | if not os.path.exists(log_path):
222 | raise FileNotFoundError('日志: %s 不存在' % log_path)
223 |
224 |
225 |
226 | # 这里需要注意 日志目录的 权限 是否有www 否则会导致 ngixn 重开日志问件 无法写入的问题
227 | cmd = '%s graceful' % server_conf['apachectl_bin']
228 | shutil.move(log_path, target_file)
229 |
230 | res = os.popen(cmd)
231 | if len(res.readlines()) > 0:
232 | cmd_res = ''
233 | for i in res.readlines():
234 | cmd_res += i + '\n'
235 | raise Exception ('reload 服务器进程失败: %s' % cmd_res)
236 |
237 | return True
238 |
239 | except Exception as e:
240 | return '切割日志失败 : %s ; error class : %s error info : %s' % (target_file ,e.__class__, e.args)
241 |
242 | """
243 | 找到匹配中的日志变量
244 | """
245 | def __replaceLogVars(self,matched):
246 |
247 |
248 | s = matched.group()
249 |
250 | if s not in self.getLogFormat():
251 |
252 | raise ValueError('handle 里面不存在日志变量:%s' % s)
253 |
254 | if 're' in self.getLogFormat()[s]:
255 | re_str = self.getLogFormat()[s]['re']
256 | else:
257 | re_str = '[\s|\S]*?'
258 |
259 | return '(%s)' % re_str
260 |
261 | """
262 | 根据录入的格式化字符串 返回 parse 所需 log_format 配置
263 | """
264 | def getLogFormatByConfStr(self ,log_format_str,log_format_vars ,log_format_name ,log_type):
265 |
266 |
267 | # 日志格式不存在 则 预编译
268 | if log_format_name not in self.log_line_pattern_dict:
269 |
270 | # 过滤下 正则中 特殊字符
271 | log_format_str = log_format_str.strip() \
272 | .replace('[', '\[').replace(']', '\]') \
273 | .replace('(', '\(').replace(')', '\)')
274 |
275 |
276 | if (log_type == 'string'):
277 |
278 |
279 | # 获取日志变量
280 | log_format_list = log_format_vars.split(self.LOG_FORMAT_SPLIT_TAG)
281 |
282 | # 按照日志格式顺序 替换日志变量成正则 进行预编译
283 | format = re.sub(r'(\%[\>|\{]?[a-zA-Z|\-|\_]+[\}|\^]?\w?)', self.__replaceLogVars, log_format_str).strip()
284 | try:
285 | re_compile = re.compile(format, re.I)
286 | except re.error:
287 | raise Exception('预编译错误,请检查日志字符串中是否包含特殊字符串; 日志:%s' % log_format_str)
288 |
289 |
290 | self.log_line_pattern_dict[log_format_name] = {
291 | 'log_format_list': log_format_list,
292 | 'log_format_recompile':re_compile
293 | }
294 |
295 |
296 |
297 |
298 |
--------------------------------------------------------------------------------
/webServer/divers/mongo.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | from flask import current_app,request,session
3 | from webServer.customer import Func,ApiCorsResponse
4 | import time,re
5 |
6 |
7 |
8 |
9 | class MongoDb():
10 |
11 | today = time.strftime('%Y-%m-%d', time.localtime(time.time()))
12 |
13 | @classmethod
14 | def get_total_ip(cls):
15 | res = current_app.db[current_app.db_engine_table].aggregate([
16 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}}},
17 | {'$group': {'_id': '$ip'}},
18 | {'$group': {'_id': '','total_num':{'$sum':1} }},
19 | {'$project': { '_id': 0}},
20 | {'$sort': {'total_num': -1}},
21 | ])
22 | res = list(res)
23 | if len(res) == 0 :
24 | return ApiCorsResponse.response({})
25 | return ApiCorsResponse.response(res[0])
26 |
27 | @classmethod
28 | def get_total_pv(cls):
29 | res = current_app.db[current_app.db_engine_table].find({'time_str': {'$regex': '^%s' % cls.today}}).count()
30 | return ApiCorsResponse.response({'total_num':res})
31 |
32 | @classmethod
33 | def get_request_num_by_url(cls):
34 | # if request.args.get('type') == 'init':
35 | # # 一分钟 * 10 10分钟
36 | # limit = 60 * 10
37 | # else:
38 | # limit = 5
39 |
40 |
41 | collection = current_app.db_engine_table
42 |
43 |
44 | # total = current_app.db[collection].find({'time_str': {'$regex': '^%s' % cls.today}}).count()
45 |
46 | res = current_app.db[collection].aggregate([
47 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}}},
48 | {'$group': {'_id': '$request_url', 'total_num': {'$sum': 1}}},
49 | {'$project': {
50 | 'request_url': '$_id',
51 | 'total_num': 1,
52 | '_id': 0,
53 | # 'percent': {'$toDouble': {'$substr': [{'$multiply': [{'$divide': ['$total_num', total]}, 100]}, 0, 4]}}
54 | }
55 | },
56 | {'$sort': {'total_num': -1}},
57 | {'$limit': 50}
58 | ])
59 |
60 | data = list(res)
61 | data.reverse()
62 |
63 | return ApiCorsResponse.response(data)
64 |
65 | @classmethod
66 | def get_request_num_by_ip(cls):
67 | if request.args.get('type') == 'init':
68 | # 一分钟 * 10 10分钟
69 | limit = 60 * 10
70 | else:
71 | limit = 5
72 |
73 | session['now_timestamp'] = int(time.time())
74 |
75 |
76 | res = current_app.db[current_app.db_engine_table].aggregate([
77 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}}},
78 | {'$group': {'_id': '$ip', 'total_num': {'$sum': 1}}},
79 | {'$project': {
80 | 'ip':'$_id',
81 | 'total_num': 1,
82 | '_id':0
83 | # 'percent':{ '$toDouble': {'$substr':[ {'$multiply':[ {'$divide':['$total_num' , total]} ,100] } ,0,4 ] } }
84 | }
85 | },
86 | {'$sort': {'total_num': -1}},
87 | {'$limit': 50}
88 | ])
89 |
90 |
91 |
92 | data = list(res)
93 | data.reverse()
94 | return ApiCorsResponse.response(data)
95 |
96 | @classmethod
97 | def get_request_urls_by_ip(cls):
98 | if not request.args.get('ip'):
99 | return ApiCorsResponse.response('缺少ip参数', False)
100 |
101 | session['now_timestamp'] = int(time.time())
102 |
103 | collection = current_app.db_engine_table
104 |
105 |
106 |
107 | res = current_app.db[collection].aggregate([
108 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}, 'ip': request.args.get('ip')}},
109 | {'$group': {'_id': '$request_url', 'total_num': {'$sum': 1}}},
110 | {'$project': {
111 | 'total_num': 1,
112 | 'request_url': '$_id',
113 | }
114 | },
115 | {'$sort': {'total_num': -1}},
116 | {'$limit': 20}
117 | ])
118 |
119 |
120 | data = list(res)
121 | data.reverse()
122 | return ApiCorsResponse.response(data)
123 |
124 | @classmethod
125 | def get_request_num_by_status(cls):
126 | session['now_timestamp'] = int(time.time())
127 |
128 |
129 |
130 | res = current_app.db[current_app.db_engine_table].aggregate([
131 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}, 'status_code': {'$ne': '200'}}},
132 | {'$group': {'_id': '$status_code', 'total_num': {'$sum': 1}}},
133 | {'$project': {'status_code': '$_id', 'total_num': 1, '_id': 0}},
134 | {'$sort': {'total_num': -1}},
135 | ])
136 |
137 | data = list(res)
138 | data.reverse()
139 |
140 | return ApiCorsResponse.response(data)
141 |
142 | @classmethod
143 | def get_request_num_by_status_code(cls):
144 | if not request.args.get('code'):
145 | return ApiCorsResponse.response('缺少code参数', False)
146 |
147 | session['now_timestamp'] = int(time.time())
148 |
149 |
150 |
151 | arg = re.findall('\d+?', request.args.get('code'))
152 | res = current_app.db[current_app.db_engine_table].aggregate([
153 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}, 'status': ''.join(arg)}},
154 | {'$group': {'_id': '$request_url', 'total_num': {'$sum': 1}}},
155 | {'$project': {'request_url': '$_id', 'total_num': 1, '_id': 0}},
156 | {'$sort': {'total_num': -1}},
157 | ])
158 |
159 | data = list(res)
160 | data.reverse()
161 |
162 | return ApiCorsResponse.response(data)
163 |
164 | @classmethod
165 | def get_request_num_by_secends(cls):
166 | if request.args.get('type') == 'init':
167 | # 一分钟 * 10 10分钟
168 | limit = 60 * 10
169 | else:
170 | limit = 5
171 |
172 |
173 | res = current_app.db[current_app.db_engine_table].aggregate([
174 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}}},
175 | {'$group': {'_id': '$timestamp', 'total_num': {'$sum': 1}}},
176 | {'$project': {'timestamp': '$_id', 'total_request_num': '$total_num', '_id': 0}},
177 | {'$sort': {'timestamp': -1}},
178 | {'$limit': limit}
179 | ])
180 |
181 | data = []
182 | for i in res:
183 | item = {}
184 | # item['timestamp'] = time.strftime('%H:%M:%S', time.localtime(i['timestamp']))
185 | # * 1000 for js timestamp
186 | item['timestamp'] = i['timestamp'] * 1000
187 | item['total_request_num'] = i['total_request_num']
188 | data.append(item)
189 |
190 | data.reverse()
191 | return ApiCorsResponse.response(data)
192 |
193 | @classmethod
194 | def get_network_traffic_by_minute(cls):
195 | current_hour = time.strftime('%Y-%m-%d %H', time.localtime(time.time()))
196 |
197 | res = current_app.db[current_app.db_engine_table].aggregate([
198 | {'$match': {'time_str': {'$regex': '^%s' % current_hour}}},
199 | {'$project': {
200 | '_id': 0,
201 | 'time_minute': {'$dateToString': {
202 | 'format': '%Y-%m-%d %H:%M', 'date': {'$dateFromString': {'dateString': '$time_str'}}
203 | }
204 | },
205 | 'request_length': {'$toInt': '$request_length'},
206 | 'bytes_sent': {'$toInt': '$bytes_sent'}
207 | },
208 | },
209 | {'$group': {
210 | '_id': '$time_minute',
211 | 'in_network_sum': {'$sum': '$request_length'},
212 | 'out_network_sum': {'$sum': '$bytes_sent'}
213 | }
214 | },
215 | {'$project': {'_id': 0, 'time_str': '$_id', 'in_network': {'$divide': ['$in_network_sum', 1024]},
216 | 'out_network': {'$divide': ['$out_network_sum', 1024]}}},
217 | {'$sort': {'time_str': -1}},
218 |
219 | ])
220 |
221 | data = []
222 | for i in res:
223 | i['time_str'] = int(time.mktime(time.strptime(i['time_str'], '%Y-%m-%d %H:%M')))
224 | data.append(i)
225 |
226 | data.reverse()
227 |
228 | return ApiCorsResponse.response(data)
229 |
230 | @classmethod
231 | def get_ip_pv_num_by_minute(cls):
232 |
233 | current_hour = time.strftime('%Y-%m-%d %H', time.localtime(time.time()))
234 |
235 | res = current_app.db[current_app.db_engine_table].aggregate([
236 | {'$match': {'time_str': {'$regex': '^%s' % current_hour}}},
237 | {'$project': {
238 | '_id': 0,
239 | 'time_minute': {
240 | '$dateToString': {
241 | 'format': '%Y-%m-%d %H:%M', 'date': {'$dateFromString': {'dateString': '$time_str'}}
242 | }
243 | },
244 | 'ip': 1
245 | },
246 | },
247 | {'$group': {
248 | '_id': '$time_minute',
249 | 'pv_num': {'$sum': 1},
250 | 'ip_arr': {'$addToSet': '$ip'}
251 | }
252 | },
253 | {'$project': {'_id': 0, 'time_str': '$_id', 'pv_num': 1, 'ip_num': {'$size': '$ip_arr'}}},
254 | {'$sort': {'time_str': -1}},
255 | ])
256 |
257 | data = []
258 | for i in res:
259 | i['time_str'] = int(time.mktime(time.strptime(i['time_str'], '%Y-%m-%d %H:%M')))
260 | data.append(i)
261 |
262 | data.reverse()
263 | return ApiCorsResponse.response(data)
264 |
265 | @classmethod
266 | def get_request_num_by_province(cls):
267 | session['now_timestamp'] = int(time.time())
268 |
269 | res = current_app.db[current_app.db_engine_table].aggregate([
270 | {'$match': {'time_str': {'$regex': '^%s' % cls.today}}},
271 | {'$group': {'_id': '$province', 'total_num': {'$sum': 1}}},
272 | {'$project': {'province': '$_id', 'value': '$total_num', '_id': 0}},
273 | {'$sort': {'total_num': -1}},
274 | ])
275 |
276 | data = list(res)
277 | data.reverse()
278 |
279 | return ApiCorsResponse.response(data)
280 |
281 | @classmethod
282 | def get_spider_by_ua(cls):
283 | res = current_app.db[current_app.db_engine_table].aggregate([
284 | {'$match': {
285 | 'time_str': {'$regex': '^%s' % cls.today} ,
286 | 'ua':{'$regex':'spider'}
287 | }
288 | },
289 | {'$group': {'_id': '$ua', 'total_num': {'$sum': 1}}},
290 | {'$project': {'ua': '$_id', 'total_num': 1, '_id': 0}},
291 | {'$sort': {'total_num': -1}},
292 | ])
293 |
294 | data = list(res)
295 | data.reverse()
296 | return ApiCorsResponse.response(data)
297 |
--------------------------------------------------------------------------------
/webServer/divers/mysql.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | from flask import Response,current_app,request,session
3 | from sqlalchemy import text
4 | from webServer.customer import Func,ApiCorsResponse
5 | import json,time,datetime
6 |
7 |
8 |
9 | # 自定义mysql 数据获取class
10 | class MysqlDb():
11 |
12 | today = time.strftime('%Y-%m-%d', time.localtime(time.time()))
13 |
14 | @classmethod
15 | def get_total_ip(cls):
16 | with current_app.db.connect() as cursor:
17 | sql = text("""
18 | select count(DISTINCT ip) as total_num from {0}
19 | where `timestamp` >= UNIX_TIMESTAMP(:today)
20 | """.format(current_app.db_engine_table)
21 | )
22 |
23 | res = cursor.execute(sql, {'today': cls.today})
24 |
25 | total = Func.fetchone(res)
26 | return ApiCorsResponse.response(total)
27 |
28 | @classmethod
29 | def get_total_pv(cls):
30 | with current_app.db.connect() as cursor:
31 | sql = text("""
32 | select count(*) as total_num from {0} FORCE INDEX(timestamp)
33 | where `timestamp` >= UNIX_TIMESTAMP(:today)
34 | """.format(current_app.db_engine_table)
35 | )
36 |
37 | res = cursor.execute(sql, {'today': cls.today})
38 |
39 | total = Func.fetchone(res)
40 | return ApiCorsResponse.response(total)
41 |
42 | @classmethod
43 | def get_request_num_by_url(cls):
44 |
45 |
46 | with current_app.db.connect() as cursor:
47 | sql = text("""
48 | select count(*) as total_num,request_url from {0}
49 | where `timestamp` >= UNIX_TIMESTAMP(:today)
50 | group by request_url
51 | order by total_num desc
52 | limit 10
53 | """.format(current_app.db_engine_table)
54 | )
55 |
56 | res = cursor.execute(sql,{'today':cls.today})
57 | data = Func.fetchall(res)
58 | data.reverse()
59 | return ApiCorsResponse.response(data)
60 |
61 | @classmethod
62 | def get_request_num_by_ip(cls):
63 | with current_app.db.connect() as cursor:
64 | sql = text("""
65 | select count(*) as total_num,ip from {0}
66 | where `timestamp` >= UNIX_TIMESTAMP(:today)
67 | group by ip
68 | order by total_num desc
69 | limit 50
70 | """.format(current_app.db_engine_table)
71 | )
72 |
73 | res = cursor.execute(sql, {'today': cls.today})
74 | data = Func.fetchall(res)
75 | data.reverse()
76 | return ApiCorsResponse.response(data)
77 |
78 | @classmethod
79 | def get_request_urls_by_ip(cls):
80 | if not request.args.get('ip'):
81 | return ApiCorsResponse.response('缺少ip参数', False)
82 |
83 | with current_app.db.connect() as cursor:
84 | sql = text("""
85 | select count(*) as total_num ,request_url from {0}
86 | where FROM_UNIXTIME(`timestamp`,'%Y-%m-%d') = :today and ip = :ip
87 | group by request_url
88 | order by total_num desc
89 | limit 50
90 | """.format(current_app.db_engine_table)
91 | )
92 |
93 | ip = request.args.get('ip')
94 |
95 |
96 | res = cursor.execute(sql, {'today': cls.today,'ip':ip})
97 | data = Func.fetchall(res)
98 | data.reverse()
99 |
100 | return ApiCorsResponse.response(data)
101 |
102 | @classmethod
103 | def get_request_num_by_status(cls):
104 | with current_app.db.connect() as cursor:
105 | sql = text("""
106 | select count(*) as total_num,`status_code` from {0}
107 | where `timestamp` >= UNIX_TIMESTAMP(:today) and `status_code` != 200
108 | group by status_code
109 | order by total_num desc
110 | limit 50
111 | """.format(current_app.db_engine_table)
112 | )
113 |
114 |
115 |
116 | res = cursor.execute(sql, {'today': cls.today})
117 | data = Func.fetchall(res)
118 | data.reverse()
119 |
120 | return ApiCorsResponse.response(data)
121 |
122 | @classmethod
123 | def get_request_num_by_status_code(cls):
124 |
125 | if not request.args.get('code'):
126 | return ApiCorsResponse.response('缺少code参数', False)
127 |
128 | with current_app.db.connect() as cursor:
129 | sql = text("""
130 | select count(*) as total_num,`request_url` from {0}
131 | where FROM_UNIXTIME(`timestamp`,'%Y-%m-%d') = :today AND `status_code` = :status_code
132 | group by request_url
133 | order by total_num desc
134 | limit 30
135 | """.format(current_app.db_engine_table)
136 | )
137 |
138 | code = request.args.get('code')
139 |
140 |
141 | res = cursor.execute(sql, {'today': cls.today,'status_code':code})
142 | data = Func.fetchall(res)
143 | data.reverse()
144 | return ApiCorsResponse.response(data)
145 |
146 | @classmethod
147 | def get_request_num_by_secends(cls):
148 |
149 | if request.args.get('type') == 'init':
150 | # 一分钟 * 10 10分钟
151 | limit = 60 * 10
152 | else:
153 | limit = 5
154 |
155 | with current_app.db.connect() as cursor:
156 | sql = text("""
157 | select count(*) as total_request_num,`timestamp` from {0}
158 | where FROM_UNIXTIME(`timestamp`,'%Y-%m-%d') = :today
159 | group by `timestamp`
160 | order by `timestamp` desc
161 | limit {1}
162 | """.format(current_app.db_engine_table,limit)
163 | )
164 |
165 |
166 | res = cursor.execute(sql, {'today': cls.today})
167 | res = Func.fetchall(res)
168 |
169 | data = []
170 | for i in res:
171 |
172 | item = {}
173 | # item['timestamp'] = time.strftime('%H:%M:%S', time.localtime(i['timestamp']))
174 | # * 1000 for js timestamp
175 | item['timestamp'] = i['timestamp'] * 1000
176 | item['total_request_num'] = i['total_request_num']
177 | data.append(item)
178 |
179 | data.reverse()
180 |
181 | return ApiCorsResponse.response(data)
182 |
183 | @classmethod
184 | def get_network_traffic_by_minute(self):
185 | with current_app.db.connect() as cursor:
186 | current_hour_str = time.strftime('%Y-%m-%d %H', time.localtime(time.time()))
187 | next_hour_str = time.strftime('%Y-%m-%d %H', time.localtime(time.time() + 3600))
188 | current_hour = int(time.mktime(time.strptime(current_hour_str, '%Y-%m-%d %H')))
189 | next_hour = int(time.mktime(time.strptime(next_hour_str, '%Y-%m-%d %H')))
190 |
191 | sql = text("""
192 | select round((sum(bytes_sent) /1024),2) as out_network, round((sum(request_length) / 1024) ,2) as in_network ,unix_timestamp(STR_TO_DATE(time_str,'%Y-%m-%d %H:%i')) as time_str
193 | from {0}
194 | where `timestamp` >= {1} and `timestamp` < {2}
195 | GROUP BY MINUTE(time_str)
196 | ORDER BY MINUTE(time_str) desc
197 | limit 10
198 | """.format(current_app.db_engine_table, current_hour, next_hour)
199 | )
200 |
201 | res = cursor.execute(sql)
202 | data = Func.fetchall(res)
203 | data.reverse()
204 |
205 | return ApiCorsResponse.response(data)
206 |
207 | @classmethod
208 | def get_ip_pv_num_by_minute(cls):
209 | with current_app.db.connect() as cursor:
210 | current_hour_str = time.strftime('%Y-%m-%d %H', time.localtime(time.time()))
211 | next_hour_str = time.strftime('%Y-%m-%d %H', time.localtime(time.time() + 3600))
212 | current_hour = int(time.mktime(time.strptime(current_hour_str, '%Y-%m-%d %H')))
213 | next_hour = int(time.mktime(time.strptime(next_hour_str, '%Y-%m-%d %H')))
214 |
215 | sql = text("""
216 | select count(DISTINCT ip) as ip_num,count(*) as pv_num ,unix_timestamp(STR_TO_DATE(time_str,'%Y-%m-%d %H:%i')) as time_str
217 | from {0}
218 | where `timestamp` >= {1} and `timestamp` < {2}
219 | GROUP BY MINUTE(time_str)
220 | ORDER BY MINUTE(time_str) desc
221 | limit 10
222 | """.format(current_app.db_engine_table ,current_hour ,next_hour )
223 | )
224 |
225 |
226 | res = cursor.execute(sql)
227 | data = Func.fetchall(res)
228 | data.reverse()
229 | return ApiCorsResponse.response(data)
230 |
231 | @classmethod
232 | def get_request_num_by_province(cls):
233 |
234 | with current_app.db.connect() as cursor:
235 | sql = text("""
236 | select count(*) as value,`province` from {0}
237 | where `timestamp` >= UNIX_TIMESTAMP(:today) AND province != '0'
238 | group by `province`
239 | order by `value` desc
240 | """.format(current_app.db_engine_table)
241 | )
242 |
243 | res = cursor.execute(sql, {'today': cls.today})
244 | data = Func.fetchall(res)
245 | data.reverse()
246 |
247 | return ApiCorsResponse.response(data)
248 |
249 | @classmethod
250 | def get_spider_by_ua(cls):
251 | with current_app.db.connect() as cursor:
252 | sql = text("""
253 | select count(*) as total_num ,ua from {0}
254 | where MATCH(`ua`) AGAINST('spider')
255 | GROUP BY ua
256 | ORDER BY total_num desc
257 | """.format(current_app.db_engine_table)
258 | )
259 |
260 | res = cursor.execute(sql, {'today': cls.today})
261 | data = Func.fetchall(res)
262 | return ApiCorsResponse.response(data)
263 |
264 | @classmethod
265 | def get_device_type_by_ua(cls):
266 | with current_app.db.connect() as cursor:
267 | sql = text("""
268 | select count(DISTINCT ua) as pc_num, (
269 | select count(DISTINCT ua) as mobile_num
270 | from {0}
271 | where match(`ua`) AGAINST('mobile xfb' IN BOOLEAN MODE)
272 | ) as mobile_num
273 | from {1}
274 | where match(`ua`) AGAINST('+gecko -mobile' IN BOOLEAN MODE)
275 | """.format(current_app.db_engine_table,current_app.db_engine_table)
276 | )
277 |
278 | res = cursor.execute(sql, {'today': cls.today})
279 | data = Func.fetchall(res)
280 | return ApiCorsResponse.response(data)
281 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/webServer/api_request.http:
--------------------------------------------------------------------------------
1 | ### 获取今日ip
2 | GET http://127.0.0.1:5000/get_total_ip
3 | Accept: */*
4 | Cache-Control: no-cache
5 | Content-Type: application/x-www-form-urlencoded
6 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
7 | Content-Type: application/x-www-form-urlencoded
8 |
9 | ### 获取今日ip
10 | GET http://127.0.0.1:5000/get_total_pv
11 | Accept: */*
12 | Cache-Control: no-cache
13 | Content-Type: application/x-www-form-urlencoded
14 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
15 | Content-Type: application/x-www-form-urlencoded
16 |
17 |
18 | ### 获取每秒钟的请求数量(pv)
19 | GET http://127.0.0.1:5000/get_request_num_by_secends
20 | Accept: */*
21 | Cache-Control: no-cache
22 | Content-Type: application/x-www-form-urlencoded
23 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
24 | Content-Type: application/x-www-form-urlencoded
25 |
26 | ### 获取每分钟的请求数量(pv)
27 | GET http://127.0.0.1:5000/get_pv_num_by_minute
28 | Accept: */*
29 | Cache-Control: no-cache
30 | Content-Type: application/x-www-form-urlencoded
31 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
32 | Content-Type: application/x-www-form-urlencoded
33 |
34 | ### 获取每分钟的IP数量(ip)
35 | GET http://127.0.0.1:5000/get_ip_num_by_minute
36 | Accept: */*
37 | Cache-Control: no-cache
38 | Content-Type: application/x-www-form-urlencoded
39 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
40 | Content-Type: application/x-www-form-urlencoded
41 |
42 |
43 | ### 获取IP请求数量最多的前50
44 | GET http://127.0.0.1:5000/get_request_num_by_ip
45 | Accept: */*
46 | Cache-Control: no-cache
47 | Content-Type: application/x-www-form-urlencoded
48 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
49 | Content-Type: application/x-www-form-urlencoded
50 |
51 |
52 |
53 |
54 | ### 获取请求最多的 url
55 | GET http://127.0.0.1:5000/get_request_num_by_url?type=init
56 | Accept: */*
57 | Cache-Control: no-cache
58 | Content-Type: application/x-www-form-urlencoded
59 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
60 | Content-Type: application/x-www-form-urlencoded
61 |
62 |
63 |
64 | ### 获取某个请求状态码的所有url
65 | GET http://127.0.0.1:5000/get_request_num_by_status_code?code=502
66 | Accept: */*
67 | Cache-Control: no-cache
68 | Content-Type: application/x-www-form-urlencoded
69 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
70 | Content-Type: application/x-www-form-urlencoded
71 |
72 | ### 获取所有请求状态码
73 | GET http://127.0.0.1:5000/get_request_num_by_status
74 | Accept: */*
75 | Cache-Control: no-cache
76 | Content-Type: application/x-www-form-urlencoded
77 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
78 | Content-Type: application/x-www-form-urlencoded
79 |
80 | ### 获取所有中国城市请求分布
81 | GET http://127.0.0.1:5000/get_request_num_by_province
82 | Accept: */*
83 | Cache-Control: no-cache
84 | Content-Type: application/x-www-form-urlencoded
85 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
86 | Content-Type: application/x-www-form-urlencoded
87 |
88 |
89 | ### 获取每秒的请求量
90 | GET http://127.0.0.1:5000/get_request_num_by_secends
91 | Accept: */*
92 | Cache-Control: no-cache
93 | Content-Type: application/x-www-form-urlencoded
94 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
95 | Content-Type: application/x-www-form-urlencoded
96 |
97 | ### 获取前50的请求量最高的ip
98 | GET http://127.0.0.1:5000/get_request_num_by_ip
99 | Accept: */*
100 | Cache-Control: no-cache
101 | Content-Type: application/x-www-form-urlencoded
102 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
103 | Content-Type: application/x-www-form-urlencoded
104 |
105 |
106 | ### 获取某个ip 请求的urls
107 | GET http://127.0.0.1:5000/get_request_urls_by_ip?ip=47.112.167.32
108 | Accept: */*
109 | Cache-Control: no-cache
110 | Content-Type: application/x-www-form-urlencoded
111 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
112 | Content-Type: application/x-www-form-urlencoded
113 |
114 | ### 从ua 获取爬虫信息
115 | GET http://127.0.0.1:5000/get_spider_by_ua
116 | Accept: */*
117 | Cache-Control: no-cache
118 | Content-Type: application/x-www-form-urlencoded
119 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
120 | Content-Type: application/x-www-form-urlencoded
121 |
122 | ### 从ua 获取设备类型 pc端 和 移动端 对比曲线
123 | GET http://127.0.0.1:5000/get_device_type_by_ua
124 | Accept: */*
125 | Cache-Control: no-cache
126 | Content-Type: application/x-www-form-urlencoded
127 | Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJhcGkubS5jb20iLCJpYXQiOjE1OTM0MzkwNzUsInN1YiI6Img1IiwiYXVkIjoiTW96aWxsYVwvNS4wIChMaW51eDsgQW5kcm9pZCA4LjAuMDsgUGl4ZWwgMiBYTCBCdWlsZFwvT1BEMS4xNzA4MTYuMDA0KSBBcHBsZVdlYktpdFwvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lXC84My4wLjQxMDMuMTE2IE1vYmlsZSBTYWZhcmlcLzUzNy4zNiIsImV4cCI6MTYyNDk3NTA3NSwidWlkIjoiNWVmOWYzNjNlYzI3MDAwMDkwMDAzYWIyIiwiYWNjb3VudCI6IjE1OTI2OTAwNjUzIn0.aPgUqu0jnsXVcQvD03xobxk446qBQ58rWX0RwXIGKjVCqPMtTjMhrM1q5OY9VPUh_Ue6HG_wxLh4Gq3zGBcmi4xywq8PrHVJZkvH4Yv0Pvw4_yo1MpTkLGrXEpye1TBNe6W13bh1cjPKGuCvk0hH_3NoqtcbN5ayuJOj0eSZNzo
128 | Content-Type: application/x-www-form-urlencoded
129 |
130 | ###
131 |
--------------------------------------------------------------------------------
/ParserAdapter/Nginx.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | from ParserAdapter.BaseAdapter import Adapter,ParseError,ReCompile
3 | import os,re,shutil,time
4 |
5 | """
6 | $remote_addr,$http_x_forwarded_for #记录客户端IP地址
7 | $remote_user #记录客户端用户名称
8 |
9 | $request #记录请求的方法 URL和HTTP协议 (包含 $request_method $request_method http协议 比如: 'GET /api/server/?size=10&page=1 HTTP/1.1')
10 | $request_method 请求方法
11 | $request_uri 请求的URL
12 | $request_length #请求的长度(包括请求行,请求头和请求正文)。
13 | $request_time #请求处理时间,单位为秒,精度毫秒;从读入客户端的第一个字节开始,直到把最后一个字符发送给客户端后进行日志写入位置。
14 | $upstream_response_time 从Nginx向后端(php-cgi)建立连接开始到接受完数据然后关闭连接为止的时间 ($request_time 包括 $upstream_response_time) 所以如果使用nginx的accesslog查看php程序中哪些接口比较慢的话,记得在log_format中加入$upstream_response_time。
15 |
16 | $status #记录请求状态码
17 | $body_bytes_sent #发送给客户端的字节数,不包括响应头的大小;该变量与Apache模块mod_log_config李的“%B”参数兼容
18 | $bytes_sent #发送给客户端的总字节数
19 | $connection #连接到序列号
20 | $connection_requests #当前通过一个链接获得的请求数量
21 |
22 | $msec #日志写入时间,单位为秒精度是毫秒。
23 | $pipe #如果请求是通过HTTP流水线(pipelined)发送,pipe值为“p”,否则为".".
24 |
25 | $http_referer #记录从那个页面链接访问过来的
26 | $http_user_agent #记录客户端浏览器相关信息
27 |
28 | $time_iso8601 ISO8601标准格式下的本地时间 2020-09-11T15:01:38+08:00
29 | $time_local #通用日志格式下的本地时间 11/Sep/2020:15:01:38 +0800
30 |
31 | $upstream_addr 服务端响应的地址
32 | $upstream_http_host 服务端响应的地址
33 |
34 |
35 | """
36 |
37 |
38 |
39 | class Handler(Adapter):
40 |
41 | def __init__(self,*args ,**kwargs):
42 | super(Handler,self).__init__(*args ,**kwargs)
43 |
44 |
45 | """
46 | 日志变量 dict map:
47 | nickname 对该变量名称取别名 ; 默认直接使用变量名称
48 | re 指定该变量匹配的正则 ; 默认 [\s|\S]+?
49 |
50 | mysql_field_type 指定该变量值mysql中的 字段类型 【当存储引擎为 mysql 的时候才需配置】
51 | mysql_key_field True (bool) 默认普通索引 ,指定索引 用字符串 UNIQUE ,FULLTEXT (区分大小写) 【当存储引擎为 mysql 的时候才需配置】
52 | extend_field 拓展字段 【拓展字段不支持nickname】
53 | 列表的时候
54 | '$remote_addr': {
55 | 'nickname':'ip' ,
56 | 'mysql_field_type':'varchar(15)', #【当存储引擎为 mysql 的时候才需配置】
57 | 'mysql_key_field': [
58 | '$time_local.timestamp', # 表示当前字段 和 $time_local 里 extend timestamp 联合索引 key ip_timestamp (ip,timestamp)
59 | '$time_iso8601.timestamp', # 表示当前字段 和 $time_iso8601 里 extend timestamp 联合索引 key ip_timestamp (ip,timestamp)
60 | ['$status','$request.request_url','$request.method'] # 表示当前字段 和 $time_iso8601 里 extend timestamp 联合索引 key ip_status_request_url_method (ip,status,request,url,method)
61 | ],
62 | 'extend_field':{ # 拓展字段 由 parse_ip_to_area方法 拓展出来的
63 | 'isp':{ # 字段名称
64 | 'mysql_field_type': 'varchar(30)', # 字段类型及长度 【当存储引擎为 mysql 的时候才需配置】
65 | },
66 | 'city':{ # 字段名称
67 | 'mysql_field_type': 'varchar(30)', # 字段类型及长度 【当存储引擎为 mysql 的时候才需配置】
68 | 'mysql_key_field': ['$status'],
69 | },
70 | 'city_id':{
71 | 'mysql_field_type': 'int(10)',
72 | },
73 | 'province':{
74 | 'mysql_field_type': 'varchar(30)',
75 | 'mysql_key_field': [
76 | '$time_local.timestamp',
77 | '$time_iso8601.timestamp',
78 | ],
79 | },
80 | 'country':{
81 | 'mysql_field_type': 'varchar(30)',
82 | }
83 | }
84 | }
85 |
86 | """
87 | def getLogFormat(self):
88 |
89 | return {
90 | ### 非 nginx 日志变量 附加自定义字段
91 | '$node_id':{
92 | 'mysql_field_type': 'varchar(255)',
93 | 'mysql_key_field' : True
94 | },
95 | '$app_name':{
96 | 'mysql_field_type': 'varchar(255)',
97 | },
98 | ### 非 nginx 日志变量 附加自定义字段
99 |
100 |
101 | # 客户端IP example 127.0.0.1
102 | '$remote_addr': {
103 | 'nickname':'ip' ,
104 | 'mysql_field_type':'varchar(15)',
105 | 'mysql_key_field': [
106 | '$time_local.timestamp',
107 | '$time_iso8601.timestamp',
108 | ['$status','$request.request_url','$request.request_method']
109 | ],
110 | 'extend_field':{
111 | 'isp':{
112 | 'mysql_field_type': 'varchar(30)',
113 | },
114 | 'city':{
115 | 'mysql_field_type': 'varchar(30)',
116 | 'mysql_key_field': ['$status'],
117 | },
118 | 'city_id':{
119 | 'mysql_field_type': 'int(10)',
120 | },
121 | 'province':{
122 | 'mysql_field_type': 'varchar(30)',
123 | 'mysql_key_field': [
124 | '$time_local.timestamp',
125 | '$time_iso8601.timestamp',
126 | ],
127 | },
128 | 'country':{
129 | 'mysql_field_type': 'varchar(30)',
130 | }
131 | }
132 | },
133 | # 请求信息 example GET /api/server/?size=50&page=1 HTTP/1.1
134 | '$request': {
135 | 'extend_field': {
136 | 'request_method': {
137 | 'mysql_field_type': 'varchar(10)',
138 | 'mysql_key_field':True,
139 | },
140 | 'request_url': {
141 | 'mysql_field_type': 'varchar(255)',
142 | 'mysql_key_field': ['$time_local.timestamp', '$time_iso8601.timestamp'],
143 | },
144 | 'args': {
145 | 'mysql_field_type': 'text',
146 | },
147 | 'server_protocol': {
148 | 'mysql_field_type': 'varchar(10)',
149 | },
150 | }
151 | },
152 | # 记录客户端用户名称 example client_name
153 | '$remote_user': { },
154 | # 客户端代理IP多个逗号分割 example 203.98.182.163, 203.98.182.169
155 | '$http_x_forwarded_for': {
156 | 'nickname':'proxy_ip',
157 | 'mysql_field_type':'varchar(255)'
158 | },
159 | # 请求方法 example GET
160 | '$request_method': {
161 | 'mysql_field_type': 'varchar(100)',
162 | },
163 | # 请求协议 example HTTP/1.1
164 | '$scheme':{
165 | 'mysql_field_type': 'varchar(255)'
166 | } ,
167 | # 服务器的HTTP版本 example “HTTP/1.0” 或 “HTTP/1.1”
168 | '$server_protocol':{
169 | 'mysql_field_type': 'varchar(10)'
170 | } ,
171 | # 请求链接 example /api/server/?size=50&page=1
172 | '$request_uri': {
173 | 'nickname':'request_url',
174 | 'mysql_field_type': 'varchar(255)',
175 | 'mysql_key_field': ['$time_local.timestamp', '$time_iso8601.timestamp'],
176 | },
177 | # post提交的数据 example name=xxx&age=18
178 | '$request_body': {
179 | 'mysql_field_type': 'mediumtext'
180 | },
181 | # 请求的字节长度 example 988
182 | '$request_length': {
183 | 're': '\d+?' ,
184 | 'type':'int',
185 |
186 | },
187 | # 请求花费的时间 example 0.018
188 | '$request_time': {
189 | 'type':'float',
190 | 'mysql_field_type':'float(10, 3)',
191 | },
192 | # 当前的Unix时间戳 nginx 版本需大于 (1.3.9, 1.2.6)
193 | '$msec': {
194 | 'mysql_field_type': 'int(16)',
195 | },
196 | # nginx交给后端cgi响应的时间(小于$request_time) example 0.215 或者 0.215, 0.838
197 | '$upstream_response_time': {
198 | 'mysql_field_type': 'varchar(50)',
199 | },
200 | # 请求状态码 example 200
201 | '$status': {
202 | 're': '\d*?',
203 | 'nickname':'status_code',
204 | 'mysql_field_type': 'int(4)',
205 | 'mysql_key_field': ['$time_local.timestamp','$time_iso8601.timestamp'],
206 | },
207 | # 发送给客户端的总字节数 (包括响应头) example 113
208 | '$bytes_sent':{
209 | 're': '\d*?' ,
210 | 'type':'int',
211 | 'mysql_field_type': 'int(4)',
212 | } ,
213 | # 发送给客户端的总字节数(不包括响应头) example 266
214 | '$body_bytes_sent':{
215 | 're': '\d*?' ,
216 | 'type':'int',
217 | 'mysql_field_type': 'int(4)',
218 | } ,
219 | # 连接到序列号 example 26
220 | '$connection':{
221 | 're': '\d*?' ,
222 | 'type':'int',
223 | 'mysql_field_type': 'int(10)',
224 | } ,
225 | # 每个连接到序列号的请求次数 example 3
226 | '$connection_requests':{
227 | 're': '\d*?',
228 | 'type':'int',
229 | 'mysql_field_type': 'int(10)',
230 | } ,
231 | # 请求头里的host属性,如果没有就返回 server_name example www.baidu.com
232 | '$host':{
233 | 'mysql_field_type': 'varchar(255)',
234 | } ,
235 | # 请求头里的host属性 example www.baidu.com
236 | '$http_host':{
237 | 'mysql_field_type': 'varchar(255)',
238 | } ,
239 | # 请求的来源网址 example www.baidu.com
240 | '$http_referer':{
241 | 'mysql_field_type': 'varchar(3000)',
242 |
243 | } ,
244 | # 客户端的UA信息 example Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
245 | '$http_user_agent':{
246 | 'nickname':'ua',
247 | 'mysql_field_type': 'mediumtext',
248 | 'mysql_key_field': 'FULLTEXT'
249 |
250 | } ,
251 | # 后端接收请求的服务的ip或地址 example unix:/tmp/php-cgi-71.sock 负载均衡中 是 192.168.0.122:80, 192.168.0.133:80
252 | '$upstream_addr':{
253 | 'mysql_field_type': 'varchar(255)',
254 | } ,
255 | # 服务端响应的地址 unix:/tmp/php-cgi-71.sock 同上 是域名
256 | '$upstream_http_host':{
257 | 'mysql_field_type': 'varchar(255)',
258 | } ,
259 | # iso8601时间格式 example 2020-09-11T15:20:43+08:00
260 | '$time_iso8601':{
261 | 'extend_field': {
262 | 'time_str': {
263 | 'mysql_field_type': 'datetime',
264 | 'mysql_key_field': True
265 | },
266 | 'timestamp': {
267 | 'mysql_field_type': 'int(10)',
268 | 'mysql_key_field': True
269 | },
270 | }
271 | } ,
272 | # 本地时间格式 example 11/Sep/2020:15:20:43 +0800
273 | '$time_local':{
274 | 'extend_field': {
275 | 'time_str': {
276 | 'mysql_field_type': 'datetime',
277 | 'mysql_key_field': True
278 | },
279 | 'timestamp': {
280 | 'mysql_field_type': 'int(10)',
281 | 'mysql_key_field': True
282 | },
283 | }
284 | } ,
285 | }
286 |
287 | # 日志解析
288 | def parse(self,log_format_name='',log_line=''):
289 |
290 |
291 | log_format_list = self.log_line_pattern_dict[log_format_name]['log_format_list']
292 | log_format_recompile = self.log_line_pattern_dict[log_format_name]['log_format_recompile']
293 |
294 | start_time = time.perf_counter()
295 |
296 | res = log_format_recompile.match(log_line)
297 |
298 |
299 | if res == None:
300 | raise ParseError('解析日志失败,请检查client 配置中 日志的 格式名称是否一致 log_format_name')
301 |
302 | matched = list(res.groups())
303 |
304 | if len(matched) == len(log_format_list):
305 | data = {}
306 | del_key_name = []
307 |
308 | for i in range(len(list(log_format_list))):
309 | key_name = log_format_list[i].replace('$','')
310 |
311 | # request 参数不支持别名
312 | if 'nickname' in self.getLogFormat()[ log_format_list[i] ] :
313 | key_name = self.getLogFormat()[ log_format_list[i] ]['nickname']
314 |
315 |
316 | # 解析 ip 对应的 地理位置信息
317 | if log_format_list[i] == '$remote_addr':
318 | ip_data = self.parse_ip_to_area(matched[i])
319 | data.update(ip_data)
320 |
321 |
322 | # 解析 $request 成 request_method ,request_url ,args ,server_protocol ,
323 | if log_format_list[i] == '$request':
324 | request_extend_data = self.parse_request_to_extend(matched[i])
325 | data.update(request_extend_data)
326 | del_key_name.append(key_name)
327 |
328 | # 解析 time_iso8601 成 timestr , timestamp
329 | if log_format_list[i] == '$time_local':
330 | time_data = self.parse_time_to_str(log_format_list[i].replace('$',''),matched[i])
331 | data.update(time_data)
332 | del_key_name.append(key_name)
333 |
334 |
335 | data[key_name] = matched[i]
336 |
337 |
338 |
339 | # 剔除掉 解析出拓展字符串的 字段
340 | for i in del_key_name:
341 | del data[i]
342 |
343 |
344 | return data
345 |
346 |
347 | # 根据录入的格式化字符串 返回 parse 所需 log_format 配置以及进行对应的表达式预编译
348 | def getLogFormatByConfStr(self ,log_format_str,log_format_vars,log_format_name ,log_type):
349 | if log_type not in ['string','json']:
350 | raise ValueError('_type 参数类型错误')
351 |
352 | # 日志格式不存在 则 预编译
353 | if log_format_name not in self.log_line_pattern_dict:
354 |
355 | # 过滤下 日志配置字符串 与 正则冲突特殊字符
356 | log_format_str = log_format_str.strip()\
357 | .replace('[','\[').replace(']','\]')\
358 | .replace('(','\(').replace(')','\)')\
359 | .replace('|','\|').replace('-','\-')\
360 | .replace('+','\+').replace('*','\*')\
361 | .replace('?','\?')
362 |
363 |
364 | if (log_type == 'string'):
365 |
366 | # 获取到匹配到的 日志格式
367 |
368 | log_format_list = log_format_vars.split(self.LOG_FORMAT_SPLIT_TAG)
369 |
370 | re_str = re.sub(r'(\$\w+)+', self.__replaceLogVars, log_format_str).strip()
371 |
372 | try:
373 | re_compile = re.compile(re_str, re.I)
374 |
375 | except re.error:
376 | raise ReCompile('预编译错误,请检查日志字符串中是否包含特殊正则字符; 日志:%s' % log_format_str)
377 |
378 | self.log_line_pattern_dict[log_format_name] = {
379 | 'log_format_list':log_format_list ,
380 | 'log_format_recompile':re_compile
381 | }
382 |
383 | # 找到匹配中的日志变量替换成正则表达式
384 | def __replaceLogVars(self,matched):
385 |
386 |
387 | s = matched.group()
388 |
389 | if s not in self.getLogFormat():
390 | raise ValueError('handle 里面不存在日志变量:%s' % s)
391 |
392 | if 're' in self.getLogFormat()[s]:
393 | re_str = self.getLogFormat()[s]['re']
394 | else:
395 | re_str = '[\s|\S]*?'
396 |
397 | return '(%s)' % re_str
398 |
399 | # 获取服务器配置文件中所有的日志配置
400 | def getLoggerFormatByServerConf(self,server_conf_path):
401 |
402 | # 根据配置文件 自动获取 log_format 字符串
403 | with open(server_conf_path,'rb') as fd:
404 | content = fd.read().decode(encoding="utf-8")
405 |
406 | defualt_log_vars = self.LOG_FORMAT_SPLIT_TAG.join(
407 | ['$remote_addr',
408 | '$remote_user',
409 | '$time_local',
410 | '$request',
411 | '$status',
412 | '$body_bytes_sent',
413 | '$http_referer',
414 | '$http_user_agent'
415 | ]
416 | )
417 | format_list = {}
418 | format_list['defualt'] = {
419 | 'log_format_str':'$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"',
420 | 'log_format_vars' : defualt_log_vars
421 | }
422 |
423 |
424 | res = re.findall(r'log_format\s+(\w+)\s+\'([\s\S]*?\S?)\'\S?\;' ,content)
425 |
426 | if len(res) == 0:
427 | return format_list
428 |
429 |
430 | for i in res:
431 | log_str = i[1].strip()
432 | log_vars = re.findall(r'(\$\w+)+',log_str)
433 | if(len(log_vars)):
434 | format_list[i[0]] = {
435 | 'log_format_str':log_str,
436 | 'log_format_vars':self.LOG_FORMAT_SPLIT_TAG.join(log_vars)
437 | }
438 |
439 | del content
440 |
441 | return format_list
442 |
443 |
444 | # 切割日志
445 | def rotatelog(self,server_conf,log_path ,target_file = None ):
446 | try:
447 |
448 | if not os.path.exists(server_conf['pid_path']):
449 | raise FileNotFoundError(server_conf['pid_path'] + '配置项 server nginx_pid_path 不存在')
450 |
451 | if not os.path.exists(log_path):
452 | raise FileNotFoundError(log_path + ' 不存在')
453 |
454 |
455 | # 这里需要注意 日志目录的 权限 是否有www 否则会导致 ngixn 重开日志问件 无法写入的问题
456 | cmd = 'kill -USR1 `cat %s`' % (server_conf['pid_path'])
457 | shutil.move(log_path, target_file)
458 |
459 | res = os.popen(cmd)
460 | if len(res.readlines()) > 0:
461 | cmd_res = ''
462 | for i in res.readlines():
463 | cmd_res += i + '\n'
464 | raise Exception ('reload 服务器进程失败: %s' % cmd_res)
465 |
466 | return True
467 | except Exception as e:
468 | return '切割日志失败 : %s ; error class : %s error info : %s' % (target_file ,e.__class__, e.args)
469 |
470 |
471 |
472 |
--------------------------------------------------------------------------------
/Src/Core.py:
--------------------------------------------------------------------------------
1 | # coding=UTF-8
2 | from ParserAdapter.BaseAdapter import ParseError,ReCompile
3 | from configparser import ConfigParser
4 | from threading import RLock
5 | from collections import deque
6 | import time,json,os,platform,importlib,logging
7 |
8 |
9 | try:
10 | # Python 3.x
11 | from urllib.parse import quote_plus
12 | except ImportError:
13 | # Python 2.x
14 | from urllib import quote_plus
15 |
16 |
17 |
18 | # 日志解析
19 | class loggerParse(object):
20 |
21 |
22 | def __init__(self ,server_type,server_conf = None):
23 |
24 | self.server_type = server_type
25 | self.__handler = Base.findAdapterHandler('server',server_type)()
26 | self.format = self.__handler.getLogFormat()
27 | if server_conf:
28 | self.logger_format = self.__handler.getLoggerFormatByServerConf(server_conf_path=server_conf)
29 |
30 |
31 | def getLogFormatByConfStr(self,log_format_str,log_format_vars,log_format_name):
32 |
33 | return self.__handler.getLogFormatByConfStr( log_format_str,log_format_vars,log_format_name,'string')
34 |
35 |
36 | def parse(self,log_format_name,log_line):
37 | return self.__handler.parse(log_format_name=log_format_name,log_line=log_line)
38 |
39 |
40 |
41 | class Base(object):
42 | conf = None
43 | CONFIG_FIEL_SUFFIX = '.ini'
44 | config_name = 'config'
45 |
46 | def __init__(self,config_name = None):
47 | self._root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
48 | self.conf = self.__getConfig(config_name)
49 | self.config_name = config_name
50 | self.__initLogging()
51 |
52 |
53 | def __initLogging(self):
54 | LOG_FORMAT = "%(asctime)s %(levelname)s %(pathname)s %(lineno)s %(message)s "
55 | DATE_FORMAT = '%Y-%m-%d %H:%M:%S '
56 | if self.__class__.__name__ == 'Base':return
57 |
58 | logg_setting_map = {
59 | 'Reader': 'inputer',
60 | 'OutputCustomer': 'outputer',
61 | }
62 |
63 | conf_name = logg_setting_map[self.__class__.__name__]
64 |
65 | if 'log_debug' in self.conf[conf_name] and self.conf[conf_name]['log_debug'] == 'True':
66 | _level = logging.DEBUG
67 | else:
68 | _level = logging.INFO
69 |
70 | logging.basicConfig(level=_level, format=LOG_FORMAT, datefmt=DATE_FORMAT,
71 | filename=r"./%s_%s.log" % (conf_name ,self.config_name) )
72 |
73 | self.logging = logging
74 |
75 | def __getConfig(self,config_name):
76 | if config_name:
77 | self.config_name = config_name
78 |
79 | if self.config_name.find(self.CONFIG_FIEL_SUFFIX) == -1:
80 | config_path = self._root + '/' + self.config_name + self.CONFIG_FIEL_SUFFIX
81 | else:
82 | config_path = self._root + '/' + self.config_name
83 |
84 | if ( not os.path.exists(config_path) ):
85 | raise FileNotFoundError('config file: %s not found ' % (config_path) )
86 |
87 | conf = ConfigParser()
88 | conf.read(config_path, encoding="utf-8")
89 |
90 | return conf
91 |
92 | @classmethod
93 | def findAdapterHandler(cls,adapter_type,name):
94 |
95 | if adapter_type not in ['server', 'queue', 'storage','traffic_analysis']:
96 | raise ValueError('%s Adapter 类型不存在' % adapter_type)
97 |
98 | if adapter_type in ['queue', 'storage']:
99 | handler_module = '%sAdapter.%s' % (adapter_type.lower().capitalize(), name.lower().capitalize())
100 | print(handler_module)
101 | elif adapter_type == 'traffic_analysis':
102 | handler_module = '%sAdapter.%s' % (adapter_type.lower().capitalize(), name.lower().capitalize())
103 | print(handler_module)
104 | exit()
105 | else:
106 | handler_module = 'ParserAdapter.%s' % name.lower().capitalize()
107 |
108 | if adapter_type == 'queue':
109 | return importlib.import_module(handler_module).QueueAp
110 | if adapter_type == 'storage':
111 | return importlib.import_module(handler_module).StorageAp
112 | if adapter_type == 'traffic_analysis':
113 | return importlib.import_module(handler_module).TrafficAnalysisAp
114 | if adapter_type == 'server':
115 | return importlib.import_module(handler_module).Handler
116 |
117 |
118 | def runMethod(self,method_name):
119 | self.logging.debug('pid:%s , %s ,%s' % (os.getpid() ,method_name ,time.perf_counter()))
120 | getattr(self,method_name)()
121 |
122 |
123 | # 生产者 实时读取日志 && 切割日志 && 上报服务器状况
124 | class Reader(Base):
125 |
126 | event = {
127 | 'cut_file' : 0,
128 | 'stop' : None,
129 | }
130 |
131 | def __init__(self,log_file_conf = None ,config_name = None):
132 | super(Reader, self).__init__(config_name=config_name)
133 |
134 | self.log_path = log_file_conf['file_path']
135 |
136 |
137 | if 'log_format_name' not in log_file_conf or len(log_file_conf['log_format_name']) == 0:
138 | self.log_format_name = 'defualt'
139 | else:
140 | self.log_format_name = log_file_conf['log_format_name']
141 |
142 |
143 | self.node_id = self.conf['inputer']['node_id']
144 |
145 | # 最大写入队列的数据量
146 | if 'max_batch_push_queue_size' in self.conf['inputer']:
147 | self.max_batch_push_queue_size = int(self.conf['inputer']['max_batch_push_queue_size'])
148 | else:
149 | self.max_batch_push_queue_size = 5000
150 |
151 | # 最大重试打开文件次数
152 | if 'max_retry_open_file_time' in self.conf['inputer']:
153 | self.max_retry_open_file_time = int(self.conf['inputer']['max_retry_open_file_time'])
154 | else:
155 | self.max_retry_open_file_time = 10
156 |
157 | # 最大重试链接 queue的次数
158 | if 'max_retry_reconnect_time' in self.conf['inputer']:
159 | self.max_retry_reconnect_time = int(self.conf['inputer']['max_retry_reconnect_time'])
160 | else:
161 | self.max_retry_reconnect_time = 20
162 |
163 |
164 | self.app_name = log_file_conf['app_name']
165 | self.server_type = log_file_conf['server_type']
166 | self.read_type = log_file_conf['read_type']
167 | self.cut_file_type = log_file_conf['cut_file_type']
168 | self.cut_file_point = log_file_conf['cut_file_point']
169 | try:
170 | self.cut_file_save_dir = log_file_conf['cut_file_save_dir']
171 | except KeyError as e:
172 | self.cut_file_save_dir = None
173 |
174 |
175 | log_prev_path = os.path.dirname(log_file_conf['file_path'])
176 |
177 | if platform.system() == 'Linux':
178 | self.newline_char = '\n'
179 | import pwd
180 | """
181 | 这里需要将 nginx 日志的所属 目录修改为 www 否则在切割日志的时候 kill -USR1 pid 之后 日志文件会被重新打开但是因权限问题不会继续写入文件中
182 | """
183 | # 检查日志目录所属用户 ; 不是 www 则修改成 www
184 | if pwd.getpwuid(os.stat(log_prev_path).st_uid).pw_name != 'www' and platform.system() == 'Linux':
185 | try:
186 | www_uid = pwd.getpwnam('www').pw_uid
187 | os.chown(log_prev_path, www_uid, www_uid)
188 | except PermissionError as e:
189 | exit('权限不足 : 修改目录: %s 所属用户和用户组 为 www 失败 ' % (log_prev_path))
190 |
191 |
192 | elif platform.system() == 'Windows':
193 | self.newline_char = '\r\n'
194 |
195 | # 内部队列
196 | self.dqueue = deque()
197 |
198 | self.queue_key = self.conf['inputer']['queue_name']
199 |
200 | self.server_conf = loggerParse(log_file_conf['server_type'],self.conf[log_file_conf['server_type']]['server_conf']).logger_format
201 |
202 | self.fd = self.__getFileFd()
203 |
204 | # 文件切割中标志
205 | self.cutting_file = False
206 | self.lock = RLock()
207 |
208 | # 外部队列handle
209 | self.queue_handle = self.findAdapterHandler('queue',self.conf['inputer']['queue']).initQueue(self)
210 | self.server_handle = self.findAdapterHandler('server',log_file_conf['server_type'])()
211 |
212 |
213 | def __getFileFd(self):
214 | try:
215 | return open(self.log_path, mode='r+' ,newline=self.newline_char)
216 |
217 | except PermissionError as e:
218 | self.event['stop'] = self.log_path + '没有权限读取文件 请尝试sudo'
219 | return False
220 |
221 | except OSError as e:
222 | self.event['stop'] = self.log_path + ' 文件不存在'
223 | return False
224 |
225 |
226 | def __cutFileHandle(self,server_conf,log_path ,target_path = None ):
227 |
228 | start_time = time.perf_counter()
229 | self.logging.debug("\n start_time -------cutting file start --- %s \n" % (
230 | start_time))
231 |
232 | file_suffix = time.strftime('%Y-%m-%d_%H-%M-%S', time.localtime())
233 |
234 | files_arr = log_path.split('/')
235 | log_name = files_arr.pop().replace('.log', '')
236 | log_path_dir = '/'.join(files_arr)
237 |
238 |
239 | if target_path :
240 | target_dir = target_path + '/' + log_name
241 | else:
242 | target_dir = log_path_dir + '/' + log_name
243 |
244 | if not os.path.exists(target_dir):
245 | try:
246 | os.makedirs(target_dir)
247 | except Exception as e:
248 | self.event['stop'] = '日志切割存储目录创建失败'
249 | return
250 |
251 | target_file = target_dir + '/' + log_name + '_' + file_suffix
252 |
253 | res = self.server_handle.rotatelog(server_conf,log_path,target_file)
254 | if(isinstance(res,str) and res != True):
255 | self.event['stop'] = res
256 |
257 | end_time = time.perf_counter()
258 | self.logging.debug('finnish file_cut take time : %s' % round((start_time - end_time)))
259 |
260 |
261 | def cutFile(self):
262 |
263 | while True:
264 | time.sleep(1)
265 | if self.event['stop']:
266 | self.logging.debug( '%s ; cutFile threading stop pid: %s' % (self.event['stop'] , os.getpid()))
267 | return
268 |
269 |
270 | if self.cut_file_type not in ['filesize', 'time']:
271 | self.event['stop'] = 'cut_file_type 只支持 filesize 文件大小 或者 time 指定每天的时间'
272 | continue
273 |
274 |
275 | self.lock.acquire()
276 |
277 | if self.cut_file_type == 'filesize' :
278 |
279 | try:
280 | now = time.strftime("%H:%M", time.localtime(time.time()))
281 | # self.logging.debug('cut_file_type: filesize ;%s ---pid: %s----thread_id: %s--- %s ---------%s' % ( now, os.getpid(), threading.get_ident(), self.cut_file_point, self.cutting_file))
282 |
283 | # 文件大小 单位 M
284 | file_size = round(os.path.getsize(self.log_path) / (1024 * 1024))
285 | if file_size < int(self.cut_file_point):
286 | self.lock.release()
287 | continue
288 |
289 | except FileNotFoundError as e:
290 | self.event['stop'] = self.log_path + '文件不存在'
291 | self.lock.release()
292 | continue
293 |
294 | self.__cutFileHandle(
295 | server_conf = dict(self.conf[self.server_type]) ,
296 | log_path= self.log_path ,
297 | target_path = self.cut_file_save_dir
298 | )
299 |
300 | self.event['cut_file'] = 1
301 |
302 | elif self.cut_file_type == 'time':
303 |
304 | now = time.strftime("%H:%M" , time.localtime(time.time()) )
305 | # self.logging.debug('cut_file_type: time ;%s ---pid: %s----thread_id: %s--- %s ---------%s' % (
306 | # now,os.getpid(), threading.get_ident(), self.cut_file_point, self.cutting_file))
307 |
308 | if now == self.cut_file_point and self.cutting_file == False:
309 | self.__cutFileHandle(
310 | server_conf = dict(self.conf[self.server_type]) ,
311 | log_path = self.log_path ,
312 | target_path = self.cut_file_save_dir
313 | )
314 |
315 | self.cutting_file = True
316 | self.event['cut_file'] = 1
317 | elif now == self.cut_file_point and self.cutting_file == True and self.event['cut_file'] == 1:
318 | self.event['cut_file'] == 0
319 |
320 |
321 | elif now != self.cut_file_point:
322 | self.cutting_file = False
323 | self.event['cut_file'] = 0
324 |
325 |
326 | self.lock.release()
327 |
328 |
329 | def pushDataToQueue(self):
330 |
331 | self.queue_handle.pushDataToQueue()
332 |
333 |
334 | def readLog(self):
335 |
336 |
337 | position = 0
338 |
339 | if self.read_type not in ['head','tail']:
340 | self.event['stop'] = 'read_type 只支持 head 从头开始 或者 tail 从末尾开始'
341 |
342 | if self.fd == False:
343 | return
344 |
345 | try:
346 | if self.read_type == 'head':
347 | self.fd.seek(position, 0)
348 | elif self.read_type == 'tail':
349 | self.fd.seek(position, 2)
350 | except Exception as e:
351 | self.event['stop'] = self.log_path + ' 文件句柄 seek 错误 %s ; %s' % (e.__class__ , e.args)
352 |
353 |
354 | max_retry_open_file_time = 3
355 | retry_open_file_time = 0
356 | while True:
357 | time.sleep(1)
358 |
359 | if self.event['stop']:
360 | self.logging.debug( '%s ; read threading stop pid: %s' % (self.event['stop'] ,os.getpid()))
361 | return
362 |
363 | start_time = time.perf_counter()
364 | # self.logging.debug("\n start_time -------pid: %s -- read file---queue len: %s---- %s \n" % ( os.getpid(), len(list(self.dqueue)), round(start_time, 2)))
365 |
366 |
367 | for line in self.fd:
368 | # 不是完整的一行继续read
369 | if line.find(self.newline_char) == -1:
370 | continue
371 |
372 | self.dqueue.append(line)
373 |
374 |
375 | end_time = time.perf_counter()
376 | self.logging.debug("\n end_time -------pid: %s -- read file- %s--line len :%s --- 耗时:%s \n" % (os.getpid(),self.log_path, len(list(self.dqueue)), round(end_time - start_time, 2)))
377 |
378 | if self.event['cut_file'] == 1 and self.event['stop'] == None:
379 | # 防止 重启进程服务后 新的日志文件并没有那么快重新打开
380 | time.sleep(1.5)
381 | self.logging.debug('--------------------reopen file--------------------at: %s' % time.time())
382 |
383 | self.fd.close()
384 | self.fd = self.__getFileFd()
385 | try:
386 | self.fd.seek(0)
387 | except AttributeError as e:
388 | time.sleep(1)
389 | retry_open_file_time = retry_open_file_time + 1
390 | if retry_open_file_time >= max_retry_open_file_time:
391 | self.event['stop'] = '重新打开文件超过最大次数 %s ' % max_retry_open_file_time
392 | continue
393 |
394 | self.event['cut_file'] = 0
395 |
396 |
397 |
398 | # 消费者 解析日志 && 存储日志
399 | class OutputCustomer(Base):
400 |
401 | def __init__(self , config_name = None):
402 |
403 | super(OutputCustomer,self).__init__(config_name)
404 |
405 | self.dqueue = deque()
406 |
407 | self.inputer_queue_type = self.conf['outputer']['queue']
408 | self.queue_key = self.conf['outputer']['queue_name']
409 |
410 |
411 | self.save_engine_conf = dict( self.conf[ self.conf['outputer']['save_engine'] ])
412 | self.save_engine_name = self.conf['outputer']['save_engine'].lower().capitalize()
413 |
414 |
415 | self.server_type = self.conf['outputer']['server_type']
416 | self.logParse = loggerParse(self.conf['outputer']['server_type'] ,server_conf=None)
417 |
418 | if 'max_batch_insert_db_size' in self.conf['outputer']:
419 | self.max_batch_insert_db_size = int(self.conf['outputer']['max_batch_insert_db_size'])
420 | else:
421 | self.max_batch_insert_db_size = 500
422 |
423 |
424 | # 外部队列handle
425 | self.queue_handle = self.findAdapterHandler('queue',self.conf['outputer']['queue']).initQueue(self)
426 | # 外部 存储引擎
427 | self.storage_handle = self.findAdapterHandler('storage',self.conf['outputer']['save_engine']).initStorage(self)
428 |
429 | # self.traffic_analysis_handel = self.findAdapterHandler('traffic_analysis',self.conf['outputer']['save_engine']).initStorage(self)
430 |
431 |
432 |
433 | def _parse_line_data(self,line):
434 |
435 | if isinstance(line ,str):
436 | line_data = json.loads(line)
437 | else:
438 | line_data = line
439 |
440 |
441 | try:
442 |
443 | # 预编译对应的正则
444 | self.logParse.getLogFormatByConfStr(line_data['log_format_str'],line_data['log_format_vars'], line_data['log_format_name'])
445 |
446 | line_data['line'] = line_data['line'].strip()
447 |
448 | parse_data = self.logParse.parse(line_data['log_format_name'], line_data['line'])
449 |
450 | # 解析数据错误
451 | except ParseError as e:
452 | self.logging.error('\n pid : %s 数据解析错误: %s 数据: %s' % (os.getpid(), e.args, line))
453 | return False
454 | except ReCompile as e:
455 | unkown_error = '\n pid : %s 预编译错误: %s ,error_class: %s ,数据: %s' % (os.getpid(), e.__class__, e.args, line)
456 | self.logging.error(unkown_error)
457 | raise Exception(unkown_error)
458 |
459 | except Exception as e:
460 | unkown_error = '\n pid : %s 未知错误: %s ,error_class: %s ,数据: %s' % (os.getpid(), e.__class__, e.args, line)
461 | self.logging.error(unkown_error)
462 | raise Exception(unkown_error)
463 |
464 |
465 | del line_data['log_format_name']
466 | del line_data['log_format_str']
467 | del line_data['log_format_vars']
468 | del line_data['line']
469 |
470 | line_data.update(parse_data)
471 |
472 |
473 | return line_data
474 |
475 |
476 | def _get_queue_count_num(self):
477 | return self.queue_handle.getDataCountNum()
478 |
479 |
480 | # 获取队列数据
481 | def getQueueData(self):
482 | return self.queue_handle.getDataFromQueue()
483 |
484 | # 消费队列
485 | def saveToStorage(self ):
486 | self.storage_handle.pushDataToStorage()
487 |
488 | def watchTraffic(self):
489 |
490 | self.traffic_analysis_handel.start()
491 |
492 | #退回队列
493 | def rollBackQueue(self,data_list):
494 | self.queue_handle.rollBackToQueue(data_list)
495 |
496 |
497 |
498 |
499 | if __name__ == "__main__":
500 | pass
501 |
502 |
--------------------------------------------------------------------------------
/StorageAdapter/Mysql.py:
--------------------------------------------------------------------------------
1 | import redis.exceptions
2 |
3 | from StorageAdapter.BaseAdapter import Adapter
4 | import time,threading,os,json,pymysql,re
5 |
6 |
7 |
8 | try:
9 | # Python 3.x
10 | from urllib.parse import quote_plus
11 | except ImportError:
12 | # Python 2.x
13 | from urllib import quote_plus
14 |
15 | class StorageAp(Adapter):
16 |
17 | db = None
18 | runner = None
19 | field_map = None
20 | key_field_map = None
21 |
22 |
23 | @classmethod
24 | def initStorage(cls,runnerObject):
25 | self = cls()
26 | self.runner = runnerObject
27 | self.conf = self.runner.conf
28 | self.logging = self.runner.logging
29 |
30 | pymysql_timeout_secends = 60
31 | try:
32 | self.db = pymysql.connect(
33 | host = self.conf['mysql']['host'],
34 | port = int(self.conf['mysql']['port']),
35 | user = quote_plus(self.conf['mysql']['username']),
36 | password = quote_plus(self.conf['mysql']['password']),
37 | db = quote_plus(self.conf['mysql']['db']),
38 | connect_timeout = pymysql_timeout_secends,
39 | read_timeout = pymysql_timeout_secends,
40 | write_timeout = pymysql_timeout_secends,
41 |
42 | )
43 |
44 | except pymysql.err.MySQLError:
45 | self.logging.error('Mysql 链接失败,请检查配置文件!')
46 | raise Exception('Mysql 链接失败,请检查配置文件!')
47 |
48 |
49 | return self
50 |
51 |
52 | def pushDataToStorage(self):
53 | retry_reconnect_time = 0
54 |
55 | while True:
56 | time.sleep(0.1)
57 | self._getTableName('table')
58 |
59 | if retry_reconnect_time == 0:
60 | try:
61 | # 获取队列数据
62 | queue_data = self.runner.getQueueData()
63 | except redis.exceptions.RedisError as e:
64 | self.logging.error(
65 | "\n outputerer -------pid: %s -- redis error at: %s time---- Exceptions %s ; %s \n" % (
66 | os.getpid(), retry_reconnect_time, e.__class__, e.args))
67 | print("\n outputerer -------pid: %s -- redis error at: %s time---- Exceptions %s ; %s \n" % (
68 | os.getpid(), retry_reconnect_time, e.__class__, e.args))
69 | return
70 |
71 |
72 |
73 |
74 | if len(queue_data) == 0:
75 | self.logging.debug('\n outputerer ---pid: %s wait for queue data \n ' % (os.getpid()))
76 | continue
77 |
78 |
79 |
80 | start_time = time.perf_counter()
81 |
82 | # 错误退回队列 (未解析的原始的数据)
83 | self.backup_for_push_back_queue = []
84 |
85 | _data = []
86 | for item in queue_data:
87 | if isinstance(item, bytes):
88 | item = item.decode(encoding='utf-8')
89 |
90 |
91 | self.backup_for_push_back_queue.append(item)
92 | # 解析日志数据
93 | item = self.runner._parse_line_data(item)
94 |
95 | if item:
96 | _data.append(item)
97 |
98 |
99 | end_time = time.perf_counter()
100 |
101 | take_time = round(end_time - start_time, 3)
102 | self.logging.debug(
103 | '\n outputerer ---pid: %s tid: %s reg data len:%s; take time : %s \n ' %
104 | (os.getpid(), threading.get_ident(), len(_data), take_time))
105 |
106 |
107 | if 'max_retry_reconnect_time' in self.conf['outputer']:
108 | max_retry_reconnect_time = int(self.conf['outputer']['max_retry_reconnect_time'])
109 | else:
110 | max_retry_reconnect_time = 3
111 |
112 | # 解析完成 批量入库
113 | try:
114 | start_time = time.perf_counter()
115 |
116 | if len(_data) == 0:
117 | continue
118 |
119 | # for reconnect
120 | self.db.ping()
121 |
122 | affected_rows = self.__insertToMysql(_data)
123 |
124 | # reset retry_reconnect_time
125 | retry_reconnect_time = 0
126 |
127 | end_time = time.perf_counter()
128 | self.logging.debug("\n outputerer -------pid: %s -- insert into mysql : %s---- end 耗时: %s \n" % (
129 | os.getpid(), affected_rows, round(end_time - start_time, 3)))
130 |
131 |
132 | except pymysql.err.DataError as e:
133 |
134 | # (1406, "Data too long for column 'isp' at row 2")
135 | if e.args[0] == 1406 :
136 | wrong_field = re.findall(r'\'(\w+)\'' ,e.args[1])
137 | if len(wrong_field) > 0:
138 | self.__changeFieldTypeReInsert(wrong_field,_data)
139 |
140 | else:
141 | self.__saveWrongData(_data ,e)
142 | continue
143 |
144 | except pymysql.err.MySQLError as e:
145 | time.sleep(2)
146 | retry_reconnect_time = retry_reconnect_time + 1
147 | if retry_reconnect_time >= max_retry_reconnect_time:
148 | self.runner.rollBackQueue(self.backup_for_push_back_queue)
149 | self.logging.error('重试重新链接 mongodb 超出最大次数 %s' % max_retry_reconnect_time)
150 | raise Exception('重试重新链接 mongodb 超出最大次数 %s' % max_retry_reconnect_time)
151 | else:
152 |
153 | self.logging.error("\n outputerer -------pid: %s -- retry_reconnect_mysql at: %s time---- Exceptions %s ; %s \n" % (
154 | os.getpid(),retry_reconnect_time,e.__class__ ,e.args))
155 | continue
156 |
157 |
158 |
159 | # 根据数据 和 nginx 解析器中的format 创建 mysql 字段类型的映射
160 | def build_field_map(self,example_data):
161 | field_map = {}
162 | key_field_map = {}
163 |
164 | # 开始组装 mysql字典
165 | data_key = list(example_data.keys())
166 |
167 | for i in self.runner.logParse.format:
168 | format_key = i.replace('$', '')
169 |
170 | # 检查默认值是否在数据中
171 | if format_key in data_key:
172 |
173 | if 'mysql_key_field' in self.runner.logParse.format[i]:
174 | key_field_map[format_key] = self.runner.logParse.format[i]['mysql_key_field']
175 |
176 | if 'mysql_field_type' in self.runner.logParse.format[i]:
177 | field_map[format_key] = self.runner.logParse.format[i]['mysql_field_type']
178 | else:
179 | field_map[format_key] = 'varchar(255)'
180 |
181 | # 检查 nickname 是否在数据中
182 | elif 'nickname' in self.runner.logParse.format[i] \
183 | and self.runner.logParse.format[i]['nickname'] in data_key:
184 |
185 | if 'mysql_key_field' in self.runner.logParse.format[i]:
186 | key_field_map[ self.runner.logParse.format[i]['nickname'] ] = self.runner.logParse.format[i]['mysql_key_field']
187 |
188 | if 'mysql_field_type' in self.runner.logParse.format[i]:
189 | field_map[ self.runner.logParse.format[i]['nickname'] ] = self.runner.logParse.format[i]['mysql_field_type']
190 | else:
191 | field_map[ self.runner.logParse.format[i]['nickname'] ] = 'varchar(255)'
192 |
193 |
194 | # 检查 extend_field 是否在数据中
195 | if 'extend_field' in self.runner.logParse.format[i]:
196 | _intersection = set(data_key).intersection(
197 | set(list(self.runner.logParse.format[i]['extend_field'].keys())))
198 |
199 | if len(_intersection):
200 | for k in _intersection:
201 |
202 | if 'mysql_key_field' in self.runner.logParse.format[i]['extend_field'][k]:
203 | key_field_map[k] = self.runner.logParse.format[i]['extend_field'][k]['mysql_key_field']
204 |
205 | if 'mysql_field_type' in self.runner.logParse.format[i]['extend_field'][k]:
206 | field_map[k] = self.runner.logParse.format[i]['extend_field'][k]['mysql_field_type']
207 | else:
208 | field_map[k] = 'varchar(255)'
209 |
210 |
211 | return field_map , key_field_map
212 |
213 | def __insertToMysql(self,data):
214 |
215 | if self.field_map == None:
216 | self.field_map ,self.key_field_map = self.build_field_map(data[0])
217 |
218 | try:
219 | fields = None
220 |
221 | _valuelist = []
222 | for item in data:
223 |
224 | fk = list(item.keys())
225 | fields = ','.join(fk)
226 |
227 | for i in item:
228 |
229 | field_type = self.field_map[i]
230 |
231 | if (field_type.find('int') > -1 or field_type.find('float') > -1) and str(item[i]) == '':
232 | item[i] = '"0"'
233 | elif str(item[i]) == '':
234 | item[i] = '"-"'
235 | else:
236 | item[i] = '"%s"' % str(item[i]).strip('"')
237 |
238 | values = '(%s)' % ','.join(list(item.values()))
239 | _valuelist.append(values)
240 | except KeyError as e:
241 | self.runner.logging.error('Exception: %s ; 数据写入错误: %s 请检查 ParserAdapter 中的 getLogFormat 配置' % (e.__class__, e.args))
242 | raise Exception(' Exception: %s ; 数据写入错误: %s ;请检查 ParserAdapter 中的 getLogFormat 配置' % (e.__class__, e.args))
243 |
244 |
245 | sql = "INSERT INTO %s(%s) VALUES %s" % (self.table,fields,','.join(_valuelist))
246 |
247 | self.debug_sql = sql
248 |
249 | try:
250 | with self.db.cursor() as cursor:
251 | affected_rows = cursor.execute(sql)
252 |
253 | self.db.commit()
254 |
255 | return affected_rows
256 | # when table not found
257 | except pymysql.err.ProgrammingError as e:
258 | create_table_flag = self._handle_queue_data_before_into_storage(self.backup_for_push_back_queue)
259 | # 创建表的时候 补插入数据
260 | if create_table_flag == True:
261 |
262 | with self.db.cursor() as cursor:
263 | affected_rows = cursor.execute(sql)
264 | self.db.commit()
265 | return affected_rows
266 | # 数据表存在的 其它错误
267 | else:
268 | self.runner.logging.error('Exception: %s ; %s 数据写入错误: %s ;sql: %s' % (e.__class__,e.args , self.debug_sql))
269 | raise Exception(' Exception: %s ; 数据写入错误: %s ;sql: %s' % (e.__class__,e.args , self.debug_sql))
270 |
271 |
272 | def __changeFieldTypeReInsert(self,wrong_field,data):
273 |
274 |
275 |
276 | for f in wrong_field:
277 | data_len_arg = []
278 | for item in data:
279 | data_len_arg.append( len(item[f]) )
280 |
281 |
282 | try:
283 | # varchar 长度 取最大的一个
284 | sql = 'ALTER TABLE `%s`.`%s` MODIFY COLUMN `%s` varchar(%s) NOT NULL' % \
285 | (self.conf['mysql']['db'], self.table, f, sorted(data_len_arg)[-1])
286 | with self.db.cursor() as cursor:
287 | cursor.execute(sql)
288 |
289 | except pymysql.err.OperationalError as e:
290 | # 字段太长导致无法加索引 Specified key was too long; max key length is 3072 bytes
291 | # 字段太长导致无法存储 Column length too big for column 'request_url' (max = 16383); use BLOB or TEXT instead
292 | if e.args[0] in [1071,1074] :
293 | if e.args[0] == 1071:
294 | ftype = 'text'
295 | if e.args[0] == 1074:
296 | ftype = 'mediumtext'
297 |
298 | # 该字段有索引 则删除
299 | table_keys = self.__getTableKeys()
300 | for k in table_keys:
301 | if k.find(f) > -1:
302 | key_name = k.split(' ')[1].strip('`')
303 | drop_index = 'ALTER TABLE `%s`.`%s` DROP INDEX `%s`' % (self.conf['mysql']['db'], self.table ,key_name)
304 | try:
305 | with self.db.cursor() as cursor:
306 | cursor.execute(drop_index)
307 | except pymysql.err.OperationalError:
308 | continue
309 |
310 |
311 | # 重建字段类型 text
312 | sql = 'ALTER TABLE `%s`.`%s` MODIFY COLUMN `%s` %s NOT NULL' % (self.conf['mysql']['db'], self.table, f , ftype)
313 | with self.db.cursor() as cursor:
314 | cursor.execute(sql)
315 |
316 |
317 |
318 |
319 |
320 | try:
321 | self.__insertToMysql(data)
322 | except pymysql.err.DataError as e:
323 | self.__saveWrongData(data ,e)
324 |
325 |
326 |
327 | def __saveWrongData(self,data ,e):
328 | # 写入错误的数据 输出成json文件以供分析
329 | error_json_file_dir = self.runner._root + '/error_insert_data/%s' % self.runner.config_name.replace(
330 | '.ini', '')
331 | error_json_file = error_json_file_dir + '/%s_pid_%s.json' % (
332 | time.strftime('%Y_%m_%d_%H:%M:%S.%s', time.localtime()), os.getpid())
333 | error_msg = "\n outputerer -------pid: %s -- pymysql.err.DataError 数据类型错误 请检查 field_map 配置---- Exceptions: %s ;异常数据已保存在 %s \n" % (
334 | os.getpid(), e.args, error_json_file)
335 | self.logging.error(error_msg)
336 |
337 | if not os.path.exists(error_json_file_dir):
338 | os.makedirs(error_json_file_dir)
339 |
340 | if not os.path.exists(error_json_file):
341 | with open(error_json_file, 'w+') as fd:
342 | json.dump(data, fd)
343 | fd.close()
344 |
345 |
346 |
347 | def getKeyFieldStrForCreateTableFromList(self,key_field_needed ,i):
348 |
349 | def func(vars,i,re_field = False):
350 | _list = []
351 | _key_field_name = []
352 |
353 | if vars.find('.') > -1:
354 | _key = vars.split('.')
355 | if _key[1] in self.runner.logParse.format[_key[0]]['extend_field']:
356 | _list.append('KEY `{0}_{1}` (`{0}`,`{1}`)'.format(i, _key[1]))
357 | _key_field_name = _key[1]
358 | else:
359 | error_str = 'self.runner.logParse.format[%s] 不存在 "%s" 属性; 请检查解析中的logformat 配置' % (_key[0], _key[1])
360 | self.runner.logging.error(error_str)
361 | raise KeyError(error_str)
362 | else:
363 | if 'nickname' in self.runner.logParse.format[vars]:
364 | field_name = self.runner.logParse.format[vars]['nickname']
365 | else:
366 | field_name = vars.replace('$', '')
367 |
368 | _key_field_name = field_name
369 | _list.append('KEY `{0}_{1}` (`{0}`,`{1}`)'.format(i, field_name))
370 |
371 | if re_field:
372 | return _key_field_name
373 |
374 | return _list
375 |
376 | karg = []
377 |
378 | for args in key_field_needed[i]:
379 |
380 | if isinstance(args, str):
381 |
382 | karg = karg + func(args,i)
383 |
384 | elif isinstance(args, list):
385 | key_str = [i]
386 |
387 | for g in args:
388 | key_str.append(func(g,i,True))
389 |
390 | karg.append('KEY %s (%s)' % ('_'.join(key_str), ','.join(key_str)))
391 |
392 |
393 | return karg
394 |
395 | def __getTableKeys(self):
396 | # 从字段中获取需要创建索引的 字段
397 | key_field_needed = self.key_field_map
398 |
399 | karg = []
400 | if len(key_field_needed):
401 |
402 | for i in key_field_needed:
403 |
404 | if isinstance(key_field_needed[i], str): # 字符串
405 | karg.append('{0} `{1}` (`{1}`)'.format(key_field_needed[i].upper(), i))
406 | elif isinstance(key_field_needed[i], bool):
407 | karg.append('KEY `{0}` (`{0}`)'.format(i))
408 | elif isinstance(key_field_needed[i], list):
409 | karg.append('KEY `{0}` (`{0}`)'.format(i))
410 | karg = karg + self.getKeyFieldStrForCreateTableFromList(key_field_needed, i)
411 | # 去重
412 | return list(set(karg))
413 |
414 | def __createTable(self,org_data):
415 |
416 | if len(org_data) > 0:
417 |
418 | fields = []
419 | for i in self.field_map:
420 | _str = "`%s` %s NOT NULL " % (i ,self.field_map[i] )
421 | fields.append(_str)
422 |
423 | # 从字段中获取需要创建索引的 字段
424 | key_field_needed = self.key_field_map
425 |
426 | key_str = ''
427 | keys_arr = self.__getTableKeys()
428 | if len(keys_arr):
429 | key_str = ',' + ','.join(keys_arr)
430 |
431 |
432 | sql = """
433 | CREATE TABLE IF NOT EXISTS `%s`.`%s` (
434 | `id` int(11) NOT NULL AUTO_INCREMENT,
435 | %s ,
436 | PRIMARY KEY (`id`)
437 | %s
438 | )
439 | """ % (self.conf['mysql']['db'], self.table ,','.join(fields),key_str)
440 |
441 |
442 | try:
443 | with self.db.cursor() as cursor:
444 | cursor.execute(sql)
445 | except pymysql.MySQLError as e:
446 |
447 | self.logging.error('数据表 %s.%s 创建失败 ;Exception: %s ; SQL:%s' % (self.conf['mysql']['db'], self.conf['mysql']['table'] , e.args ,sql))
448 | raise Exception('数据表 %s.%s 创建失败 ;Exception: %s ; SQL:%s' % (self.conf['mysql']['db'], self.conf['mysql']['table'], e.args ,sql))
449 |
450 |
451 | def __checkTableExist(self):
452 | sql = "SELECT table_name FROM information_schema.TABLES WHERE table_name ='%s'" % self.table;
453 | with self.db.cursor() as cursor:
454 | cursor.execute(sql)
455 | res = cursor.fetchone()
456 |
457 | return res
458 |
459 | # 检查table 是否存在
460 | def _handle_queue_data_before_into_storage(self ,org_data):
461 |
462 | res = self.__checkTableExist()
463 |
464 | if not res:
465 | self.logging.warn('没有发现数据表,开始创建数据表')
466 | self.__createTable(org_data)
467 | return True
468 |
469 |
470 | return False
471 |
472 | # 在持久化存储之前 对 队列中的数据 进行预处理 ,比如 update ,delete 等操作
473 | def _handle_queue_data_after_into_storage(self):
474 | pass
475 |
476 |
477 |
478 |
479 | # 流量分析
480 | class TrafficAnalysisAp(StorageAp):
481 |
482 | def analysisTraffic(self):
483 | table_exist = self.__checkTableExist()
484 |
485 | print(table_exist)
486 | pass
--------------------------------------------------------------------------------
/webServer/static/js/index.js:
--------------------------------------------------------------------------------
1 | /*********************left start********************************/
2 |
3 | // 请求量最高的IP TOP50 ------------------------------- start -------------------------------
4 | var top_ip_chart = echarts.init(document.querySelector(".bar .chart"));
5 | window.addEventListener("resize", function () {
6 | top_ip_chart.resize();
7 | });
8 | window.chart_load_func['top_ip_chart'] = function () {
9 |
10 | $.ajax({
11 | url: host + '/get_request_num_by_ip',
12 | type:'GET',
13 | async:true,
14 | headers: { 'Authorization':Authorization,},
15 | success:function(msg){
16 | data = msg.data
17 | var xdata = []
18 | var ydata = []
19 | for (var i=0; i< data.length ;i++){
20 | xdata.push(data[i]['ip'])
21 | ydata.push(data[i]['total_num'])
22 | }
23 | // 指定配置和数据
24 | var option = {
25 | color: ["#2f89cf"],
26 | tooltip: {
27 | trigger: "axis",
28 | axisPointer: {
29 | // 坐标轴指示器,坐标轴触发有效
30 | type: "line" // 默认为直线,可选为:'line' | 'shadow'
31 | }
32 | },
33 | grid: {
34 | left: "0%",
35 | top: "10px",
36 | right: "0%",
37 | bottom: "4%",
38 | containLabel: true
39 | },
40 | xAxis: [
41 | {
42 | type: "category",
43 | data: xdata,
44 | axisTick: {
45 | alignWithLabel: true
46 | },
47 | axisLabel: {
48 | textStyle: {
49 | color: "rgba(255,255,255,.6)",
50 | fontSize: "12"
51 | }
52 | },
53 | axisLine: {
54 | show: true
55 | }
56 | }
57 | ],
58 | yAxis: [
59 | {
60 | type: "value",
61 | axisLabel: {
62 | textStyle: {
63 | color: "rgba(255,255,255,.6)",
64 | fontSize: "12"
65 | }
66 | },
67 | axisLine: {
68 | lineStyle: {
69 | color: "rgba(255,255,255,.1)"
70 | // width: 1,
71 | // type: "solid"
72 | }
73 | },
74 | splitLine: {
75 | lineStyle: {
76 | color: "rgba(255,255,255,.1)"
77 | }
78 | }
79 | }
80 | ],
81 | series: [
82 | {
83 | name: "IP",
84 | type: "bar",
85 | barWidth: "35%",
86 | // data: [200, 300, 300, 900, 1500, 1200, 600],
87 | data: ydata,
88 | itemStyle: {
89 | barBorderRadius: 5
90 | }
91 | }
92 | ]
93 | };
94 |
95 | // 把配置给实例对象
96 | top_ip_chart.setOption(option);
97 | }
98 | })
99 |
100 |
101 |
102 | }
103 | // 请求量最高的IP TOP50 ------------------------------- end -------------------------------
104 |
105 |
106 | // 最近10分钟每分钟流量 ------------------------------- start -------------------------------
107 | var network_traffic_by_minute = echarts.init(document.querySelector(".line .chart"));
108 | window.addEventListener("resize", function () {
109 | network_traffic_by_minute.resize();
110 | });
111 | window.chart_load_func['network_traffic_by_minute'] = function (type = null) {
112 | if(type == 'init'){
113 | __url = host + '/get_network_traffic_by_minute?type=init'
114 | }else {
115 | __url = host + '/get_network_traffic_by_minute'
116 | }
117 | $.ajax({
118 | url:__url,
119 | type:'GET',
120 | async:true,
121 | headers: { 'Authorization':Authorization,},
122 | success:function(msg){
123 | data = msg.data
124 |
125 | secends_values = {
126 | 'in_network':[],
127 | 'out_network':[],
128 | 'xAxis':[]
129 | }
130 | secends_xAxis = []
131 | $.each(data,function(k,v){
132 | secends_values['in_network'].push(Math.round(v['in_network'],0))
133 | secends_values['out_network'].push(Math.round(v['out_network'],0))
134 | secends_values['xAxis'].push(timestampToTime(v['time_str']))
135 | })
136 |
137 | // 2. 指定配置和数据
138 | var option = {
139 | color: ["#00f2f1", "#ed3f35"],
140 | tooltip: {
141 | // 通过坐标轴来触发
142 | trigger: "axis"
143 | },
144 | legend: {
145 | // 距离容器10%
146 | right: "10%",
147 | // 修饰图例文字的颜色
148 | textStyle: {
149 | color: "#4c9bfd"
150 | }
151 | // 如果series 里面设置了name,此时图例组件的data可以省略
152 | // data: ["邮件营销", "联盟广告"]
153 | },
154 | grid: {
155 | top: "20%",
156 | left: "3%",
157 | right: "4%",
158 | bottom: "3%",
159 | show: true,
160 | borderColor: "#012f4a",
161 | containLabel: true
162 | },
163 |
164 | xAxis: {
165 | type: "category",
166 | boundaryGap: false,
167 | data: secends_values['xAxis'],
168 | // 去除刻度
169 | axisTick: {
170 | show: false
171 | },
172 | // 修饰刻度标签的颜色
173 | axisLabel: {
174 | color: "rgba(255,255,255,.7)"
175 | },
176 | // 去除x坐标轴的颜色
177 | axisLine: {
178 | show: false
179 | }
180 | },
181 | yAxis: {
182 | type: "value",
183 | // 去除刻度
184 | axisTick: {
185 | show: false
186 | },
187 | // 修饰刻度标签的颜色
188 | axisLabel: {
189 | color: "rgba(255,255,255,.7)"
190 | },
191 | // 修改y轴分割线的颜色
192 | splitLine: {
193 | lineStyle: {
194 | color: "#012f4a"
195 | }
196 | }
197 | },
198 | series: [
199 | {
200 | name:'入网 KB',
201 | type: "line",
202 | // stack: "总量",
203 | smooth: true,
204 | data: secends_values['in_network']
205 | },
206 | {
207 | name:'出网 KB',
208 | type: "line",
209 | // stack: "总量",
210 | smooth: true,
211 | data: secends_values['out_network']
212 | }
213 | ]
214 | };
215 | // 3. 把配置和数据给实例对象
216 | network_traffic_by_minute.setOption(option);
217 | }
218 |
219 |
220 | })
221 |
222 |
223 |
224 | }
225 | // 最近10分钟pv ------------------------------- end -------------------------------
226 |
227 |
228 | // 非200状态码 二级分类 ------------------------------- start -------------------------------
229 | var status_code_chart = echarts.init(document.querySelector(".bar1 .chart"));
230 | window.addEventListener("resize", function () {
231 | status_code_chart.resize();
232 | });
233 | window.chart_load_func['status_code_chart'] = function () {
234 | $.ajax({
235 | url: host + '/get_request_num_by_status',
236 | type:'GET',
237 | async:true,
238 | headers: { 'Authorization':Authorization,},
239 | success:function(msg){
240 | var data = [];
241 | var titlename = [];
242 | var valdata = [];
243 | var maxRang = []
244 | var total = 0
245 | $.each(msg.data ,function(k,v){
246 | total = total + v.total_num
247 | valdata.push(v.total_num)
248 | titlename.push('状态码:' + v.status_code.toString())
249 | })
250 |
251 | $.each(msg.data ,function(k,v){
252 | percent = (v.total_num / total) * 100
253 | data.push(percent.toFixed(2))
254 | maxRang.push(100)
255 | })
256 |
257 |
258 |
259 | var myColor = ["#1089E7", "#F57474", "#56D0E3", "#F8B448", "#8B78F6"];
260 | option = {
261 | //图标位置
262 | grid: {
263 | top: "10%",
264 | left: "22%",
265 | bottom: "10%"
266 | },
267 | xAxis: {
268 | show: false
269 | },
270 | yAxis: [
271 | {
272 | show: true,
273 | data: titlename,
274 | inverse: true,
275 | axisLine: {
276 | show: false
277 | },
278 | splitLine: {
279 | show: false
280 | },
281 | axisTick: {
282 | show: false
283 | },
284 | axisLabel: {
285 | color: "#fff",
286 | rich: {
287 | lg: {
288 | backgroundColor: "#339911",
289 | color: "#fff",
290 | borderRadius: 15,
291 | // padding: 5,
292 | align: "center",
293 | width: 15,
294 | height: 15
295 | }
296 | }
297 | }
298 | },
299 | {
300 | show: true,
301 | inverse: true,
302 | data: valdata,
303 | axisLabel: {
304 | textStyle: {
305 | fontSize: 12,
306 | color: "#fff"
307 | }
308 | }
309 | }
310 | ],
311 | series: [
312 | {
313 | name: "条",
314 | type: "bar",
315 | yAxisIndex: 0,
316 | data: data,
317 | barCategoryGap: 50,
318 | barWidth: 10,
319 | itemStyle: {
320 | normal: {
321 | barBorderRadius: 20,
322 | color: function (params) {
323 | var num = myColor.length;
324 | return myColor[params.dataIndex % num];
325 | }
326 | }
327 | },
328 | label: {
329 | normal: {
330 | show: true,
331 | position: "right",
332 | formatter: "{c}%"
333 | }
334 | }
335 | },
336 | {
337 | name: "框",
338 | type: "bar",
339 | yAxisIndex: 1,
340 | barCategoryGap: 50,
341 | data: maxRang,
342 | barWidth: 15,
343 | itemStyle: {
344 | normal: {
345 | color: "none",
346 | borderColor: "#00c1de",
347 | borderWidth: 3,
348 | barBorderRadius: 15
349 | }
350 | }
351 | }
352 | ]
353 | };
354 |
355 | // 使用刚指定的配置项和数据显示图表。
356 | status_code_chart.setOption(option);
357 |
358 |
359 | }
360 |
361 | })
362 | }
363 | // 非200状态码 二级分类 ------------------------------- end -------------------------------
364 |
365 |
366 | // 最近10分钟IP / PV ------------------------------- start -------------------------------
367 | var request_ip_pv_by_minute = echarts.init(document.querySelector(".member .chart"));
368 | window.addEventListener("resize", function () {
369 | request_ip_pv_by_minute.resize();
370 | });
371 | window.chart_load_func['request_ip_pv_by_minute'] = function () {
372 | $.ajax({
373 | url: host + '/get_ip_pv_num_by_minute',
374 | type:'GET',
375 | async:true,
376 | headers: { 'Authorization':Authorization,},
377 | success:function(msg){
378 | data = msg.data
379 |
380 |
381 | nums = {
382 | 'ip_num':[],
383 | 'pv_num':[]
384 | }
385 | timestamp = []
386 | $.each(data,function(k,v){
387 |
388 | nums['ip_num'].push(v['ip_num'])
389 | nums['pv_num'].push(v['pv_num'])
390 | timestamp.push(timestampToTime(v['time_str']))
391 | })
392 |
393 |
394 |
395 | option = {
396 | tooltip: {
397 | trigger: "axis",
398 | axisPointer: {
399 | lineStyle: {
400 | color: "#dddc6b"
401 | }
402 | }
403 | },
404 | legend: {
405 | top: "0%",
406 | textStyle: {
407 | color: "rgba(255,255,255,.5)",
408 | fontSize: "12"
409 | }
410 | },
411 | grid: {
412 | left: "10",
413 | top: "30",
414 | right: "10",
415 | bottom: "10",
416 | containLabel: true
417 | },
418 |
419 | xAxis: [
420 | {
421 | type: "category",
422 | boundaryGap: false,
423 | axisLabel: {
424 | textStyle: {
425 | color: "rgba(255,255,255,.6)",
426 | fontSize: 12
427 | }
428 | },
429 | axisLine: {
430 | lineStyle: {
431 | color: "rgba(255,255,255,.2)"
432 | }
433 | },
434 |
435 | data: timestamp
436 | },
437 | {
438 | axisPointer: { show: false },
439 | axisLine: { show: false },
440 | position: "bottom",
441 | offset: 20
442 | }
443 | ],
444 | yAxis: [
445 | {
446 | type: "value",
447 | axisTick: { show: false },
448 | axisLine: {
449 | lineStyle: {
450 | color: "rgba(255,255,255,.1)"
451 | }
452 | },
453 | axisLabel: {
454 | textStyle: {
455 | color: "rgba(255,255,255,.6)",
456 | fontSize: 12
457 | }
458 | },
459 |
460 | splitLine: {
461 | lineStyle: {
462 | color: "rgba(255,255,255,.1)"
463 | }
464 | }
465 | }
466 | ],
467 | series: [
468 | {
469 | name: "每分钟IP数",
470 | type: "line",
471 | smooth: true,
472 | symbol: "circle",
473 | symbolSize: 5,
474 | showSymbol: true,
475 | lineStyle: {
476 | normal: {
477 | color: "#00d887",
478 | width: 2
479 | }
480 | },
481 | areaStyle: {
482 | normal: {
483 | color: new echarts.graphic.LinearGradient(
484 | 0,
485 | 0,
486 | 0,
487 | 1,
488 | [
489 | {
490 | offset: 0,
491 | color: "rgba(0, 216, 135, 0.4)"
492 | },
493 | {
494 | offset: 0.8,
495 | color: "rgba(0, 216, 135, 0.1)"
496 | }
497 | ],
498 | false
499 | ),
500 | shadowColor: "rgba(0, 0, 0, 0.1)"
501 | }
502 | },
503 | itemStyle: {
504 | normal: {
505 | color: "#00d887",
506 | borderColor: "rgba(221, 220, 107, .1)",
507 | borderWidth: 12
508 | }
509 | },
510 | data: nums['ip_num']
511 | },
512 | {
513 | name: "每分钟PV数",
514 | type: "line",
515 | smooth: true,
516 | symbol: "circle",
517 | symbolSize: 5,
518 | showSymbol: true,
519 | lineStyle: {
520 | normal: {
521 | color: "#006cff",
522 | width: 2
523 | }
524 | },
525 | areaStyle: {
526 | normal: {
527 | color: new echarts.graphic.LinearGradient(
528 | 0,
529 | 0,
530 | 0,
531 | 1,
532 | [
533 | {
534 | offset: 0,
535 | color: "rgba(0, 216, 135, 0.4)"
536 | },
537 | {
538 | offset: 0.8,
539 | color: "rgba(0, 216, 135, 0.1)"
540 | }
541 | ],
542 | false
543 | ),
544 | shadowColor: "rgba(0, 0, 0, 0.1)"
545 | }
546 | },
547 | itemStyle: {
548 | normal: {
549 | color: "#006cff",
550 | borderColor: "rgba(221, 220, 107, .1)",
551 | borderWidth: 12
552 | }
553 | },
554 | data: nums['pv_num']
555 | }
556 | ]
557 | };
558 | // 使用刚指定的配置项和数据显示图表。
559 | request_ip_pv_by_minute.setOption(option);
560 | }
561 |
562 | })
563 | }
564 | // 最近10分钟IP ------------------------------- end -------------------------------
565 |
566 |
567 | // 热门接口URL请求TOP 10分布 ------------------------------- start -------------------------------
568 | var request_num_by_url = echarts.init(document.querySelector(".pie .chart"));
569 | window.addEventListener("resize", function () {
570 | request_num_by_url.resize();
571 | });
572 | window.chart_load_func['request_num_by_url'] = function () {
573 | $.ajax({
574 | url: host + '/get_request_num_by_url',
575 | type:'GET',
576 | async:true,
577 | headers: { 'Authorization':Authorization,},
578 | success:function(msg){
579 | data = msg.data
580 | range_keys = []
581 | range_values = []
582 | $.each(data ,function(k,v){
583 | range_keys.push(v['request_url'])
584 | range_values.push({value:v['total_num'] ,name:v['request_url']})
585 | })
586 | option = {
587 | tooltip: {
588 | trigger: "item",
589 | formatter: "{a}
{b}: {c} ({d}%)",
590 | position: function (p) {
591 | //其中p为当前鼠标的位置
592 | return [p[0] + 10, p[1] - 10];
593 | }
594 | },
595 | legend: {
596 | top: "90%",
597 | itemWidth: 10,
598 | itemHeight: 10,
599 | data: range_keys,
600 | textStyle: {
601 | color: "rgba(255,255,255,.5)",
602 | fontSize: "12"
603 | }
604 | },
605 | series: [
606 | {
607 | name: "接口分布",
608 | type: "pie",
609 | center: ["50%", "42%"],
610 | radius: ["40%", "60%"],
611 | color: [
612 | "#006cff",
613 | "#60cda0",
614 | "#ed8884",
615 | "#ff9f7f",
616 | "#0096ff",
617 | "#9fe6b8",
618 | "#32c5e9",
619 | "#1d9dff"
620 | ],
621 | label: { show: true },
622 | labelLine: { show: true },
623 | data:range_values
624 | }
625 | ]
626 | };
627 |
628 | // 使用刚指定的配置项和数据显示图表。
629 | request_num_by_url.setOption(option);
630 |
631 |
632 | }
633 |
634 | })
635 | }
636 | // 热门接口URL请求TOP 10 ------------------------------- end -------------------------------
637 |
638 |
639 | // 搜索引擎蜘蛛占比 ------------------------------- start -------------------------------
640 | var spider_by_ua = echarts.init(document.querySelector(".pie1 .chart"));
641 | window.addEventListener("resize", function () {
642 | spider_by_ua.resize();
643 | });
644 | window.chart_load_func['spider_by_ua'] = function () {
645 | $.ajax({
646 | url: host + '/get_spider_by_ua',
647 | type:'GET',
648 | async:true,
649 | headers: { 'Authorization':Authorization,},
650 | success:function(msg){
651 | data = msg.data
652 |
653 | company_data = [ ]
654 |
655 | $.each(data,function(k,v){
656 | company_data.push({value:v['total_num'] , name:v['ua']})
657 | })
658 |
659 | var option = {
660 | legend: {
661 | top: "90%",
662 | itemWidth: 10,
663 | itemHeight: 10,
664 | textStyle: {
665 | color: "rgba(255,255,255,.5)",
666 | fontSize: "12"
667 | }
668 | },
669 | tooltip: {
670 | trigger: "item",
671 | formatter: "{a}
{b} : {c} ({d}%)"
672 | },
673 | // 注意颜色写的位置
674 | color: [
675 | "#006cff",
676 | "#60cda0",
677 | "#ed8884",
678 | "#ff9f7f",
679 | "#0096ff",
680 | "#9fe6b8",
681 | "#32c5e9",
682 | "#1d9dff"
683 | ],
684 | series: [
685 | {
686 | name: "前十占比",
687 | type: "pie",
688 | // 如果radius是百分比则必须加引号
689 | radius: ["10%", "70%"],
690 | center: ["50%", "42%"],
691 | // roseType: "radius",
692 | data: company_data,
693 | // 修饰饼形图文字相关的样式 label对象
694 | label: {
695 | fontSize: 10
696 | },
697 | // 修饰引导线样式
698 | labelLine: {
699 | // 连接到图形的线长度
700 | length: 10,
701 | // 连接到文字的线长度
702 | length2: 10
703 | }
704 | }
705 | ]
706 | };
707 | // 3. 配置项和数据给我们的实例化对象
708 | spider_by_ua.setOption(option);
709 | }
710 | })
711 |
712 | }
713 | // 搜索引擎蜘蛛占比 ------------------------------- end -------------------------------
--------------------------------------------------------------------------------