252 lines
9.2 KiB
Python
252 lines
9.2 KiB
Python
import httplib
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
import urllib2
|
|
import urllib
|
|
from urlparse import urlsplit
|
|
|
|
from monagent.collector.checks import AgentCheck
|
|
|
|
|
|
DEFAULT_MAX_CONTAINERS = 20
|
|
|
|
LXC_METRICS = [
|
|
{
|
|
"cgroup": "memory",
|
|
"file": "lxc/%s/memory.stat",
|
|
"metrics": {
|
|
"active_anon": ("docker.mem.active_anon", "gauge"),
|
|
"active_file": ("docker.mem.active_file", "gauge"),
|
|
"cache": ("docker.mem.cache", "gauge"),
|
|
"hierarchical_memory_limit": ("docker.mem.hierarchical_memory_limit", "gauge"),
|
|
"hierarchical_memsw_limit": ("docker.mem.hierarchical_memsw_limit", "gauge"),
|
|
"inactive_anon": ("docker.mem.inactive_anon", "gauge"),
|
|
"inactive_file": ("docker.mem.inactive_file", "gauge"),
|
|
"mapped_file": ("docker.mem.mapped_file", "gauge"),
|
|
"pgfault": ("docker.mem.pgfault", "gauge"),
|
|
"pgmajfault": ("docker.mem.pgmajfault", "gauge"),
|
|
"pgpgin": ("docker.mem.pgpgin", "gauge"),
|
|
"pgpgout": ("docker.mem.pgpgout", "gauge"),
|
|
"rss": ("docker.mem.rss", "gauge"),
|
|
"swap": ("docker.mem.swap", "gauge"),
|
|
"unevictable": ("docker.mem.unevictable", "gauge"),
|
|
"total_active_anon": ("docker.mem.total_active_anon", "gauge"),
|
|
"total_active_file": ("docker.mem.total_active_file", "gauge"),
|
|
"total_cache": ("docker.mem.total_cache", "gauge"),
|
|
"total_inactive_anon": ("docker.mem.total_inactive_anon", "gauge"),
|
|
"total_inactive_file": ("docker.mem.total_inactive_file", "gauge"),
|
|
"total_mapped_file": ("docker.mem.total_mapped_file", "gauge"),
|
|
"total_pgfault": ("docker.mem.total_pgfault", "gauge"),
|
|
"total_pgmajfault": ("docker.mem.total_pgmajfault", "gauge"),
|
|
"total_pgpgin": ("docker.mem.total_pgpgin", "gauge"),
|
|
"total_pgpgout": ("docker.mem.total_pgpgout", "gauge"),
|
|
"total_rss": ("docker.mem.total_rss", "gauge"),
|
|
"total_swap": ("docker.mem.total_swap", "gauge"),
|
|
"total_unevictable": ("docker.mem.total_unevictable", "gauge"),
|
|
}
|
|
},
|
|
{
|
|
"cgroup": "cpuacct",
|
|
"file": "lxc/%s/cpuacct.stat",
|
|
"metrics": {
|
|
"user": ("docker.cpu.user", "gauge"),
|
|
"system": ("docker.cpu.system", "gauge"),
|
|
},
|
|
},
|
|
]
|
|
|
|
DOCKER_METRICS = {
|
|
"SizeRw": ("docker.disk.size", "gauge"),
|
|
}
|
|
|
|
DOCKER_TAGS = [
|
|
"Command",
|
|
"Image",
|
|
]
|
|
|
|
|
|
class UnixHTTPConnection(httplib.HTTPConnection, object):
|
|
|
|
"""Class used in conjuction with UnixSocketHandler to make urllib2
|
|
|
|
compatible with Unix sockets.
|
|
"""
|
|
|
|
def __init__(self, unix_socket):
|
|
self._unix_socket = unix_socket
|
|
|
|
def connect(self):
|
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
sock.connect(self._unix_socket)
|
|
self.sock = sock
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
httplib.HTTPConnection.__init__(self, *args, **kwargs)
|
|
return self
|
|
|
|
|
|
class UnixSocketHandler(urllib2.AbstractHTTPHandler):
|
|
|
|
"""Class that makes Unix sockets work with urllib2 without any additional dependencies.
|
|
|
|
"""
|
|
|
|
def unix_open(self, req):
|
|
full_path = "%s%s" % urlsplit(req.get_full_url())[1:3]
|
|
path = os.path.sep
|
|
for part in full_path.split("/"):
|
|
path = os.path.join(path, part)
|
|
if not os.path.exists(path):
|
|
break
|
|
unix_socket = path
|
|
# add a host or else urllib2 complains
|
|
url = req.get_full_url().replace(unix_socket, "/localhost")
|
|
new_req = urllib2.Request(url, req.get_data(), dict(req.header_items()))
|
|
new_req.timeout = req.timeout
|
|
return self.do_open(UnixHTTPConnection(unix_socket), new_req)
|
|
|
|
unix_request = urllib2.AbstractHTTPHandler.do_request_
|
|
|
|
|
|
class Docker(AgentCheck):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(Docker, self).__init__(*args, **kwargs)
|
|
urllib2.install_opener(urllib2.build_opener(UnixSocketHandler()))
|
|
self._mounpoints = {}
|
|
for metric in LXC_METRICS:
|
|
self._mounpoints[metric["cgroup"]] = self._find_cgroup(metric["cgroup"])
|
|
|
|
def check(self, instance):
|
|
dimensions = instance.get("dimensions") or {}
|
|
containers = self._get_containers(instance)
|
|
if not containers:
|
|
self.warning("No containers are running.")
|
|
|
|
max_containers = instance.get('max_containers', DEFAULT_MAX_CONTAINERS)
|
|
|
|
if not instance.get("exclude") or not instance.get("include"):
|
|
if len(containers) > max_containers:
|
|
self.warning(
|
|
"Too many containers to collect. Please refine the containers to collect by editing the configuration file. Truncating to %s containers" %
|
|
max_containers)
|
|
containers = containers[:max_containers]
|
|
|
|
collected_containers = 0
|
|
for container in containers:
|
|
container_dimensions = dimensions.copy()
|
|
container_dimensions['container_names'] = ' '.join(container["Names"])
|
|
container_dimensions['docker_tags'] = ' '.join(DOCKER_TAGS)
|
|
|
|
# Check if the container is included/excluded
|
|
if not self._is_container_included(instance, container["Names"]):
|
|
continue
|
|
|
|
collected_containers += 1
|
|
if collected_containers > max_containers:
|
|
self.warning(
|
|
"Too many containers are matching the current configuration. Some containers will not be collected. Please refine your configuration")
|
|
break
|
|
|
|
for key, (dd_key, metric_type) in DOCKER_METRICS.items():
|
|
if key in container:
|
|
getattr(self, metric_type)(
|
|
dd_key, int(container[key]), dimensions=container_dimensions)
|
|
for metric in LXC_METRICS:
|
|
mountpoint = self._mounpoints[metric["cgroup"]]
|
|
stat_file = os.path.join(mountpoint, metric["file"] % container["Id"])
|
|
stats = self._parse_cgroup_file(stat_file)
|
|
for key, (dd_key, metric_type) in metric["metrics"].items():
|
|
if key in stats:
|
|
getattr(self, metric_type)(
|
|
dd_key, int(stats[key]), dimensions=container_dimensions)
|
|
|
|
@staticmethod
|
|
def _make_tag(key, value):
|
|
return "%s:%s" % (key.lower(), value.strip())
|
|
|
|
@staticmethod
|
|
def _is_container_included(instance, tags):
|
|
def _is_tag_included(tag):
|
|
for exclude_rule in instance.get("exclude") or []:
|
|
if re.match(exclude_rule, tag):
|
|
for include_rule in instance.get("include") or []:
|
|
if re.match(include_rule, tag):
|
|
return True
|
|
return False
|
|
return True
|
|
for tag in tags:
|
|
if _is_tag_included(tag):
|
|
return True
|
|
return False
|
|
|
|
def _get_containers(self, instance):
|
|
"""Gets the list of running containers in Docker.
|
|
|
|
"""
|
|
return self._get_json("%(url)s/containers/json" % instance, params={"size": 1})
|
|
|
|
def _get_container(self, instance, cid):
|
|
"""Get container information from Docker, gived a container Id.
|
|
|
|
"""
|
|
return self._get_json("%s/containers/%s/json" % (instance["url"], cid))
|
|
|
|
def _get_json(self, uri, params=None):
|
|
"""Utility method to get and parse JSON streams.
|
|
|
|
"""
|
|
if params:
|
|
uri = "%s?%s" % (uri, urllib.urlencode(params))
|
|
self.log.debug("Connecting to: %s" % uri)
|
|
req = urllib2.Request(uri, None)
|
|
try:
|
|
request = urllib2.urlopen(req)
|
|
except urllib2.URLError as e:
|
|
if "Errno 13" in str(e):
|
|
raise Exception(
|
|
"Unable to connect to socket. dd-agent user must be part of the 'docker' group")
|
|
raise
|
|
response = request.read()
|
|
return json.loads(response)
|
|
|
|
@staticmethod
|
|
def _find_cgroup(hierarchy):
|
|
"""Finds the mount point for a specified cgroup hierarchy.
|
|
|
|
Works with old style and new style mounts.
|
|
"""
|
|
try:
|
|
fp = open("/proc/mounts")
|
|
mounts = map(lambda x: x.split(), fp.read().splitlines())
|
|
finally:
|
|
fp.close()
|
|
cgroup_mounts = filter(lambda x: x[2] == "cgroup", mounts)
|
|
# Old cgroup style
|
|
if len(cgroup_mounts) == 1:
|
|
return cgroup_mounts[0][1]
|
|
for _, mountpoint, _, opts, _, _ in cgroup_mounts:
|
|
if hierarchy in opts:
|
|
return mountpoint
|
|
|
|
def _parse_cgroup_file(self, file_):
|
|
"""Parses a cgroup pseudo file for key/values.
|
|
|
|
"""
|
|
fp = None
|
|
try:
|
|
self.log.debug("Opening file: %s" % file_)
|
|
try:
|
|
fp = open(file_)
|
|
except IOError:
|
|
raise IOError(
|
|
"Can't open %s. If you are using Docker 0.9.0 or higher, the Datadog agent is not yet compatible with these versions. Please get in touch with Datadog Support for more information" %
|
|
file_)
|
|
return dict(map(lambda x: x.split(), fp.read().splitlines()))
|
|
|
|
finally:
|
|
if fp is not None:
|
|
fp.close()
|