Current File : //opt/cloudlinux/venv/lib/python3.11/site-packages/cl_plus/collectors/mysql.py |
# coding=utf-8
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2020 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
import time
import warnings
from typing import Optional, Tuple
try:
import pymysql
except ImportError:
pymysql = None
from clcommon.cpapi import db_access
from clcommon.cpapi.cpapiexceptions import NoDBAccessData, NotSupported
from clcommon import mysql_lib
from .collector_base import CollectorBase
# enough to collect only several times per minute, cause we rely on DB counter
MYSQL_COLLECTION_INTERVAL = 30
def _total_queries_num_request(db, query_str):
"""
Execs passed query and returns value
"""
with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=pymysql.Warning)
data = db.execute_query(query_str)
return data[0][1]
class MySQLCollector(CollectorBase):
class DBAccess:
def __init__(self, login: Optional[str] = None, password: Optional[str] = None):
self.login = login
self.password = password
self.expire_time = time.time() + 3600
def __eq__(self, other):
return self.login == other.login and self.password == other.password
def __bool__(self):
return bool(self.login and self.password and not self.is_expired())
def is_expired(self) -> bool:
return time.time() > self.expire_time
def __init__(self, _logger):
super(MySQLCollector, self).__init__(_logger)
self._is_mysql_error = False
self.collection_interval = MYSQL_COLLECTION_INTERVAL
self.access = self.DBAccess()
self.previous_num, self.current_num = None, None
self._aggregation_times = 0
def init(self):
"""
Initialize MySQL collector
:return: None
"""
self._aggregated_data = 0
self._logger.info("MySQL collector init")
self._logger.info('MySQL collector initial values: previous: %s, current: %s',
str(self.previous_num),
str(self.current_num))
def _get_total_queries_num(self, dblogin: str, dbpass: str):
"""
Gets total queries number via Queries variable in DB
it is a total counter of queries made to DB
it is used be such monitoring system as Nagios and mysqladmin returns it
https://github.com/nagios-plugins/nagios-plugins/blob/master/plugins/check_mysql.c
"""
if not pymysql:
return "No MySQL client libraries installed", 0
try:
connector = mysql_lib.MySQLConnector(host='localhost',
user=dblogin,
passwd=dbpass,
db='mysql',
use_unicode=True,
charset='utf8')
with connector.connect() as db:
total = _total_queries_num_request(db, 'show global status where Variable_name = \'Questions\';')
self._aggregation_times += 1
# since mysql_lib is our lib, it could through different exceptions
except Exception as e:
return str(e), 0
return 'OK', int(total)
def _get_db_access(self) -> Tuple[str, str]:
"""
Get DB access data from cpapi function. Logs error
:return: tuple (db_root_login, db_password)
None, None if error
"""
if not self.access:
try:
access = db_access() # {'pass': str, 'login': str}
self.access = self.DBAccess(access['login'], access['pass'])
except (NoDBAccessData, NotSupported, KeyError, TypeError) as e:
# Can't retrieve data
if not self._is_mysql_error:
self._logger.warn("MySQL collector: can't obtain MySQL DB access: %s", str(e))
self._is_mysql_error = True
return self.access.login, self.access.password
def aggregate_new_data(self):
"""
Retrieve and aggregate new data
:return None
"""
self._get_db_access()
if not self.access: # access data still is not set
return
# New data present - aggregate
message, total_queries = self._get_total_queries_num(self.access.login, self.access.password)
if message == 'OK':
self.previous_num = self.current_num
self.current_num = total_queries
# no previous value to compare during 1st iteration after collector is started
if self.previous_num is not None and self.current_num is not None:
# if mysql counter was re-set
if self.current_num < self.previous_num:
self._logger.info('MySQL collector: QUERIES counter was re-set in database, '
're-set collectors previous counter as well')
self.previous_num = 0
self._aggregated_data += self.current_num - self.previous_num
self._is_mysql_error = False
return
# Retrieve data error
if not self._is_mysql_error:
self._logger.error("MySQL collector can't obtain MySQL queries number: %s", message)
self._is_mysql_error = True
def get_averages(self):
"""
Get collector's averages data
:return: dict:
{ "mysql_queries_num": 16}
or None if can't get data
"""
mysql_queries_num = max(0, self._aggregated_data - self._aggregation_times)
self._aggregated_data = 0
self._aggregation_times = 0
return {"mysql_queries_num": mysql_queries_num}