0x01 前言

经常有人问我家里服务器的功率是多少,我网站的访问量是多少,在经过一番斗争后我决定将这些数据通过status pages展示出来。

在前两周我配置好了一款名为Cachet开源的status pages服务(NGX Services Status Page),配置过程请参考以下文章:

这次我还是使用python脚本调用各个系统的API,然后将数据填充到Cachet中,最终以图表的形式展示出来:

0x02 准备

首先,需要安装上面的文章配置好Cachet,然后登入到控制台,到METRICS标签中添加图表:

先配置电功率的图表,在配置过程中需要注意“图表计算方法”这个选项,我的计划是每分钟从zabbix中取数据,然后写入Cachet,所以这里需要选择average(平均数);然后配置nginx的访问次数,而这里则需要选择sum(总和):

上图中的“Suffix”为单位,可根据实际情况进行填写。

填写完所有内容后即可单击“ADD”以完成图表的创建。

在开始编写脚本之前需要Cachet相关的API信息,所有的API可以在以下地址中找到:

因为需要在图表中插入数据,所以在这里我们需要用到以下API:

还需要在zabbix中取数据,所以需要在以下地址中寻找zabbix的API信息:

而我的nginx日志是在ELK中分析的,所以还需要找到elasticsearch相关的信息:

这部分的内容比较复杂,需要有一个系统的学习或初步的认识才能理解,所以这里不多做讲解。

0x03 脚本

首先需要定义三个服务的API地址:

zbx_api_url = 'http://zabbix.t.com/api_jsonrpc.php'
es6_api_url = 'http://es6-node1.t.com:9200/'
cachethq_url = 'https://status.ngx.hk/api/v1/'

0x03.1 时间戳

我这个脚本会每分钟执行一次,而我希望将这些数据插入Cachet中的整分钟点钟(这句话有点难理解),例如:

  • 19:01:00
  • 19:02:00
  • 19:03:00
  • 19:04:00

与此同时,Cachet的API仅接受时间戳格式的时间表达,所以,我需要预先处理下时间:

def get_datetime():
    datetime_now = datetime.today().strftime("%Y-%m-%d %H:%M:00")
    datetime_now = datetime.strptime(datetime_now, '%Y-%m-%d %H:%M:%S')
    datetime_old = datetime_now - timedelta(minutes=1)

    timestamp_now = datetime_now.timestamp()
    timestamp_now = int(timestamp_now * 1000)

    timestamp_old = datetime_old.timestamp()
    timestamp_old = int(timestamp_old * 1000)

    output_dict = dict()
    output_dict['timestamp_now'] = str(timestamp_now)
    output_dict['timestamp_old'] = str(timestamp_old)
    return output_dict

先定义一个函数,获取当前的时间,但秒数我强制修改为“00”,然后再用strptime格式化。

因为需要在elasticsearch中搜索前一分钟内的访问量,所以还需要算出前一分钟的时间,用变量名datetime_old表示。

最后输出以下格式的字典:

{'timestamp_old': '1529933400000', 'timestamp_now': '1529933460000'}

0x03.2 zabbix

zabbix的API调用需要通过用户名和密码获取一个token,用完之后还需要登出以销毁该token,所以会有以下两个函数,分别实现登入和登出:

def zbx_login(zbx_username, zbx_passwd, zbx_id):
    payload = {
        "jsonrpc": "2.0",
        "method": "user.login",
        "params": {
            "user": zbx_username,
            "password": zbx_passwd
        },
        "id": zbx_id,
    }
    headers = {'content-type': 'application/json'}
    req_run = requests.post(zbx_api_url, data=json.dumps(payload), headers=headers)
    req_content = json.loads(req_run.text)
    return req_content


def zbx_logout(zbx_login_id, zbx_login_token):
    payload = {
        "jsonrpc": "2.0",
        "method": "user.logout",
        "params": [],
        "id": zbx_login_id,
        "auth": zbx_login_token
    }
    headers = {'content-type': 'application/json'}
    req_run = requests.post(zbx_api_url, data=json.dumps(payload), headers=headers)
    req_content = json.loads(req_run.text)
    return req_content

在登入函数中的payload有个id值,这里用时间戳填充,其实这是不严谨的,应该用UUID填充,这样才能保证该token与id对应的唯一性,但我的应用环境中要求不高,所以直接用时间戳填充。

登入和登出函数的输出内容如下:

#zbx_login
{'jsonrpc': '2.0', 'id': '1529934000000', 'result': 'e925d67c6fc019447db9e63eb1d27a21'}

#zbx_logout
{'jsonrpc': '2.0', 'id': '1529934000000', 'result': True}

取得zabbix token之后就可以调用history.get这个API获取最新的一条数据了:

