{"action":"created","comment":{"url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/comments/25488331","id":25488331,"diff_hunk":"@@ -0,0 +1,591 @@\n+#!/usr/bin/python\n+# Copyright 2014 Google Inc. All rights reserved.\n+#\n+# Licensed under the Apache License, Version 2.0 (the \"License\");\n+# you may not use this file except in compliance with the License.\n+# You may obtain a copy of the License at\n+#\n+#     http://www.apache.org/licenses/LICENSE-2.0\n+#\n+# Unless required by applicable law or agreed to in writing, software\n+# distributed under the License is distributed on an \"AS IS\" BASIS,\n+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+# See the License for the specific language governing permissions and\n+# limitations under the License.\n+\"\"\"Summary:\n+  mlab_export.py serializes RRD data collected by the M-Lab, collectd plugin.\n+\n+  mlab_export.py supports many options for use by a human operator. However,\n+  typical usage should be automated using something like crond. Because typical\n+  usage is automated, all parameters have sensible defaults, tuned to running on\n+  M-Lab.\n+\n+Overview of default operation:\n+  * Script finds all RRD files under --rrddir_prefix.\n+  * Script opens a file for output under --output_dir (optionally compressed).\n+  * Script reads values from each RRD between --ts_start and --ts_end.\n+  * Script determines ts_start and ts_end from 'mtime' on LAST_EXPORT_FILENAME.\n+    - On first run, LAST_EXPORT_FILENAME is created. ts_end is set to nearest\n+      hour from current time, and ts_start is set to ts_end minus --length.\n+    - On later runs, ts_start is set to the 'mtime' of LAST_EXPORT_FILENAME.\n+      And, ts_end is set to the most recent hour.\n+  * Script only exports metrics defined in the export_metrics.conf config file.\n+  * On success, script sets mtime on LAST_EXPORT_FILENAME to ts_end, in\n+    preparation for the next run.\n+\n+Examples:\n+  # Default operation should not require additional parameters.\n+  ./mlab-export.py\n+\n+  # Useful for testing, --noupdate exports the last hour without the\n+  # side-effects of modifying the mtime of LAST_EXPORT_FILENAME.\n+  ./mlab-export.py --noupdate --output example.json\n+\n+  # To export a different set of metrics than the global default.\n+  ./mlab-export.py --noupdate --output example.json --export_metrics metrics.cfg\n+\n+  # List rrd file names, the raw metric names, or the canonical metric names.\n+  ./mlab-export.py --show_rrdfile\n+  ./mlab-export.py --show_rawmetric --show_metric\n+\n+  # Show the collectd-nagios command line for checking each metric.\n+  ./mlab-export.py --show_nagios\n+\"\"\"\n+import ConfigParser\n+import fcntl\n+import gzip\n+import json\n+import logging\n+import os\n+import socket\n+import sys\n+import time\n+\n+# Third-party modules.\n+import gflags as flags\n+import rrdtool\n+\n+\n+COLLECTD_INTERVAL = 10\n+EXPORT_DIR = '/var/spool/mlab_utility'\n+HOSTNAME = None\n+LAST_EXPORT_FILENAME = '/var/lib/collectd/lastexport.tstamp'\n+LIST_OPTIONS = ('rrdfile', 'metric', 'metric_raw', 'metric_skipped')\n+METRIC_MAP = None\n+METRIC_MAP_CONF = '/usr/share/collectd-mlab/export_metrics.conf'\n+RRD_PREFIX = '/var/lib/collectd/rrd/'\n+\n+flags.DEFINE_string(\n+    'rrddir_prefix', RRD_PREFIX, 'Root directory of RRD files to export.')\n+flags.DEFINE_integer(\n+    'length', 3600, 'Length of time to export in seconds.', lower_bound=0)\n+flags.DEFINE_integer(\n+    'step', COLLECTD_INTERVAL, 'Time between RRD values in seconds. This value '\n+    'must equal the value in the collectd config. Inaccurate values will not '\n+    'change the intervals of exported data, but could result in samples being '\n+    'skipped at the beginning or end of the export window.', lower_bound=1)\n+flags.DEFINE_integer(\n+    'ts_start', None,\n+    'Timestamp to start export, in seconds since the epoch.', lower_bound=0)\n+flags.DEFINE_integer(\n+    'ts_end', None,\n+    'Timestamp to end export, in seconds since the epoch.', lower_bound=0)\n+flags.DEFINE_integer(\n+    'ts_offset', 900, 'Amount of time (seconds) that must have past after '\n+    'ts_end to ensure that values cached by collectd have been flushed to disk '\n+    'before attempting an export.', lower_bound=0)\n+flags.DEFINE_multistring(\n+    'ignored_experiments', [], 'List of experiment names to ignore. Experiment '\n+    'must be in \"slice.site\" form not \"site_slice\".')\n+flags.DEFINE_bool(\n+    'pretty_json', None, 'Add extra indenting to json output (for debugging).')\n+flags.DEFINE_string(\n+    'output_dir', EXPORT_DIR, 'Root directory of json output files.')\n+flags.DEFINE_string(\n+    'output', None, 'Name of json output file. Set automatically if not given.')\n+flags.DEFINE_string(\n+    'export_metrics', METRIC_MAP_CONF, 'File name with metric map. The metric '\n+    'map defines canonical metric names for raw, metric names taken from '\n+    'collectd RRD files.')\n+flags.DEFINE_bool(\n+    'verbose', False, 'Increase verbosity level.')\n+flags.DEFINE_bool(\n+    'show_nagios', False, 'Shows collectd-nagios commands to monitor metrics.')\n+flags.DEFINE_bool(\n+    'show_rrdfile', False, 'Shows the RRD files opened during export.')\n+flags.DEFINE_bool(\n+    'show_metric', False, 'Shows the canonical metric names during export.')\n+flags.DEFINE_bool(\n+    'show_rawmetric', False, 'Shows the raw metric name before translating to '\n+    'the canonical name. This can be helpful to know before adding new metrics '\n+    'to the export metrics configuration file.')\n+flags.DEFINE_bool(\n+    'show_skipped', False, 'Shows the raw metric names that are not exported. '\n+    'This option may be helpful when adding new metrics to the export_metrics.')\n+flags.DEFINE_bool(\n+    'update', True, 'Update timestamps on successful export. Update is always '\n+    'disabled when any --show_* option is enabled.')\n+flags.DEFINE_bool(\n+    'compress', False,\n+    'Compresses output and adds .gz extension to output filename.')\n+\n+\n+class Error(Exception):\n+  \"\"\"Base error type for this file.\"\"\"\n+\n+\n+class TimeOptionError(Error):\n+  \"\"\"An error related to export times or ranges.\"\"\"\n+\n+\n+def init_global():\n+  global HOSTNAME\n+  # NOTE: This should be the hostname of root context, not slice context.\n+  HOSTNAME = socket.gethostname()\n+  logging.basicConfig(format='%(message)s', level=logging.INFO)\n+\n+\n+class LastTime(object):\n+  \"\"\"Manages interactions with export timestamp.\"\"\"\n+\n+  def __init__(self, filename):\n+    self._filename = filename\n+    self._lock_handle = None\n+    self._first_creation = not os.path.exists(self._filename)\n+\n+  def __enter__(self):\n+    self.open()\n+    return self\n+\n+  def __exit__(self, *unused_args):\n+    self.close()\n+\n+  def open(self):\n+    \"\"\"Opens the timestamp file, and creates the file if it does not exist.\"\"\"\n+    if self._first_creation:\n+      # Since the file does not exist, use mode 'w' to create it.\n+      self._lock_handle = open(self._filename, 'w')\n+    else:\n+      # File *does* exist, so use mode 'r' to preserve the mtime on the file.\n+      self._lock_handle = open(self._filename, 'r')\n+\n+  def close(self):\n+    \"\"\"Closes the timestamp file.\"\"\"\n+    if self._lock_handle is not None:\n+      self._lock_handle.close()\n+\n+  def get_mtime(self):\n+    \"\"\"Returns the file mtime, or zero if file was just created.\"\"\"\n+    if self._first_creation:\n+      # Indicate that there is no timestamp, so the caller can use a default.\n+      return 0\n+    return int(os.stat(self._filename).st_mtime)\n+\n+  def update_mtime(self, mtime):\n+    \"\"\"Updates the atime & mtime on the export timestamp file.\"\"\"\n+    os.utime(self._filename, (mtime, mtime))\n+\n+  def lock(self):\n+    \"\"\"Attempt to acquire advisory lock on export timestamp file.\n+\n+    This method returns immediatly.\n+\n+    Returns:\n+      bool, True if the lock is acquired, False otherwise.\n+    \"\"\"\n+    # Attempt an exclusive lock without blocking. On success, fcntl.flock()\n+    # returns no value. On error, it throws IOError EAGAIN \"temporarily\n+    # unavailable.\"\n+    try:\n+      fcntl.flock(self._lock_handle, fcntl.LOCK_EX|fcntl.LOCK_NB)\n+      return True\n+    except IOError:\n+      return False\n+\n+\n+def align_timestamp(timestamp, step):\n+  \"\"\"Adjusts 'timestamp' down to a multiple of 'step' size.\n+\n+  Args:\n+    timestamp: int, timestamp in seconds since the epoch.\n+    step: int, interval to align.\n+  Returns:\n+    int, timestamp adjusted to multiple of step.\n+  \"\"\"\n+  return timestamp - (timestamp % step)\n+\n+\n+def default_end_time(options):\n+  \"\"\"Calculates a default, end timestamp.\n+\n+  Args:\n+    options: flags.FlagValues, the runtime options. These values are read:\n+        options.length, options.step, options.update, options.ts_offset.\n+  Returns:\n+    int, timestamp in seconds since the epoch.\n+  \"\"\"\n+  ts_current = int(time.time())\n+\n+  # Align current ts to a multiple of length.\n+  ts_end = align_timestamp(ts_current, options.length)\n+\n+  # Align end ts to a multiple of step size (just in case).\n+  ts_end = align_timestamp(ts_end, options.step)\n+\n+  if options.update and (ts_current - ts_end) < options.ts_offset:\n+    raise TimeOptionError('ts_current - ts_end < min offset: %s - %s < %s' %\n+        (ts_current, ts_end, options.ts_offset))\n+  return ts_end\n+\n+\n+def default_start_time(options, ts_previous):\n+  \"\"\"Calculates a default start timestamp.\n+\n+  Args:\n+    options: flags.FlagValues, the runtime options. These values are read:\n+        options.length, options.step, options.update, options.ts_end.\n+    ts_previous: int, timestamp in seconds since epoch of last\n+        successful export. On first run, this value should be zero.\n+  Returns:\n+    int, timestamp in seconds since the epoch.\n+  \"\"\"\n+  # Only when both options.update and ts_previous are set, skip assignment.\n+  if not (options.update and ts_previous):\n+    # If either update or ts_previous were not set, then choose a default.\n+    ts_previous = options.ts_end - options.length\n+\n+  return align_timestamp(ts_previous, options.step)\n+\n+\n+def assert_start_and_end_times(options):\n+  \"\"\"Performs a sanity check on start and end timestamps.\n+\n+  This method asserts that both ts_end is less than ts_start and that the\n+  difference between them is greater than options.length.\n+\n+  Args:\n+    options: flags.FlagValues, the runtime options. These values are read:\n+        options.length, options.ts_start, options.ts_end.\n+  Raises:\n+    TimeOptionError, if a start & end time constraint is violated.\n+  \"\"\"\n+  # Always check if basic constratins are respected.\n+  if options.ts_end < options.ts_start:\n+    raise TimeOptionError('Start time cannot be later than end time.')\n+\n+  if options.ts_end - options.ts_start < options.length:\n+    msg = ('Start and end times difference is less than length: '+\n+           '%s - %s = %s < %s' % (options.ts_end, options.ts_start,\n+           (options.ts_end - options.ts_start), options.length))\n+    raise TimeOptionError(msg)\n+\n+  logging.debug('Exporting: %s to %s', time.ctime(options.ts_start),\n+                time.ctime(options.ts_end))\n+\n+\n+def default_output_name(ts_start, ts_end, output_dir):\n+  \"\"\"Creates a default output filename based on time range and output dir.\n+\n+  Filenames are formatted with time stamps as:\n+      <output_dir>/resource-utilization/YYYY/MM/DD/<HOSTNAME>/\n+          metrics-YYYYMMDDTHHMMSS-to-YYYYMMDDTHHMMSS.json\n+\n+  Args:\n+    ts_start: int, starting timestamp of export in seconds since the epoch.\n+    ts_end: int, ending timestamp of export in seconds since the epoch.\n+    output_dir: str, base path of directory for output.\n+  Returns:\n+    str, absolute path of generated output file name.\n+  \"\"\"\n+  filename = 'metrics-%s-to-%s.json' % (\n+      time.strftime('%Y%m%dT%H%M%S', time.localtime(ts_start)),\n+      time.strftime('%Y%m%dT%H%M%S', time.localtime(ts_end)))\n+  date_path = time.strftime('%Y/%m/%d', time.localtime(ts_start))\n+  full_path = os.path.join(\n+      output_dir, 'resource-utilization', date_path, HOSTNAME)\n+  if not os.path.exists(full_path):\n+    os.makedirs(full_path)\n+  return os.path.join(full_path, filename)\n+\n+\n+def get_canonical_names(filename, value_name, options):\n+  \"\"\"Converts raw filename and value names from RRD into canonical export names.\n+\n+  Args:\n+    filename: str, the absolute path of an rrd file.\n+    value_name: str, the name of the value being exported from the RRD.\n+    options: flags.FlagValues, the runtime options. This method uses\n+        options.rrddir_prefix and all option.show_* flags.\n+  Returns:\n+    (str, str, str), with HOSTNAME, experiment name, metric name.\n+  \"\"\"\n+  # Strip rrddir_prefix and split directory components.\n+  file_fields = filename.replace(options.rrddir_prefix, '').split('/')\n+\n+  # The zeroth field is always the context hostname.\n+  if HOSTNAME == file_fields[0]:\n+    # The root context represents whole-system metrics.\n+    experiment = 'system'\n+  else:\n+    # A slice hostname. Everything remaining after stripping hostname.\n+    experiment = file_fields[0].replace('.' + HOSTNAME, '')\n+\n+  raw_fields = file_fields[1:-1]\n+  raw_fields.append(file_fields[-1].replace('.rrd', ''))\n+  if value_name != 'value':\n+    raw_fields.append(value_name)\n+\n+  metric_raw = '.'.join(raw_fields)\n+\n+  # NOTE: convert the raw metric name into the canonical form, or None.\n+  metric = METRIC_MAP.get(metric_raw, None)\n+\n+  # Optionally print extra information.\n+  if options.show_nagios:\n+    cmd = ('collectd-nagios -s $COLLECTD_UNIXSOCK -H {host} '+\n+           '-n {metric} -d {value} [-w <l:h> -c <l:h>]')\n+    cmd = cmd.format(host=file_fields[0],\n+                     metric='/'.join(file_fields[1:]).replace('.rrd', ''),\n+                     value=value_name)\n+    logging.info(cmd)\n+  if options.show_rrdfile:\n+    logging.info(\"rrdfile: %s\", filename.replace(options.rrddir_prefix, ''))\n+  if options.show_rawmetric:\n+    logging.info(\"metric_raw: %s\", metric_raw)\n+  if options.show_skipped and not metric:\n+    logging.info(\"metric_skipped: %s\", metric_raw)\n+  if options.show_metric and metric:\n+    logging.info(\"metric: %s\", metric)\n+\n+  return (HOSTNAME, experiment, metric)\n+\n+\n+def get_json_record(hostname, experiment, metric, timestamps, values):\n+  \"\"\"Creates a dict suitable for export to json.\n+\n+  Args:\n+    hostname, str, hostname of host system.\n+    experiment: str, name of experiment running on host.\n+    metric: str, the canonical metric name for values.\n+    timestamps: iterable of int, timestamps corresponding to each value.\n+    values: iterable of float, values corresponding to each timestamp.\n+  Returns:\n+    dict, with keys for hostname, experiment, metric, and sample.\n+  \"\"\"\n+  logging.debug('%s %s %s', hostname, experiment, metric)\n+  json_data = {\n+      'hostname': hostname, 'experiment': experiment, 'metric': metric}\n+  json_data['sample'] = get_json_record_samples(timestamps, values)\n+  return json_data\n+\n+\n+def get_json_record_samples(timestamps, values):\n+  \"\"\"Converts a sequences of timestampes and values for a json record.\n+\n+  The timestamps and values arguments must be the same length.\n+\n+  Args:\n+    timestamps: iterable of int, timestamps corresponding to each value.\n+    values: iterable of float, values corresponding to each timestamp.\n+  Returns:\n+    list of dict, each dict has keys timestamp and value.\n+  \"\"\"\n+  samples = []\n+  assert(len(timestamps) == len(values))\n+  for i in xrange(0, len(timestamps)):","path":"export/mlab_export.py","position":null,"original_position":395,"commit_id":"96f7dba0b6601c03932cad18cf9c0a57b0c243df","original_commit_id":"273817a05851f12114fa4e66d07f07094e7cdf0c","user":{"login":"stephen-soltesz","id":1085316,"avatar_url":"https://avatars.githubusercontent.com/u/1085316?v=3","gravatar_id":"","url":"https://api.github.com/users/stephen-soltesz","html_url":"https://github.com/stephen-soltesz","followers_url":"https://api.github.com/users/stephen-soltesz/followers","following_url":"https://api.github.com/users/stephen-soltesz/following{/other_user}","gists_url":"https://api.github.com/users/stephen-soltesz/gists{/gist_id}","starred_url":"https://api.github.com/users/stephen-soltesz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephen-soltesz/subscriptions","organizations_url":"https://api.github.com/users/stephen-soltesz/orgs","repos_url":"https://api.github.com/users/stephen-soltesz/repos","events_url":"https://api.github.com/users/stephen-soltesz/events{/privacy}","received_events_url":"https://api.github.com/users/stephen-soltesz/received_events","type":"User","site_admin":false},"body":"Oh, I did notice that the initial `0` here is unnecessary for `range` and `xrange`. bc8ea7274f39e223b0199a280b1f9ec3ab236095","created_at":"2015-02-27T05:20:36Z","updated_at":"2015-02-27T05:20:36Z","html_url":"https://github.com/m-lab/collectd-mlab/pull/7#discussion_r25488331","pull_request_url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7","_links":{"self":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/comments/25488331"},"html":{"href":"https://github.com/m-lab/collectd-mlab/pull/7#discussion_r25488331"},"pull_request":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7"}}},"pull_request":{"url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7","id":27999467,"html_url":"https://github.com/m-lab/collectd-mlab/pull/7","diff_url":"https://github.com/m-lab/collectd-mlab/pull/7.diff","patch_url":"https://github.com/m-lab/collectd-mlab/pull/7.patch","issue_url":"https://api.github.com/repos/m-lab/collectd-mlab/issues/7","number":7,"state":"open","locked":false,"title":"Adds mlab_export.py for serializing rrd data.","user":{"login":"stephen-soltesz","id":1085316,"avatar_url":"https://avatars.githubusercontent.com/u/1085316?v=3","gravatar_id":"","url":"https://api.github.com/users/stephen-soltesz","html_url":"https://github.com/stephen-soltesz","followers_url":"https://api.github.com/users/stephen-soltesz/followers","following_url":"https://api.github.com/users/stephen-soltesz/following{/other_user}","gists_url":"https://api.github.com/users/stephen-soltesz/gists{/gist_id}","starred_url":"https://api.github.com/users/stephen-soltesz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephen-soltesz/subscriptions","organizations_url":"https://api.github.com/users/stephen-soltesz/orgs","repos_url":"https://api.github.com/users/stephen-soltesz/repos","events_url":"https://api.github.com/users/stephen-soltesz/events{/privacy}","received_events_url":"https://api.github.com/users/stephen-soltesz/received_events","type":"User","site_admin":false},"body":"This change adds an export script that would be run periodically to\r\nserialize portions of the RRD data for collection by the data collection\r\npipeline.","created_at":"2015-01-25T22:39:21Z","updated_at":"2015-02-27T05:20:36Z","closed_at":null,"merged_at":null,"merge_commit_sha":"57c4012ebb0473830e8bced70aeb7c9d9329d1ee","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7/commits","review_comments_url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7/comments","review_comment_url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/comments{/number}","comments_url":"https://api.github.com/repos/m-lab/collectd-mlab/issues/7/comments","statuses_url":"https://api.github.com/repos/m-lab/collectd-mlab/statuses/96f7dba0b6601c03932cad18cf9c0a57b0c243df","head":{"label":"stephen-soltesz:review-export-scripts","ref":"review-export-scripts","sha":"96f7dba0b6601c03932cad18cf9c0a57b0c243df","user":{"login":"stephen-soltesz","id":1085316,"avatar_url":"https://avatars.githubusercontent.com/u/1085316?v=3","gravatar_id":"","url":"https://api.github.com/users/stephen-soltesz","html_url":"https://github.com/stephen-soltesz","followers_url":"https://api.github.com/users/stephen-soltesz/followers","following_url":"https://api.github.com/users/stephen-soltesz/following{/other_user}","gists_url":"https://api.github.com/users/stephen-soltesz/gists{/gist_id}","starred_url":"https://api.github.com/users/stephen-soltesz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephen-soltesz/subscriptions","organizations_url":"https://api.github.com/users/stephen-soltesz/orgs","repos_url":"https://api.github.com/users/stephen-soltesz/repos","events_url":"https://api.github.com/users/stephen-soltesz/events{/privacy}","received_events_url":"https://api.github.com/users/stephen-soltesz/received_events","type":"User","site_admin":false},"repo":{"id":26023666,"name":"collectd-mlab","full_name":"stephen-soltesz/collectd-mlab","owner":{"login":"stephen-soltesz","id":1085316,"avatar_url":"https://avatars.githubusercontent.com/u/1085316?v=3","gravatar_id":"","url":"https://api.github.com/users/stephen-soltesz","html_url":"https://github.com/stephen-soltesz","followers_url":"https://api.github.com/users/stephen-soltesz/followers","following_url":"https://api.github.com/users/stephen-soltesz/following{/other_user}","gists_url":"https://api.github.com/users/stephen-soltesz/gists{/gist_id}","starred_url":"https://api.github.com/users/stephen-soltesz/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephen-soltesz/subscriptions","organizations_url":"https://api.github.com/users/stephen-soltesz/orgs","repos_url":"https://api.github.com/users/stephen-soltesz/repos","events_url":"https://api.github.com/users/stephen-soltesz/events{/privacy}","received_events_url":"https://api.github.com/users/stephen-soltesz/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/stephen-soltesz/collectd-mlab","description":"Collectd plugin and configuration for Measurement Lab servers.","fork":true,"url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab","forks_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/forks","keys_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/teams","hooks_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/hooks","issue_events_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/issues/events{/number}","events_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/events","assignees_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/assignees{/user}","branches_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/branches{/branch}","tags_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/tags","blobs_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/git/refs{/sha}","trees_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/statuses/{sha}","languages_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/languages","stargazers_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/stargazers","contributors_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/contributors","subscribers_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/subscribers","subscription_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/subscription","commits_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/commits{/sha}","git_commits_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/git/commits{/sha}","comments_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/comments{/number}","issue_comment_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/issues/comments{/number}","contents_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/contents/{+path}","compare_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/merges","archive_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/downloads","issues_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/issues{/number}","pulls_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/pulls{/number}","milestones_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/milestones{/number}","notifications_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/labels{/name}","releases_url":"https://api.github.com/repos/stephen-soltesz/collectd-mlab/releases{/id}","created_at":"2014-10-31T16:22:44Z","updated_at":"2015-01-23T02:20:49Z","pushed_at":"2015-02-27T04:41:11Z","git_url":"git://github.com/stephen-soltesz/collectd-mlab.git","ssh_url":"git@github.com:stephen-soltesz/collectd-mlab.git","clone_url":"https://github.com/stephen-soltesz/collectd-mlab.git","svn_url":"https://github.com/stephen-soltesz/collectd-mlab","homepage":null,"size":370,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"m-lab:master","ref":"master","sha":"5a730295bbbfc097fa42d809ac8743756fad7801","user":{"login":"m-lab","id":3496264,"avatar_url":"https://avatars.githubusercontent.com/u/3496264?v=3","gravatar_id":"","url":"https://api.github.com/users/m-lab","html_url":"https://github.com/m-lab","followers_url":"https://api.github.com/users/m-lab/followers","following_url":"https://api.github.com/users/m-lab/following{/other_user}","gists_url":"https://api.github.com/users/m-lab/gists{/gist_id}","starred_url":"https://api.github.com/users/m-lab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m-lab/subscriptions","organizations_url":"https://api.github.com/users/m-lab/orgs","repos_url":"https://api.github.com/users/m-lab/repos","events_url":"https://api.github.com/users/m-lab/events{/privacy}","received_events_url":"https://api.github.com/users/m-lab/received_events","type":"Organization","site_admin":false},"repo":{"id":26023658,"name":"collectd-mlab","full_name":"m-lab/collectd-mlab","owner":{"login":"m-lab","id":3496264,"avatar_url":"https://avatars.githubusercontent.com/u/3496264?v=3","gravatar_id":"","url":"https://api.github.com/users/m-lab","html_url":"https://github.com/m-lab","followers_url":"https://api.github.com/users/m-lab/followers","following_url":"https://api.github.com/users/m-lab/following{/other_user}","gists_url":"https://api.github.com/users/m-lab/gists{/gist_id}","starred_url":"https://api.github.com/users/m-lab/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/m-lab/subscriptions","organizations_url":"https://api.github.com/users/m-lab/orgs","repos_url":"https://api.github.com/users/m-lab/repos","events_url":"https://api.github.com/users/m-lab/events{/privacy}","received_events_url":"https://api.github.com/users/m-lab/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/m-lab/collectd-mlab","description":"Collectd plugin and configuration for Measurement Lab servers.","fork":false,"url":"https://api.github.com/repos/m-lab/collectd-mlab","forks_url":"https://api.github.com/repos/m-lab/collectd-mlab/forks","keys_url":"https://api.github.com/repos/m-lab/collectd-mlab/keys{/key_id}","collaborators_url":"https://api.github.com/repos/m-lab/collectd-mlab/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/m-lab/collectd-mlab/teams","hooks_url":"https://api.github.com/repos/m-lab/collectd-mlab/hooks","issue_events_url":"https://api.github.com/repos/m-lab/collectd-mlab/issues/events{/number}","events_url":"https://api.github.com/repos/m-lab/collectd-mlab/events","assignees_url":"https://api.github.com/repos/m-lab/collectd-mlab/assignees{/user}","branches_url":"https://api.github.com/repos/m-lab/collectd-mlab/branches{/branch}","tags_url":"https://api.github.com/repos/m-lab/collectd-mlab/tags","blobs_url":"https://api.github.com/repos/m-lab/collectd-mlab/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/m-lab/collectd-mlab/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/m-lab/collectd-mlab/git/refs{/sha}","trees_url":"https://api.github.com/repos/m-lab/collectd-mlab/git/trees{/sha}","statuses_url":"https://api.github.com/repos/m-lab/collectd-mlab/statuses/{sha}","languages_url":"https://api.github.com/repos/m-lab/collectd-mlab/languages","stargazers_url":"https://api.github.com/repos/m-lab/collectd-mlab/stargazers","contributors_url":"https://api.github.com/repos/m-lab/collectd-mlab/contributors","subscribers_url":"https://api.github.com/repos/m-lab/collectd-mlab/subscribers","subscription_url":"https://api.github.com/repos/m-lab/collectd-mlab/subscription","commits_url":"https://api.github.com/repos/m-lab/collectd-mlab/commits{/sha}","git_commits_url":"https://api.github.com/repos/m-lab/collectd-mlab/git/commits{/sha}","comments_url":"https://api.github.com/repos/m-lab/collectd-mlab/comments{/number}","issue_comment_url":"https://api.github.com/repos/m-lab/collectd-mlab/issues/comments{/number}","contents_url":"https://api.github.com/repos/m-lab/collectd-mlab/contents/{+path}","compare_url":"https://api.github.com/repos/m-lab/collectd-mlab/compare/{base}...{head}","merges_url":"https://api.github.com/repos/m-lab/collectd-mlab/merges","archive_url":"https://api.github.com/repos/m-lab/collectd-mlab/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/m-lab/collectd-mlab/downloads","issues_url":"https://api.github.com/repos/m-lab/collectd-mlab/issues{/number}","pulls_url":"https://api.github.com/repos/m-lab/collectd-mlab/pulls{/number}","milestones_url":"https://api.github.com/repos/m-lab/collectd-mlab/milestones{/number}","notifications_url":"https://api.github.com/repos/m-lab/collectd-mlab/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/m-lab/collectd-mlab/labels{/name}","releases_url":"https://api.github.com/repos/m-lab/collectd-mlab/releases{/id}","created_at":"2014-10-31T16:22:28Z","updated_at":"2015-02-09T01:40:48Z","pushed_at":"2015-02-09T01:40:46Z","git_url":"git://github.com/m-lab/collectd-mlab.git","ssh_url":"git@github.com:m-lab/collectd-mlab.git","clone_url":"https://github.com/m-lab/collectd-mlab.git","svn_url":"https://github.com/m-lab/collectd-mlab","homepage":null,"size":376,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1,"mirror_url":null,"open_issues_count":4,"forks":1,"open_issues":4,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7"},"html":{"href":"https://github.com/m-lab/collectd-mlab/pull/7"},"issue":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/issues/7"},"comments":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/issues/7/comments"},"review_comments":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7/comments"},"review_comment":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/pulls/7/commits"},"statuses":{"href":"https://api.github.com/repos/m-lab/collectd-mlab/statuses/96f7dba0b6601c03932cad18cf9c0a57b0c243df"}}}}