# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import subprocess import xml.parsers.expat # python 2.4 compatible from monasca_agent.collector.checks import AgentCheck class Varnish(AgentCheck): # XML parsing bits, a.k.a. Kafka in Code def _reset(self): self._current_element = "" self._current_metric = "varnish" self._current_value = 0 self._current_str = "" self._current_type = "" def _start_element(self, name, attrs): self._current_element = name def _end_element(self, name): if name == "stat": m_name = self.normalize(self._current_metric) if self._current_type in ("a", "c"): self.rate(m_name, int(self._current_value)) elif self._current_type in ("i", "g"): self.gauge(m_name, int(self._current_value)) else: # Unsupported data type, ignore self._reset() return # don't save # reset for next stat element self._reset() elif name in ("type", "ident", "name"): self._current_metric += "." + self._current_str def _char_data(self, data): self.log.debug("Data %s [%s]" % (data, self._current_element)) data = data.strip() if len(data) > 0 and self._current_element != "": if self._current_element == "value": self._current_value = int(data) elif self._current_element == "flag": self._current_type = data else: self._current_str = data def check(self, instance): """Extract stats from varnishstat -x The text option (-1) is not reliable enough when counters get large. VBE.media_video_prd_services_01(10.93.67.16,,8080).happy18446744073709551615 2 types of data, "a" for counter ("c" in newer versions of varnish), "i" for gauge ("g") https://github.com/varnish/Varnish-Cache/blob/master/include/tbl/vsc_fields.h Bitmaps are not supported. fetch_304 0 a Fetch no body (304) n_sess_mem 334 i N struct sess_mem LCK vcl creat 1 a Created locks """ # Not configured? Not a problem. if instance.get("varnishstat", None) is None: raise Exception("varnishstat is not configured") dimensions = self._set_dimensions(None, instance) name = instance.get('name') # Get the varnish version from varnishstat output, error = subprocess.Popen([instance.get("varnishstat"), "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() # Assumptions regarding varnish's version use_xml = True arg = "-x" # varnishstat argument version = 3 m1 = re.search(r"varnish-(\d+)", output, re.MULTILINE) # v2 prints the version on stderr, v3 on stdout m2 = re.search(r"varnish-(\d+)", error, re.MULTILINE) if m1 is None and m2 is None: self.log.warn("Cannot determine the version of varnishstat, assuming 3 or greater") self.log.warn("Cannot determine the version of varnishstat, assuming 3 or greater") else: if m1 is not None: version = int(m1.group(1)) elif m2 is not None: version = int(m2.group(1)) self.log.debug("Varnish version: %d" % version) # Location of varnishstat if version <= 2: use_xml = False arg = "-1" cmd = [instance.get("varnishstat"), arg] if name is not None: cmd.extend(['-n', name]) dimensions.update({'varnish_name': name}) else: dimensions.update({'varnish_name': 'default'}) try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) output, error = proc.communicate() except Exception: self.log.error(u"Failed to run %s" % repr(cmd)) raise if error and len(error) > 0: self.log.error(error) self._parse_varnishstat(output, use_xml, dimensions) def _parse_varnishstat(self, output, use_xml, dimensions): if use_xml: p = xml.parsers.expat.ParserCreate() p.StartElementHandler = self._start_element p.EndElementHandler = self._end_element p.CharacterDataHandler = self._char_data self._reset() p.Parse(output, True) else: for line in output.split("\n"): self.log.debug("Parsing varnish results: %s" % line) fields = line.split() if len(fields) < 3: break name, gauge_val, rate_val = fields[0], fields[1], fields[2] metric_name = self.normalize(name, prefix="varnish") # Now figure out which value to pick if rate_val.lower() in ("nan", "."): # col 2 matters self.log.debug("Varnish (gauge) %s %d" % (metric_name, int(gauge_val))) self.gauge(metric_name, int(gauge_val), dimensions=dimensions) else: # col 3 has a rate (since restart) self.log.debug("Varnish (rate) %s %d" % (metric_name, int(gauge_val))) self.rate(metric_name, float(gauge_val), dimensions=dimensions)