def get_zbx_item_value(zbx_token, zbx_item_id):
    payload = {
        "jsonrpc": "2.0",
        "method": "history.get",
        "params": {
            "output": "extend",
            "history": 0,
            "itemids": zbx_item_id,
            "sortfield": "clock",
            "sortorder": "DESC",
            "limit": 1
        },
        "auth": zbx_token,
        "id": 1
    }
    headers = {'content-type': 'application/json'}
    req_run = requests.post(zbx_api_url, data=json.dumps(payload), headers=headers)
    req_content = json.loads(req_run.text)
    req_value = req_content['result'][0]['value']
    return str(req_value)

因为在zabbix中,item的id是唯一的,所以只需要找到相应的item,就可以找到相应的id:

然后利用zabbix token与item id一起调用该函数,返还的结果如下:

553.8000

其实返还的实际内容是json格式的,只是经过数据提取后,直接返还了实际的数值:

{
  'id': 1,
  'result': [
    {
      'ns': '598647172',
      'clock': '1529934932',
      'value': '593.8000',
      'itemid': '31712'
    }
  ],
  'jsonrpc': '2.0'
}

0x03.3 elasticsearch

elasticsearch的数据统计比较简单,因为我只想知道一分钟内的访问量有多少,那么我只需要计算出这一分钟内elasticsearch接收到多少条日志就可以了,至于请求的内容是图片、js还是css我并不关心,所以payload的json如下:

{
  "size": 0,
  "_source": {
    "excludes": []
  },
  "aggs": {},
  "stored_fields": [
    "@timestamp"
  ],
  "query": {
    "bool": {
      "must": [
        {
          "match_all": {}
        },
        {
          "range": {
            "@timestamp": {
              "gte": es_gte,
              "lte": es_lte,
              "format": "epoch_millis"
            }
          }
        }
      ]
    }
  }
}

另外,在请求的header中必须将payload设置为json:

headers = {'content-type': 'application/json'}

完整的函数如下:

def get_number_of_visits(es_index_name, es_gte, es_lte):
    payload = {
        "size": 0,
        "_source": {
            "excludes": []
        },
        "aggs": {},
        "stored_fields": [
            "@timestamp"
        ],
        "query": {
            "bool": {
                "must": [
                    {
                        "match_all": {}
                    },
                    {
                        "range": {
                            "@timestamp": {
                                "gte": es_gte,
                                "lte": es_lte,
                                "format": "epoch_millis"
                            }
                        }
                    }
                ]
            }
        }
    }
    headers = {'content-type': 'application/json'}
    req_run = requests.post(es6_api_url + es_index_name + '/_search', data=json.dumps(payload), headers=headers)
    req_content = json.loads(req_run.text)
    req_value = req_content['hits']['total']
    return str(req_value)

和zabbix获取数值的函数一样,最终输出的结果也是经过处理的:

118

而原始的返还内容如下:

{
  'took': 2,
  '_shards': {
    'skipped': 0,
    'total': 5,
    'failed': 0,
    'successful': 5
  },
  'hits': {
    'hits': [],
    'total': 118,
    'max_score': 0.0
  },
  'timed_out': False
}

0x03.4 Cachet

获取到所需要的数据后,接下来需要将数据填充到Cachet中,直接调用API即可:

def cachethq_metrics_add_point(api_token, metric_id, metric_value, metric_timestamp):
    req_url = cachethq_url + 'metrics/' + str(metric_id) + '/points'
    payload = {
        "value": metric_value,
        "timestamp": metric_timestamp
    }
    headers = {'X-Cachet-Token': api_token}
    req_run = requests.request('POST', req_url, data=payload, headers=headers)
    req_content = json.loads(req_run.text)
    return req_content

Cachet的API调用也需要验证身份,不过不需要使用用户名和密码,而是通过在header中添加“X-Cachet-Token”的token值即可,而这个token可以在控制台中点击头像,在个人信息配置页面中找到:

该函数中有metric_id这个变量,这是图表的id值,可以在控制台的图表页面中找到:

如果该函数能正常运行,那么会返还以下内容:

{
  'data': {
    'created_at': '2018-06-25 22:14:00',
    'metric_id': 1,
    'id': 20475,
    'counter': 1,
    'value': 576.4,
    'calculated_value': 576.4,
    'updated_at': '2018-06-25 22:14:32'
  }
}

0x04 收尾

完整的脚本可以在以下地址中找到:

将脚本放置到相应的路径后,再通过crond进行定时调用:

*/1 * * * * root /usr/bin/python3 /usr/local/services_data/shell/cachethq/main_temp.py

0x05 结语

经过将近一个月的测试与观察,一切运行正常。接下来计划增加各类模块,以便对接zabbix的监控,从而实现自动化调整各个服务的状态。