"""
OneAgent Memcached plugin
=========================
Gathers Memcached stats.
The full description of stats is described at https://github.com/memcached/memcached/blob/master/doc/protocol.txt.
Supported communication protocols are below:
* TCP - simple text lines
* Unix sockets - simple text lines
* UDP - text lines with binary header
All needed configuration is retrieved from parsing process command line.
"""
from ruxit.api.data import PluginMeasurement
from ruxit.api.base_plugin import BasePlugin
from ruxit.api.exceptions import ConfigException
from enum import IntEnum
import os
import stat
import socket
import struct
from contextlib import closing
class _CommunicationType(IntEnum):
tcp = 1
udp = 2
unix = 3
[docs]class MemcachedPlugin(BasePlugin):
"""
Memcached plugin class.
This plugin is stateful - initialize and close methods are defined in order to maintain one server connection for the lifetime of the plugin.
"""
absolute_values = {
"curr_connections",
"limit_maxbytes",
"bytes"
}
"""
Values retrieved from Memcached and send to Ruxit server as absolute values.
"""
per_second_values = {
"cmd_get",
"cmd_set",
"get_hits",
"get_misses",
"evictions",
"bytes_read",
"bytes_written"
}
"""
Values retrieved from Memcached and send to Ruxit server as per second values.
"""
SOCKET_TIMEOUT_SECONDS = 10
[docs] def set_communication_data(self, command_line):
"""
Parse command line and set communication protocol data.
Memcached command parameters are described at http://linux.die.net/man/1/memcached
Following data are discovered:
* TCP interface: IP address and port
* UDP interface: IP address and port
* Unix socket: Unix socket name
"""
self.port = 11211
self.interface = "127.0.0.1"
self.comm_type = _CommunicationType.tcp
commands = command_line.split(" ")
prev_command = ""
for command in commands:
if prev_command == "-s":
self.unix_socket = command
self.comm_type = _CommunicationType.unix
permissions = stat.S_IMODE(os.stat(command).st_mode)
if (permissions & 0o666) == 0o666:
return True
raise ConfigException("Access to unix socket forbidden: %s" % command)
elif prev_command == "-l":
self.interface = command
elif prev_command == "-U":
self.comm_type = _CommunicationType.udp
try:
self.port = int(command)
except ValueError:
self.port = None
elif prev_command == "-p":
self.comm_type = _CommunicationType.tcp
try:
self.port = int(command)
except ValueError:
self.port = None
prev_command = command
return self.port is not None
[docs] def initialize(self, **kwargs):
"""
Initializes Memcached plugin.
Raises:
ruxit.api.exceptions.ConfigException there is no Memcached process or command line isn't recognized
"""
self.entity = kwargs["associated_entity"]
for process in self.entity.processes:
command_line = process.properties.get("CmdLine", "")
if self.set_communication_data(command_line):
return
else:
raise ConfigException("Unable to parse Memcached commandline")
raise ConfigException("There is no Memcached process in PGI")
def _query_tcp(self, bytes_to_read=2048):
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as client_socket:
client_socket.settimeout(self.SOCKET_TIMEOUT_SECONDS)
client_socket.connect((self.interface, self.port))
return self._get_client_socket_data(client_socket, bytes_to_read)
def _query_ux_socket(self, bytes_to_read=2048):
with closing((socket.socket(socket.AF_UNIX, socket.SOCK_STREAM))) as client_socket:
client_socket.settimeout(self.SOCKET_TIMEOUT_SECONDS)
client_socket.connect(self.unix_socket)
return self._get_client_socket_data(client_socket, bytes_to_read)
def _query_udp(self):
with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as client_socket:
command = struct.pack("!Hhhh", 1, 0, 1, 0) + 'stats\r\n'.encode('utf-8')
client_socket.sendto(command, (self.interface, self.port))
output = ""
client_socket.settimeout(self.SOCKET_TIMEOUT_SECONDS)
data, server = client_socket.recvfrom(8192)
output += data[8:].decode("utf-8")
if "ERROR" in output:
raise ConfigException("'ERROR' in memcached stats output")
elif 'END' not in output:
raise ConfigException('Memcached stats are incomplete')
return output
def _get_client_socket_data(self, client_socket, bytes_to_read):
client_socket.send("stats\r\n".encode())
output = ""
iteration = 1
while "END" not in output:
output += client_socket.recv(bytes_to_read).decode()
if "ERROR" in output:
raise ConfigException("'ERROR' in memcached stats output")
if iteration > 10:
raise ConfigException("Error while getting memcached data")
iteration += 1
return output
[docs] def query(self, **kwargs):
"""
Calls specific communication protocol request, parses response and send data to Ruxit server.
"""
try:
if self.comm_type == _CommunicationType.tcp:
output = self._query_tcp()
elif self.comm_type == _CommunicationType.udp:
output = self._query_udp()
elif self.comm_type == _CommunicationType.unix:
output = self._query_ux_socket()
else:
output = ""
bytes_stat = None
limit_max_bytes = None
for line in output.splitlines():
parsed = line.split(" ")
if "STAT" == parsed[0]:
metric = parsed[1]
if metric in self.absolute_values:
self.results_builder.add_absolute_result(PluginMeasurement(key=parsed[1]+'2', value=parsed[2]))
if metric == 'bytes':
bytes_stat = float(parsed[2])
elif metric == 'limit_maxbytes':
limit_max_bytes = float(parsed[2])
elif metric in self.per_second_values:
self.results_builder.add_per_second_result(PluginMeasurement(key=parsed[1]+'2', value=parsed[2]))
if bytes_stat is not None and limit_max_bytes is not None:
self.results_builder.add_absolute_result(PluginMeasurement(
key='cache_usage2',
value=int(100*(bytes_stat/limit_max_bytes))
))
except ConnectionRefusedError as ex:
raise ConfigException('No connection to port "%s"' % self.port) from ex
except socket.timeout as ex:
raise ConfigException('Socket timed out while getting data') from ex