diff --git a/checks/check_status.py b/checks/check_status.py index 65983c647b..b8da8aaa5e 100644 --- a/checks/check_status.py +++ b/checks/check_status.py @@ -161,6 +161,13 @@ def _header_lines(self, indent): lines.append(l) return lines + [""] + def to_dict(self): + return { + 'pid': self.created_by_pid, + 'status_date': "%s (%ss ago)" % (self.created_at.strftime('%Y-%m-%d %H:%M:%S'), + self.created_seconds_ago()), + } + @classmethod def _not_running_message(cls): lines = cls._title_lines() + [ @@ -396,6 +403,55 @@ def body_lines(self): return lines + def to_dict(self): + status_info = AgentStatus.to_dict(self) + + # Hostnames + status_info['hostnames'] = {} + metadata_whitelist = [ + 'hostname', + 'fqdn', + 'ipv4', + 'instance-id' + ] + if self.metadata: + for key, host in self.metadata.items(): + for whitelist_item in metadata_whitelist: + if whitelist_item in key: + status_info['hostnames'][key] = host + break + + # Checks.d Status + status_info['checks'] = {} + for cs in self.check_statuses: + status_info['checks'][cs.name] = {'instances': {}} + for s in cs.instance_statuses: + status_info['checks'][cs.name]['instances'][s.instance_id] = { + 'status': s.status, + 'has_error': s.has_error(), + 'has_warnings': s.has_warnings(), + } + if s.has_error(): + status_info['checks'][cs.name]['instances'][s.instance_id]['error'] = s.error + if s.has_warnings(): + status_info['checks'][cs.name]['instances'][s.instance_id]['warnings'] = s.warnings + status_info['checks'][cs.name]['metric_count'] = cs.metric_count + status_info['checks'][cs.name]['event_count'] = cs.event_count + + # Emitter status + status_info['emitter'] = [] + for es in self.emitter_statuses: + check_status = { + 'name': es.name, + 'status': es.status, + 'has_error': es.has_error(), + } + if es.has_error(): + check_status['error'] = es.error + status_info['emitter'].append(check_status) + + return status_info + class DogstatsdStatus(AgentStatus): @@ -421,6 +477,16 @@ def body_lines(self): ] return lines + def to_dict(self): + status_info = AgentStatus.to_dict(self) + status_info.update({ + 'flush_count': self.flush_count, + 'packet_count': self.packet_count, + 'packets_per_second': self.packets_per_second, + 'metric_count': self.metric_count, + }) + return status_info + class ForwarderStatus(AgentStatus): @@ -442,3 +508,12 @@ def body_lines(self): def has_error(self): return self.flush_count == 0 + + def to_dict(self): + status_info = AgentStatus.to_dict(self) + status_info.update({ + 'flush_count': self.flush_count, + 'queue_length': self.queue_length, + 'queue_size': self.queue_size, + }) + return status_info diff --git a/config.py b/config.py index 7603063bfc..bfde3ff4c7 100644 --- a/config.py +++ b/config.py @@ -258,7 +258,17 @@ def get_config(parse_args=True, cfg_path=None, options=None): else: agentConfig['use_pup'] = True - if agentConfig['use_pup']: + # Concerns only Windows + if config.has_option('Main', 'use_web_info_page'): + agentConfig['use_web_info_page'] = config.get('Main', 'use_web_info_page').lower() in ("yes", "true") + else: + agentConfig['use_web_info_page'] = True + + # Pup doesn't work on Windows + if sys.platform == 'win32': + agentConfig['use_pup'] = False + + if agentConfig['use_pup'] or agentConfig['use_web_info_page']: if config.has_option('Main', 'pup_url'): agentConfig['pup_url'] = config.get('Main', 'pup_url') else: diff --git a/packaging/datadog-agent/win32/build.ps1 b/packaging/datadog-agent/win32/build.ps1 index 5a13f6f777..322ac48216 100644 --- a/packaging/datadog-agent/win32/build.ps1 +++ b/packaging/datadog-agent/win32/build.ps1 @@ -23,6 +23,9 @@ cp ..\..\..\checks.d\* install_files\checks.d mkdir install_files\conf.d cp ..\..\..\conf.d\* install_files\conf.d +# Copy the pup files into the install_files +cp -R ..\..\..\dist\pup install_files\pup + ## Generate the UI install with NSIS # Assumes makensis.exe is within the PATH @@ -31,21 +34,23 @@ cp ..\..\..\conf.d\* install_files\conf.d ## Generate the CLI installer with WiX - # Generate fragments for the files in checks.d and conf.d + # Generate fragments for the files in checks.d, conf.d and pup heat dir install_files\checks.d -gg -dr INSTALLDIR -var var.InstallFilesChecksD -cg checks.d -o wix\checksd.wxs + heat dir install_files\pup -gg -dr INSTALLDIR -var var.InstallFilesPup -cg pup -o wix\pup.wxs heat dir install_files\conf.d -gg -dr APPLIDATIONDATADIRECTORY -t wix\confd.xslt -var var.InstallFilesConfD -cg conf.d -o wix\confd.wxs # Create .wixobj files from agent.wxs, confd.wxs, checksd.wxs - $opts = '-dInstallFiles=install_files', '-dWixRoot=wix', '-dInstallFilesChecksD=install_files\checks.d', '-dInstallFilesConfD=install_files\conf.d', "-dAgentVersion=$version" - candle $opts wix\agent.wxs wix\checksd.wxs wix\confd.wxs + $opts = '-dInstallFiles=install_files', '-dWixRoot=wix', '-dInstallFilesChecksD=install_files\checks.d', '-dInstallFilesConfD=install_files\conf.d', '-dInstallFilesPup=install_files\pup', "-dAgentVersion=$version" + candle $opts wix\agent.wxs wix\checksd.wxs wix\confd.wxs wix\pup.wxs # Light to create the msi - light agent.wixobj checksd.wixobj confd.wixobj -o ..\..\..\build\ddagent.msi + light agent.wixobj checksd.wixobj confd.wixobj pup.wixobj -o ..\..\..\build\ddagent.msi # Clean up rm *wixobj* rm -r install_files\conf.d rm -r install_files\checks.d +rm -r install_files\pup rm -r install_files\Microsoft.VC90.CRT # Move back to the root workspace diff --git a/packaging/datadog-agent/win32/install_files/datadog-agent-status.url b/packaging/datadog-agent/win32/install_files/datadog-agent-status.url new file mode 100644 index 0000000000..b5fbd13d2b --- /dev/null +++ b/packaging/datadog-agent/win32/install_files/datadog-agent-status.url @@ -0,0 +1,6 @@ +[InternetShortcut] +URL=http://localhost:17125/status +IconFile=.\pup\static\favicon.ico +IconIndex=0 +IDList= +HotKey=0 diff --git a/packaging/datadog-agent/win32/install_files/datadog_win32.conf b/packaging/datadog-agent/win32/install_files/datadog_win32.conf index 447b2bbbd4..77c270f720 100644 --- a/packaging/datadog-agent/win32/install_files/datadog_win32.conf +++ b/packaging/datadog-agent/win32/install_files/datadog_win32.conf @@ -37,16 +37,16 @@ use_mount: no # ca_certs = datadog-cert.pem # ========================================================================== # -# Pup configuration +# Information page configuration # ========================================================================== # -# Pup is a small server that displays metric data collected by the agent. -# Think of it as a fancy status page or a toe dip into the world of -# datadog. It can be connected to on the port below. +# Display general information about the running agent on a webpage +# Available at http://:/status +# By default http://localhost:17125/status -# use_pup: yes +# use_web_info_page: yes # pup_port: 17125 -# pup_url: http://localhost:17125 +# pup_interface: localhost # ========================================================================== # # DogStatsd configuration # diff --git a/packaging/datadog-agent/win32/nsis/agent.nsi b/packaging/datadog-agent/win32/nsis/agent.nsi index 1cdbfdaac4..e435cd4cd3 100644 --- a/packaging/datadog-agent/win32/nsis/agent.nsi +++ b/packaging/datadog-agent/win32/nsis/agent.nsi @@ -153,10 +153,14 @@ Section "Datadog Agent" SecDummy File "..\install_files\shell.exe" File "..\install_files\ca-certificates.crt" File "..\install_files\datadog-cert.pem" + File "..\install_files\datadog-agent-status.url" ; Install all of the checks.d checks File /r "..\install_files\checks.d" + ; Install all of the info page web files + File /r "..\install_files\pup" + ; Config does in App Data ; Only write the config if it doesn't exist yet ${IfNot} ${FileExists} "$0\Datadog\datadog.conf" @@ -202,6 +206,7 @@ Section "Uninstall" Delete "$INSTDIR\ca-certificates.crt" Delete "$INSTDIR\datadog-cert.pem" Delete "$INSTDIR\license.txt" + Delete "$INSTDIR\datadog-agent-status.url" Delete "$INSTDIR\Uninstall.exe" RMDir "$INSTDIR" diff --git a/packaging/datadog-agent/win32/wix/agent.wxs b/packaging/datadog-agent/win32/wix/agent.wxs index e0c2d3cc24..b73451dd83 100644 --- a/packaging/datadog-agent/win32/wix/agent.wxs +++ b/packaging/datadog-agent/win32/wix/agent.wxs @@ -81,6 +81,9 @@ + + + @@ -105,12 +108,14 @@ + + diff --git a/pup/pup.html b/pup/pup.html index b7a8a70d8e..bcba73d6f4 100644 --- a/pup/pup.html +++ b/pup/pup.html @@ -23,6 +23,9 @@

Listening on port {{ port }}


+ +
+

No metrics to search through.

diff --git a/pup/pup.py b/pup/pup.py index 1fcef10636..1e2161b4bd 100644 --- a/pup/pup.py +++ b/pup/pup.py @@ -25,6 +25,10 @@ import logging import zlib +# Status page +import platform +from checks.check_status import DogstatsdStatus, ForwarderStatus, CollectorStatus, logger_info + # 3p import tornado from tornado import ioloop @@ -32,7 +36,7 @@ from tornado import websocket # project -from config import get_config +from config import get_config, get_version from util import json log = logging.getLogger('pup') @@ -105,6 +109,20 @@ 'events' ] +# Define settings, path is different if using py2exe +frozen = getattr(sys, 'frozen', '') +if not frozen: + agent_root = os.path.join(os.path.dirname(__file__), '..') +else: + # Using py2exe + agent_root = os.path.dirname(sys.executable) + +settings = { + "static_path": os.path.join(agent_root, "pup", "static"), + "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", + "xsrf_cookies": True, +} + # Check if using old version of Python. Pup's usage of defaultdict requires 2.5 or later, # and tornado only supports 2.5 or later. The agent supports 2.6 onwards it seems. if int(sys.version_info[1]) <= 5: @@ -173,10 +191,26 @@ def agent_update(payload): class MainHandler(tornado.web.RequestHandler): def get(self): - self.render("pup.html", + self.render(os.path.join(agent_root, "pup", "pup.html"), title="Pup", port=port) +class StatusHandler(tornado.web.RequestHandler): + def get(self): + dogstatsd_status = DogstatsdStatus.load_latest_status() + forwarder_status = ForwarderStatus.load_latest_status() + collector_status = CollectorStatus.load_latest_status() + self.render(os.path.join(agent_root, "pup", "status.html"), + port=port, + platform=platform.platform(), + agent_version=get_version(), + python_version=platform.python_version(), + logger_info=logger_info(), + dogstatsd=dogstatsd_status.to_dict(), + forwarder=forwarder_status.to_dict(), + collector=collector_status.to_dict(), + ) + class PostHandler(tornado.web.RequestHandler): def post(self): try: @@ -207,13 +241,6 @@ def on_message(self): def on_close(self): del listeners[self] -settings = { - "static_path": os.path.join(os.path.dirname(__file__), "static"), - "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", - "xsrf_cookies": True, -} - - def tornado_logger(handler): """ Override the tornado logging method. @@ -228,11 +255,11 @@ def tornado_logger(handler): request_time = 1000.0 * handler.request.request_time() log_method("%d %s %.2fms", handler.get_status(), handler._request_summary(), request_time) - application = tornado.web.Application([ (r"/", MainHandler), + (r"/status", StatusHandler), (r"/(.*\..*$)", tornado.web.StaticFileHandler, dict(path=settings['static_path'])), (r"/pupsocket", PupSocket), @@ -259,6 +286,28 @@ def run_pup(config): scheduler.start() io_loop.start() +def run_info_page(): + global port + + config = get_config(parse_args=False) + + info_page_application = tornado.web.Application([ + (r"/", StatusHandler), + (r"/status", StatusHandler), + (r"/(.*\..*$)", tornado.web.StaticFileHandler, + dict(path=settings['static_path'])), + ], log_function=tornado_logger) + + port = config.get('pup_port', 17125) + interface = config.get('pup_interface', 'localhost') + + info_page_application.listen(port, address=interface) + + io_loop = ioloop.IOLoop.instance().start() + +def stop_info_page(): + ioloop.IOLoop.instance().stop() + def main(): """ Parses arguments and starts Pup server """ diff --git a/pup/static/OpenSans-Bold-webfont.ttf b/pup/static/OpenSans-Bold-webfont.ttf new file mode 100755 index 0000000000..7ab5d85bfd Binary files /dev/null and b/pup/static/OpenSans-Bold-webfont.ttf differ diff --git a/pup/static/pup.css b/pup/static/pup.css index 00fda674d6..5b333289e8 100644 --- a/pup/static/pup.css +++ b/pup/static/pup.css @@ -80,6 +80,13 @@ ul { font-style: normal; } +@font-face { + font-family: 'Open Sans'; + src: local("Open Sans"), url("./OpenSans-Bold-webfont.ttf") format("truetype");; + font-weight: bold; + font-style: normal; +} + /* layout */ html { height: 100%; @@ -115,10 +122,9 @@ html { #content { height: 100%; - padding: 0 0 0 190px; + padding: 0 0 0 190px; } - /* content area */ #waiting { width: 500px; @@ -324,6 +330,10 @@ pre { color: #aaa; } +#agent-status { + text-align: center; +} + #port { font-weight: 600; } @@ -584,3 +594,63 @@ pre { fill: #6f56a2; z-index: -1; } + +/* Status page */ + +#status { + width: 600px; + margin: 50px auto 0 auto; +} + +#status h1, #status h2, #status h3 { + color: #6f56a2; +} + +#status h4 { + font-weight: bold; +} + +#status ul { + margin-bottom: 10px; +} + +#status ul ul { + margin-left: 30px; +} +#status ul ul li { + list-style-type: square; +} + +#status dt::after { + content: ':'; +} + +#status dt { + float: left; + width: 180px; + font-weight: bold; +} + +#status ul dl { + margin: 0px; +} + +#status ul dt { + width: 145px; + font-weight: normal; +} + +#status .status-ok { + color: green; + font-weight: bold; +} + +#status .status-warning { + color: orange; + font-weight: bold; +} + +#status .status-error { + color: red; + font-weight: bold; +} \ No newline at end of file diff --git a/pup/status.html b/pup/status.html new file mode 100644 index 0000000000..f8f09acaf4 --- /dev/null +++ b/pup/status.html @@ -0,0 +1,144 @@ + + + + + + + + + Datadog Agent information + + + + + + +
+
+
+

Agent info

+ + +

General information

+ +
+
Agent version
{{ agent_version }}
+
Platform
{{ platform }}
+
Python version
{{ python_version }}
+
Logger
{{ logger_info }}
+
+ + +

Collector

+ +
+
Status date
{{ collector['status_date'] }}
+
Pid
{{ collector['pid'] }}
+
+ +

Hostnames

+ + {% if collector['hostnames'] %} +
+ {% for key, host in collector['hostnames'].iteritems() %} +
{{ key }}
{{ host }}
+ {% end %} +
+ {% else %} +

No host information available yet.

+ {% end %} + +

Checks

+ + {% if collector['checks'] %} +
    + {% for name, check in collector['checks'].iteritems() %} +
  • {{ name }}

    +
      + {% for id, instance in check['instances'].iteritems() %} +
    • +
      +
      Instance #{{ id }}
      {{ instance['status'] }}
      + {% if instance['has_error'] %} +
      Error
      {{ instance['error'] }}
      + {% end %} + {% if instance['has_warnings'] %} +
      Warnings
      +
      + {% if len(instance['warnings']) > 1 %} +
      + {% end %} + {% for warning in instance['warnings'] %} + {{warning}}
      + {% end %} +
      + {% end %} +
      +
    • + {% end %} +
    • Collected {{ check['metric_count'] }} metrics and {{ check['event_count'] }} events
    • +
  • + {% end %} +
+ {% else %} +

No checks have run yet.

+ {% end %} + +

Emitter

+ + {% if collector['emitter'] %} +
+ {% for status in collector['emitter'] %} +
{{ status['name'] }}
+
+ {{ status['status'] }} + {% if status['has_error'] %} +
Error: {{ status['error'] }} + {% end %} +
+ {% end %} +
+ {% else %} +

No emitters have run yet.

+ {% end %} + + + +

Forwarder

+ +
+
Status date
{{ forwarder['status_date'] }}
+
Pid
{{ forwarder['pid'] }}
+
+ +
+
Flush count
{{ forwarder['flush_count'] }}
+
Queue size
{{ forwarder['queue_size'] }}
+
Queue length
{{ forwarder['queue_length'] }}
+
+ + +

DogstatsD

+ +
+
Status date
{{ dogstatsd['status_date'] }}
+
Pid
{{ dogstatsd['pid'] }}
+
+ +
+
Flush count
{{ dogstatsd['flush_count'] }}
+
Packet count
{{ dogstatsd['packet_count'] }}
+
Packets per second
{{ dogstatsd['packets_per_second'] }}
+
Metric count
{{ dogstatsd['metric_count'] }}
+
+ +
+ + + +

Powered by Datadog

+
+ +
+ + diff --git a/setup.py b/setup.py index a6414c70a9..0e65643deb 100644 --- a/setup.py +++ b/setup.py @@ -82,7 +82,11 @@ def __init__(self, **kw): 'console': ['win32\shell.py'], 'service': [agent_svc], 'zipfile': None, - 'data_files': [("Microsoft.VC90.CRT", glob(r'C:\Python27\redist\*.*'))], + 'data_files': [ + ("Microsoft.VC90.CRT", glob(r'C:\Python27\redist\*.*')), + ('pup', glob('pup/status.html')), + ('pup/static', glob('pup/static/*.*')), + ], } setup( diff --git a/util.py b/util.py index 37c8c19d81..b27dd32edc 100644 --- a/util.py +++ b/util.py @@ -1,6 +1,5 @@ import os import platform -import resource import signal import socket import subprocess @@ -259,8 +258,9 @@ class Watchdog(object): If you instantiate more than one, you're also asking for trouble. """ def __init__(self, duration, max_mem_mb = 2000): - """Set the duration - """ + import resource + + #Set the duration self._duration = int(duration) signal.signal(signal.SIGALRM, Watchdog.self_destruct) diff --git a/win32/agent.py b/win32/agent.py index 16c2b4cd57..adeed95891 100644 --- a/win32/agent.py +++ b/win32/agent.py @@ -21,6 +21,7 @@ from config import (get_config, set_win32_cert_path, get_system_stats, load_check_directory) from win32.common import handle_exe_click +from pup import pup class AgentSvc(win32serviceutil.ServiceFramework): _svc_name_ = "ddagent" @@ -33,6 +34,7 @@ def __init__(self, args): config = get_config(parse_args=False) self.forwarder = DDForwarder(config) self.dogstatsd = DogstatsdThread(config) + self.pup = PupThread(config) # Setup the correct options so the agent will use the forwarder opts, args = Values({ @@ -52,6 +54,7 @@ def SvcStop(self): self.forwarder.stop() self.agent.stop() self.dogstatsd.stop() + self.pup.stop() self.running = False def SvcDoRun(self): @@ -62,6 +65,7 @@ def SvcDoRun(self): self.forwarder.start() self.agent.start() self.dogstatsd.start() + self.pup.start() # Loop to keep the service running since all DD services are # running in separate threads @@ -118,7 +122,7 @@ def __init__(self, agentConfig): self.forwarder = Application(port, agentConfig, watchdog=False) def run(self): - self.forwarder.run() + self.forwarder.run() def stop(self): self.forwarder.stop() @@ -136,9 +140,22 @@ def stop(self): self.server.stop() self.reporter.stop() +class PupThread(threading.Thread): + def __init__(self, agentConfig): + threading.Thread.__init__(self) + self.is_enabled = agentConfig.get('use_web_info_page', True) + self.pup = pup + + def run(self): + if self.is_enabled: + self.pup.run_info_page() + + def stop(self): + if self.is_enabled: + self.pup.stop_info_page() if __name__ == '__main__': if len(sys.argv) == 1: handle_exe_click(AgentSvc._svc_name_) else: - win32serviceutil.HandleCommandLine(AgentSvc) + win32serviceutil.HandleCommandLine(AgentSvc) \ No newline at end of file