├── .gitignore ├── Changelog.md ├── Dockerfile.init ├── Makefile ├── README.md ├── apache ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── apache.conf.example ├── apache.py ├── apache.yml ├── requirements.txt └── status_template ├── docker ├── docker.conf ├── docker.py ├── docker.yml └── requirements.txt ├── hosts ├── mongo ├── Dockerfile ├── README.md ├── mongo.conf.example ├── mongo.py ├── mongo.yml └── requirements.txt ├── mysql ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── mysql.conf.example ├── mysql.py ├── mysql.yml └── requirements.txt ├── nginx ├── CHANGELOG.md ├── Dockerfile ├── README.md ├── nginx.conf.example ├── nginx.py ├── nginx.yml └── status_template ├── postgres ├── Dockerfile ├── README.md ├── postgres.conf.example ├── postgres.py ├── postgres.yml └── requirements.txt └── redisdb ├── Dockerfile ├── README.md ├── redisdb.conf.example ├── redisdb.py ├── redisdb.yml └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 2 | # 11.08.2014 Changelog 3 | 4 | ## Redis 1.1 5 | 6 | - Replace host/port with redis:// connection url 7 | -------------------------------------------------------------------------------- /Dockerfile.init: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:latest 2 | MAINTAINER Martin Rusev 3 | 4 | RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv AD53961F 5 | 6 | RUN echo 'deb http://bg.archive.ubuntu.com/ubuntu trusty main universe' > /etc/apt/sources.list && \ 7 | echo 'deb http://bg.archive.ubuntu.com/ubuntu trusty-updates main restricted' >> /etc/apt/sources.list && \ 8 | echo 'deb http://bg.archive.ubuntu.com/ubuntu trusty-security main' >> /etc/apt/sources.list 9 | 10 | RUN echo 'deb http://packages.amon.cx/repo amon contrib' >> /etc/apt/sources.list 11 | RUN apt-get update 12 | 13 | 14 | RUN apt-get install software-properties-common -y --force-yes 15 | RUN apt-add-repository ppa:ansible/ansible 16 | RUN apt-get update 17 | RUN apt-get install ansible mysql-server apache2 nginx postgresql mongodb-server -y --force-yes 18 | 19 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | cleanup: 2 | rm -f Dockerfile 3 | 4 | test_mongo: cleanup 5 | cp mongo/Dockerfile Dockerfile 6 | docker build --rm=true . 7 | rm Dockerfile 8 | 9 | test_mysql: cleanup 10 | cp mysql/Dockerfile Dockerfile 11 | docker build --rm=true . 12 | rm Dockerfile 13 | 14 | test_apache: cleanup 15 | cp apache/Dockerfile Dockerfile 16 | docker build --rm=true . 17 | rm Dockerfile 18 | 19 | test_nginx: cleanup 20 | cp nginx/Dockerfile Dockerfile 21 | docker build --rm=true . 22 | rm Dockerfile 23 | 24 | test_redisdb: cleanup 25 | cp redisdb/Dockerfile Dockerfile 26 | docker build --rm=true . 27 | rm Dockerfile 28 | 29 | 30 | test_postgres: cleanup 31 | cp postgres/Dockerfile Dockerfile 32 | docker build --rm=true . 33 | rm Dockerfile 34 | 35 | # Build the base container -> amonbase 36 | init: cleanup 37 | docker pull phusion/baseimage 38 | cp Dockerfile.init Dockerfile 39 | docker build -t amonbase . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Amon Plugins Library 2 | ===================== 3 | 4 | [Amon](https://amon.cx) is a hosted monitoring solution. Amon uses open-source plugins (written in Python) 5 | to monitor a wide variety of application metrics. 6 | 7 | Each folder in this repository represents one Amon plugin. 8 | 9 | 10 | Getting Started 11 | --------------------------------- 12 | 13 | 14 | You will find detailed information about how plugins work in Amon at https://amon.cx/docs#plugins 15 | 16 | 17 | 18 | 19 | How to create your own Amon plugin 20 | --------------------------------- 21 | 22 | Anyone can create a Amon plugin. Get started by: 23 | 24 | 1. looking at the examples in this Repository 25 | 2. reading the development guide at https://amon.cx/docs#plugins 26 | 27 | When you have something working you'd like to share, drop me a note at . 28 | 29 | Or, send a pull request here on github. Also don't hesitate to contact me before or during 30 | plugin development if you need guidance. 31 | -------------------------------------------------------------------------------- /apache/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.3 2 | ======= 3 | 4 | * Detailed request data through goaccess 5 | 6 | 1.0.2 7 | ======= 8 | 9 | * Sends error data to Amon 10 | 11 | 1.0.1 12 | ======= 13 | 14 | * Properly logs and error if the status url is not found -------------------------------------------------------------------------------- /apache/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amonbase:latest 2 | 3 | RUN apt-get install -y --force-yes amon-agent 4 | 5 | 6 | RUN /etc/init.d/amon-agent status 7 | 8 | ADD hosts /etc/amonagent/hosts 9 | ADD apache/ /etc/amonagent/plugins/apache/ 10 | 11 | RUN ansible-playbook /etc/amonagent/plugins/apache/apache.yml -i /etc/amonagent/hosts -v 12 | 13 | RUN cat /var/log/apache2/access.log 14 | RUN ls -lh /var/log/apache2 15 | RUN amonpm test apache 16 | 17 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /apache/README.md: -------------------------------------------------------------------------------- 1 | # Apache Plugin 2 | 3 | 4 | Monitors Apache, reporting: 5 | 6 | - Idle, Busy workers 7 | - Requests per second 8 | - Total accesses/bytes 9 | 10 | ## Installing 11 | 12 | The Apache plugin requires Amon agent version 0.8+. To check your agent version: `$ /etc/init.d/amon-agent status` 13 | You also need to enable `mod_status` in your apache configuration. You can see a tutorial [http://www.cyberciti.biz/faq/apache-server-status/](http://www.cyberciti.biz/faq/apache-server-status/) 14 | 15 | To install the plugin: 16 | 17 | 18 | $ cp /etc/amonagent/plugins/apache/apache.conf/example /etc/amonagent/plugins-enabled/apache.conf 19 | 20 | 21 | ## Configuration 22 | 23 | * **status_url** - Your apache status url. 24 | 25 | Example: If you have the following in your apache configuration file ``, the url will be 26 | `http://yourserver/server-status?auto` 27 | 28 | ## Testing 29 | 30 | To test the installation, run the following: 31 | 32 | 33 | $ /etc/init.d/amon-agent plugins 34 | 35 | 36 | If everything works as expected, restart the agent and in a couple of minutes you will see the metrics in Amon 37 | -------------------------------------------------------------------------------- /apache/apache.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "status_url": "{{ status_url }}", 3 | "log_file": "/var/log/apache2/access.log" 4 | } -------------------------------------------------------------------------------- /apache/apache.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import tempfile 3 | import json 4 | import subprocess 5 | 6 | from amonagent.modules.plugins import AmonPlugin 7 | 8 | class ApachePlugin(AmonPlugin): 9 | 10 | VERSION = '1.0.3' 11 | 12 | GAUGES = { 13 | 'IdleWorkers': 'performance.idle_workers', 14 | 'BusyWorkers': 'performance.busy_workers', 15 | 'CPULoad': 'performance.cpu_load', 16 | 'ReqPerSec': 'requests.per.second', 17 | 'Total kBytes': 'net.bytes', 18 | 'Total Accesses': 'net.hits', 19 | } 20 | 21 | def get_versions(self, status_url): 22 | 23 | response = requests.head(status_url) 24 | library = response.headers.get('server') 25 | 26 | self.version(apache=library, plugin=self.VERSION) 27 | 28 | 29 | def collect(self): 30 | status_url = self.config.get('status_url') 31 | log_file = self.config.get('log_file', "") 32 | response = None 33 | 34 | 35 | try: 36 | self.get_versions(status_url) 37 | except Exception, e: 38 | self.error(e) 39 | 40 | 41 | try: 42 | response = requests.get(status_url, timeout=5) 43 | except Exception, e: 44 | self.error(e) 45 | 46 | 47 | 48 | try: 49 | status_code = response.status_code 50 | except: 51 | status_code = None 52 | 53 | if status_code == 200: 54 | status = response.text.splitlines() 55 | 56 | for line in status: 57 | key, value = line.split(':') 58 | 59 | if key in self.GAUGES.keys(): 60 | normalized_key = self.GAUGES[key] 61 | 62 | 63 | try: 64 | value = float(value) 65 | except ValueError: 66 | continue 67 | 68 | 69 | self.gauge(normalized_key, value) 70 | 71 | # Get detailed stats with goaccess 72 | configfile = tempfile.NamedTemporaryFile() 73 | # Default log format - with vhosts 74 | log_content = """ 75 | date_format %d/%b/%Y 76 | log_format %^:%^ %h %^[%d:%t %^] "%r" %s %b 77 | time_format %H:%M:%S 78 | """ 79 | configfile.write(log_content) 80 | configfile.flush() 81 | 82 | command = ["goaccess", "-f", log_file, "-p", configfile.name, "-o", "json"] 83 | 84 | print command 85 | server_data = subprocess.Popen(command, stdout=subprocess.PIPE if format else None) 86 | out, err = server_data.communicate() 87 | 88 | try: 89 | json_result = json.loads(out) 90 | except: 91 | json_result = None 92 | 93 | if json_result: 94 | general = json_result.get('general') 95 | requests_result = json_result.get('requests') 96 | not_found_result = json_result.get('not_found') 97 | 98 | ignored_general_keys = ['date_time', 'log_path'] 99 | for key, value in general.items(): 100 | if key not in ignored_general_keys: 101 | name = self.normalize(key) 102 | self.counter(name, value) 103 | 104 | loop_over = {'requests': requests_result, 'not_found': not_found_result} 105 | for key, data in loop_over.items(): 106 | if data: 107 | result = {'data': []} 108 | 109 | try: 110 | headers = data[0].keys() 111 | except: 112 | headers = None 113 | 114 | if headers: 115 | result['headers'] = headers 116 | 117 | for r in data: 118 | result['data'].append(r.values()) 119 | 120 | self.result[key] = result -------------------------------------------------------------------------------- /apache/apache.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/apache" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled" 7 | name: "apache" 8 | status_url: "http://127.0.0.1/server-status?auto" 9 | goaccess_list_file: "/etc/apt/sources.list.d/goaccess.list" 10 | tasks: 11 | - 12 | name: Apache | Enable mod_status 13 | command: a2enmod status 14 | when: ansible_os_family == "Debian" 15 | - 16 | name: Copy the plugin configuration file 17 | template: src={{ plugins_path }}/{{ name }}.conf.example dest={{ enabled_plugins }}/{{ name }}.conf 18 | 19 | - 20 | apt_key: url=http://deb.goaccess.io/gnugpg.key state=present 21 | when: ansible_os_family == "Debian" 22 | - 23 | name: Create Goaccess list file 24 | lineinfile: dest={{goaccess_list_file}} regexp='^' line='' state=present create=True 25 | when: ansible_os_family == "Debian" 26 | - 27 | name: Add Goaccess Repository. 28 | lineinfile: "dest={{goaccess_list_file}} line='deb http://deb.goaccess.io {{ ansible_distribution_release }} main'" 29 | when: ansible_os_family == "Debian" 30 | - 31 | apt: name=goaccess state=latest update_cache=yes 32 | when: ansible_os_family == "Debian" 33 | - 34 | template: src={{ plugins_path }}/status_template dest=/etc/apache2/mods-enabled/status.conf 35 | when: ansible_os_family == "Debian" 36 | - 37 | service: name=apache2 state=restarted 38 | when: ansible_os_family == "Debian" 39 | 40 | - 41 | yum: name=goaccess state=latest 42 | when: ansible_os_family == "RedHat" 43 | - 44 | template: src={{ plugins_path }}/status_template dest=/etc/httpd/conf.d/status.conf 45 | when: ansible_os_family == "RedHat" 46 | - 47 | service: name=httpd state=restarted 48 | when: ansible_os_family == "RedHat" 49 | -------------------------------------------------------------------------------- /apache/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.0.0 -------------------------------------------------------------------------------- /apache/status_template: -------------------------------------------------------------------------------- 1 | 2 | ExtendedStatus On 3 | 4 | SetHandler server-status 5 | Order deny,allow 6 | Deny from all 7 | Allow from 127.0.0.1 8 | 9 | 10 | -------------------------------------------------------------------------------- /docker/docker.conf: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /docker/docker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from docker import Client 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | class ContainerDataCollector(object): 7 | 8 | # @stats[:cpu_percent] += calculate_cpu_percent(container_id, stats["cpu_stats"]["cpu_usage"]["total_usage"], s 9 | # tats["cpu_stats"]["system_cpu_usage"], stats["cpu_stats"]["cpu_usage"]["percpu_usage"].count) 10 | 11 | 12 | # def calculate_cpu_percent(container_id, total_container_cpu, total_system_cpu, num_processors) 13 | # # The CPU values returned by the docker api are cumulative for the life of the process, which is not what we want. 14 | # now = Time.now 15 | # last_cpu_stats = memory(container_id) 16 | # if @last_run && last_cpu_stats 17 | # container_cpu_delta = total_container_cpu - last_cpu_stats[:container_cpu] 18 | # system_cpu_delta = total_system_cpu - last_cpu_stats[:system_cpu] 19 | # cpu_percent = cpu_percent(container_cpu_delta, system_cpu_delta, num_processors) 20 | # end 21 | # remember(container_id => {:container_cpu => total_container_cpu, :system_cpu => total_system_cpu}) 22 | # return cpu_percent || 0 23 | # end 24 | def calculate_cpu_percent(self, prev_cpu, current_cpu): 25 | cpu_percent = float(0) 26 | 27 | num_processors = len(current_cpu["cpu_usage"]['percpu_usage']) 28 | 29 | 30 | container_cpu_delta = current_cpu["cpu_usage"]['total_usage']-prev_cpu["cpu_usage"]['total_usage'] 31 | system_cpu_delta = current_cpu['system_cpu_usage']-prev_cpu['system_cpu_usage'] 32 | 33 | 34 | if container_cpu_delta > 0 and system_cpu_delta > 0: 35 | cpu_percent = (container_cpu_delta / system_cpu_delta) * num_processors * 100.0 36 | 37 | return cpu_percent 38 | 39 | def container_processes(self, container_id): 40 | result = {} 41 | container_top = self.client.top(container_id) 42 | 43 | container_processes = container_top.get('Processes') 44 | header = container_top.get('Titles') 45 | 46 | if container_processes and header: 47 | header = [x.lower() for x in header] 48 | result = {'header': header, 'data': container_processes} 49 | 50 | return result 51 | 52 | 53 | def collect_container_data(self, container): 54 | container_id = container.get('Id') 55 | total_loops = 0 56 | 57 | try: 58 | name = container.get('Names')[0].strip("/") 59 | except: 60 | name = "" 61 | 62 | result = { 63 | "created": container.get('Created'), 64 | "container_id": container_id, 65 | "name": name, 66 | "image": container.get('Image'), 67 | "status": container.get('Status'), 68 | "processes": self.container_processes(container_id) 69 | } 70 | 71 | 72 | 73 | 74 | # @stats[:cpu_percent] += calculate_cpu_percent(container_id, stats["cpu_stats"]["cpu_usage"]["total_usage"], s 75 | # tats["cpu_stats"]["system_cpu_usage"], stats["cpu_stats"]["cpu_usage"]["percpu_usage"].count) 76 | # @stats[:memory_usage] += stats["memory_stats"]["usage"].to_f / 1024.0 / 1024.0 77 | # @stats[:memory_limit] += stats["memory_stats"]["limit"].to_f / 1024.0 / 1024.0 78 | # @stats[:network_in] += stats["network"]["rx_bytes"].to_f / 1024.0 79 | # @stats[:network_out] += stats["network"]["tx_bytes"].to_f / 1024.0 80 | prev_cpu = {} 81 | for i in self.client.stats(container_id, True): 82 | if total_loops == 0: 83 | # Remember CPU 84 | prev_cpu = i['cpu_stats'] 85 | 86 | if total_loops == 1: 87 | result['network_out'] = i['network']['tx_bytes'] / 1024 88 | result['network_in'] = i['network']['rx_bytes'] / 1024 89 | result['memory'] = i['memory_stats']['usage'] / 1024 / 1024 90 | result['memory_limit'] = i['memory_stats']['limit'] / 1024 / 1024 91 | result['cpu_percent'] = self.calculate_cpu_percent(prev_cpu, i['cpu_stats']) 92 | return result 93 | elif total_loops == 2: 94 | break 95 | 96 | total_loops = total_loops+1 97 | 98 | 99 | return result 100 | 101 | 102 | def collect(self): 103 | result = [] 104 | try: 105 | self.client = Client(base_url='unix://var/run/docker.sock', version='1.17') 106 | except: 107 | log.exception('Unable to connect to the Docker API.') 108 | self.client = False 109 | 110 | if self.client: 111 | 112 | try: 113 | running_containers = self.client.containers(filters={'status': 'running'}) 114 | except: 115 | running_containers = [] 116 | 117 | procs = [] 118 | total_running_containers = len(running_containers) 119 | if total_running_containers > 0: 120 | 121 | # Wait for Docker to release an update with all the containers data collected in one go, until then: 122 | for container in running_containers: 123 | p = self.collect_container_data(container) 124 | result.append(p) 125 | 126 | return result 127 | 128 | 129 | 130 | 131 | container_data_collector = ContainerDataCollector() -------------------------------------------------------------------------------- /docker/docker.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled/" 7 | tasks: 8 | - 9 | template: src={{ plugins_path }}docker/docker.conf.example dest={{ enabled_plugins }}docker.conf 10 | - 11 | name: Install Plugin requirements 12 | pip: requirements={{ plugins_path }}docker/requirements.txt -------------------------------------------------------------------------------- /docker/requirements.txt: -------------------------------------------------------------------------------- 1 | docker-py -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | localhost ansible_connection=local -------------------------------------------------------------------------------- /mongo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amonbase:latest 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y --force-yes amon-agent mongodb-server 5 | 6 | ADD hosts /etc/amonagent/hosts 7 | ADD mongo/mongo.yml /etc/amonagent/plugins/mongo/mongo.yml 8 | ADD mongo/mongo.conf.example /etc/amonagent/plugins/mongo/mongo.conf.example 9 | 10 | RUN service mongodb start 11 | 12 | RUN ansible-playbook /etc/amonagent/plugins/mongo/mongo.yml -i /etc/amonagent/hosts -v 13 | 14 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /mongo/README.md: -------------------------------------------------------------------------------- 1 | # Mongo Plugin 2 | 3 | 4 | Monitors MongoDB parsing the output from the `db.serverStatus()` command. Sends to Amon the the following metrics: 5 | 6 | - Connections - http://docs.mongodb.org/manual/reference/command/serverStatus/#connections 7 | - Opcounters - http://docs.mongodb.org/manual/reference/command/serverStatus/#opcounters 8 | - Cursors - http://docs.mongodb.org/manual/reference/command/serverStatus/#cursors 9 | - IndexCounters - http://docs.mongodb.org/manual/reference/command/serverStatus/#indexcounters 10 | - Asserts - http://docs.mongodb.org/manual/reference/command/serverStatus/#asserts 11 | - Metrics - http://docs.mongodb.org/manual/reference/command/serverStatus/#metrics 12 | 13 | ## Installing 14 | 15 | The Mongo plugin requires Amon agent version 0.8+. To check your agent version: `$ /etc/init.d/amon-agent status` 16 | 17 | To install the plugin: 18 | 19 | $ apt-get install python-dev # pymongo requirements on Debian/Ubuntu 20 | $ yum install python-devel # pymongo requirements on CentOS/Amazon AMI/Fedora 21 | 22 | $ cp /etc/amonagent/plugins/mongo/mongo.conf.example /etc/amonagent/plugins-enabled/mongo.conf 23 | $ sudo pip install -r /etc/amonagent/plugins/mongo/requirements.txt 24 | 25 | 26 | ## Configuration 27 | 28 | * **server** - Mongo URI connection string http://docs.mongodb.org/manual/reference/connection-string/. 29 | 30 | 31 | If you have enabled authentication for your Mongo database, you have to create a read only admin user for Amon: 32 | 33 | 34 | $ mongo 35 | $ use admin 36 | $ db.auth("admin", "admin-password") 37 | $ db.addUser("amon", "your-desired-password", true) 38 | 39 | 40 | ## Testing 41 | 42 | To test the installation, run the following: 43 | 44 | 45 | $ /etc/init.d/amon-agent plugins 46 | 47 | 48 | If everything works as expected, restart the agent and in a couple of minutes you will see the metrics in Amon 49 | -------------------------------------------------------------------------------- /mongo/mongo.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "server": "{{ uri }}" 3 | } -------------------------------------------------------------------------------- /mongo/mongo.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | try: 4 | import pymongo 5 | except ImportError: 6 | raise Exception('Python PyMongo Module can not be imported. Please check the installation instruction on https://github.com/amonapp/amon-plugins/tree/master/mongo') 7 | 8 | from pymongo import uri_parser 9 | 10 | from amonagent.modules.plugins import AmonPlugin 11 | 12 | class MongoPlugin(AmonPlugin): 13 | 14 | VERSION = '1.0' 15 | 16 | 17 | GAUGES = [ 18 | "indexCounters.btree.missRatio", 19 | "globalLock.ratio", 20 | "connections.current", 21 | "connections.available", 22 | "mem.resident", 23 | "mem.virtual", 24 | "mem.mapped", 25 | "cursors.totalOpen", 26 | "cursors.timedOut", 27 | 28 | "stats.indexes", 29 | "stats.indexSize", 30 | "stats.objects", 31 | "stats.dataSize", 32 | "stats.storageSize", 33 | 34 | "replSet.health", 35 | "replSet.state", 36 | "replSet.replicationLag", 37 | 38 | ] 39 | 40 | COUNTERS = [ 41 | "indexCounters.btree.accesses", 42 | "indexCounters.btree.hits", 43 | "indexCounters.btree.misses", 44 | "opcounters.insert", 45 | "opcounters.query", 46 | "opcounters.update", 47 | "opcounters.delete", 48 | "opcounters.getmore", 49 | "opcounters.command", 50 | "asserts.regular", 51 | "asserts.warning", 52 | "asserts.msg", 53 | "asserts.user", 54 | "asserts.rollovers", 55 | "metrics.document.deleted", 56 | "metrics.document.inserted", 57 | "metrics.document.returned", 58 | "metrics.document.updated", 59 | "metrics.getLastError.wtime.num", 60 | "metrics.getLastError.wtime.totalMillis", 61 | "metrics.getLastError.wtimeouts", 62 | "metrics.operation.fastmod", 63 | "metrics.operation.idhack", 64 | "metrics.operation.scanAndOrder", 65 | "metrics.queryExecutor.scanned", 66 | "metrics.record.moves", 67 | ] 68 | 69 | METRICS = GAUGES + COUNTERS 70 | 71 | DEFAULT_TIMEOUT = 10 72 | 73 | SLOW_QUERIES_ROWS = ['millis', 'ns', 'op', 'query', 'ts'] 74 | COLLECTION_ROWS = ['count','ns','avgObjSize', 'totalIndexSize', 'indexSizes', 'size'] 75 | 76 | def get_versions(self): 77 | mongodb_version = self.conn.server_info()['version'] 78 | 79 | 80 | self.version(mongo=mongodb_version, plugin=self.VERSION, pymongo=pymongo.version) 81 | 82 | 83 | def collect(self): 84 | server = self.config.get('server') 85 | 86 | if server == None: 87 | self.error("Missing 'server' in mongo config") 88 | return 89 | 90 | 91 | parsed = uri_parser.parse_uri(server) 92 | 93 | username = parsed.get('username') 94 | password = parsed.get('password') 95 | db_name = parsed.get('database') 96 | slowms = self.config.get('slowms', 25) 97 | 98 | if not db_name: 99 | self.log.info('No MongoDB database found in URI. Defaulting to admin.') 100 | db_name = 'admin' 101 | 102 | do_auth = True 103 | if username is None or password is None: 104 | self.log.debug("Mongo: cannot extract username and password from config %s" % server) 105 | 106 | do_auth = False 107 | 108 | try: 109 | self.conn = pymongo.Connection(server, network_timeout=self.DEFAULT_TIMEOUT) 110 | except Exception, e: 111 | self.error(e) 112 | return 113 | 114 | if self.conn == None: 115 | return 116 | 117 | db = self.conn[db_name] 118 | 119 | if do_auth: 120 | if not db.authenticate(username, password): 121 | self.error("Mongo: cannot connect with config %s" % server) 122 | 123 | status = db["$cmd"].find_one({"serverStatus": 1}) 124 | status['stats'] = db.command('dbstats') 125 | 126 | # Handle replica data, if any 127 | # See http://www.mongodb.org/display/DOCS/Replica+Set+Commands#ReplicaSetCommands-replSetGetStatus 128 | try: 129 | data = {} 130 | 131 | replSet = db.command('replSetGetStatus') 132 | if replSet: 133 | primary = None 134 | current = None 135 | 136 | # find nodes: master and current node (ourself) 137 | for member in replSet.get('members'): 138 | if member.get('self'): 139 | current = member 140 | if int(member.get('state')) == 1: 141 | primary = member 142 | 143 | # If we have both we can compute a lag time 144 | if current is not None and primary is not None: 145 | lag = current['optimeDate'] - primary['optimeDate'] 146 | # Python 2.7 has this built in, python < 2.7 don't... 147 | if hasattr(lag,'total_seconds'): 148 | data['replicationLag'] = lag.total_seconds() 149 | else: 150 | data['replicationLag'] = (lag.microseconds + \ 151 | (lag.seconds + lag.days * 24 * 3600) * 10**6) / 10.0**6 152 | 153 | if current is not None: 154 | data['health'] = current['health'] 155 | 156 | data['state'] = replSet['myState'] 157 | status['replSet'] = data 158 | except Exception, e: 159 | if "OperationFailure" in repr(e) and "replSetGetStatus" in str(e): 160 | pass 161 | else: 162 | raise e 163 | 164 | # If these keys exist, remove them for now as they cannot be serialized 165 | try: 166 | status['backgroundFlushing'].pop('last_finished') 167 | except KeyError: 168 | pass 169 | try: 170 | status.pop('localTime') 171 | except KeyError: 172 | pass 173 | 174 | # Go through the metrics and save the values 175 | for m in self.METRICS: 176 | 177 | # each metric is of the form: x.y.z with z optional 178 | # and can be found at status[x][y][z] 179 | 180 | value = status 181 | try: 182 | for c in m.split("."): 183 | value = value[c] 184 | except KeyError: 185 | continue 186 | 187 | # value is now status[x][y][z] 188 | assert type(value) in (types.IntType, types.LongType, types.FloatType) 189 | 190 | # Check if metric is a gauge or rate 191 | if m in self.GAUGES: 192 | self.gauge(m, value) 193 | 194 | 195 | if m in self.COUNTERS: 196 | self.counter(m, value) 197 | 198 | 199 | # Performance 200 | params = {"millis": { "$gt" : slowms }} 201 | performance = db['system.profile']\ 202 | .find(params)\ 203 | .sort("ts", pymongo.DESCENDING)\ 204 | .limit(10) 205 | 206 | if performance.clone().count() > 0: 207 | 208 | slow_queries_result = { 209 | 'headers': self.SLOW_QUERIES_ROWS, 210 | 'data': [] 211 | } 212 | 213 | for r in performance: 214 | row_list = [r.get(key) for key in self.SLOW_QUERIES_ROWS] 215 | normalized_row = map(self.normalize_row_value, row_list) 216 | 217 | slow_queries_result['data'].append(normalized_row) 218 | 219 | self.result['slow_queries'] = slow_queries_result 220 | 221 | # Collection sizes 222 | collections = db.collection_names(include_system_collections=False) 223 | if len(collections) > 0: 224 | 225 | collection_size_results = { 226 | 'headers': self.COLLECTION_ROWS, 227 | 'data': [] 228 | } 229 | 230 | 231 | for col in collections: 232 | collection_stats = db.command("collstats", col) 233 | col_list = [collection_stats.get(key) for key in self.COLLECTION_ROWS] 234 | normalized_row = map(self.normalize_row_value, col_list) 235 | 236 | collection_size_results['data'].append(normalized_row) 237 | 238 | self.result['tables_size'] = collection_size_results 239 | 240 | self.get_versions() 241 | self.conn.close() -------------------------------------------------------------------------------- /mongo/mongo.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled/" 7 | vars_prompt: 8 | - name: "uri" 9 | prompt: "Please enter your MongoDB connection string (Example: mongodb://127.0.0.1:27017/db)" 10 | private: no 11 | tasks: 12 | - 13 | name: Install python-devel 14 | yum: name=python-devel state=latest 15 | when: ansible_os_family == "RedHat" 16 | - 17 | name: Install python-dev 18 | apt: name=python-dev 19 | when: ansible_os_family == "Debian" 20 | - 21 | template: src={{ plugins_path }}mongo/mongo.conf.example dest={{ enabled_plugins }}mongo.conf 22 | - 23 | name: Install Plugin requirements 24 | pip: requirements={{ plugins_path }}mongo/requirements.txt -------------------------------------------------------------------------------- /mongo/requirements.txt: -------------------------------------------------------------------------------- 1 | pymongo -------------------------------------------------------------------------------- /mysql/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.1 2 | ======= 3 | 4 | * Slow queries log 5 | 6 | 7 | 1.0.1 8 | ======= 9 | 10 | * CentOS specific installation -------------------------------------------------------------------------------- /mysql/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amonbase:latest 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y --force-yes amon-agent mysql-server 5 | 6 | ADD hosts /etc/amonagent/hosts 7 | ADD mysql/mysql.yml /etc/amonagent/plugins/mysql/mysql.yml 8 | ADD mysql/mysql.conf.example /etc/amonagent/plugins/mysql/mysql.conf.example 9 | 10 | RUN ansible-playbook /etc/amonagent/plugins/mysql/mysql.yml -i /etc/amonagent/hosts -v 11 | 12 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /mysql/README.md: -------------------------------------------------------------------------------- 1 | # MySQL Plugin 2 | 3 | 4 | Monitors MySQL. Parses the output from the `SHOW STATUS` command https://dev.mysql.com/doc/refman/5.0/en/show-status.html. 5 | 6 | 7 | ## Installing 8 | 9 | The MySQL plugin requires Amon agent version 0.8+. To check your agent version: `$ /etc/init.d/amon-agent status`. 10 | 11 | To install the plugin: 12 | 13 | 14 | $ cp /etc/amonagent/plugins/mysql/mysql.conf.example /etc/amonagent/plugins-enabled/mysql.conf 15 | $ sudo pip install -r /etc/amonagent/plugins/mysql/requirements.txt 16 | 17 | ## Configuration 18 | 19 | * **host** - Your MySQL host, defaults to localhost 20 | * **port** - Your MySQL port, defaults to 3306 21 | * **password** - Your MySQL password, defined when creating the amon user 22 | * **user** - Your MySQL user, defaults to amon 23 | * **socket** - (Optional), If you connect to your MySQL instance through an Unix socket. 24 | 25 | 26 | You have to create an Amon user with replication rights: 27 | 28 | $ sudo mysql -e "create user 'amon'@'localhost' identified by 'desired-password';" 29 | $ sudo mysql -e "grant replication client on *.* to 'amon'@'localhost';" 30 | 31 | ## Testing 32 | 33 | To test the installation, run the following: 34 | 35 | 36 | $ /etc/init.d/amon-agent plugins 37 | 38 | 39 | If everything works as expected, restart the agent and in a couple of minutes you will see the metrics in Amon 40 | -------------------------------------------------------------------------------- /mysql/mysql.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "user": "amon", 3 | "password": "{{ password }}", 4 | "port": 3306 5 | } -------------------------------------------------------------------------------- /mysql/mysql.py: -------------------------------------------------------------------------------- 1 | try: 2 | import MySQLdb 3 | except ImportError: 4 | raise Exception("Cannot import MySQLdb module.") 5 | 6 | from amonagent.modules.plugins import AmonPlugin 7 | 8 | 9 | class MySQLPLugin(AmonPlugin): 10 | 11 | VERSION = '1.1' 12 | 13 | GAUGES = { 14 | 'Max_used_connections': 'net.max_connections', 15 | 'Open_files': 'performance.open_files', 16 | 'Table_locks_waited': 'performance.table_locks_waited', 17 | 'Threads_connected': 'performance.threads_connected', 18 | 'Innodb_current_row_locks': 'innodb.current_row_locks', 19 | } 20 | 21 | 22 | COUNTERS = { 23 | 'Connections': 'net.connections', 24 | 'Innodb_data_reads': 'innodb.data_reads', 25 | 'Innodb_data_writes': 'innodb.data_writes', 26 | 'Innodb_os_log_fsyncs': 'innodb.os_log_fsyncs', 27 | 'Innodb_row_lock_waits': 'innodb.row_lock_waits', 28 | 'Innodb_row_lock_time': 'innodb.row_lock_time', 29 | 'Innodb_mutex_spin_waits': 'innodb.mutex_spin_waits', 30 | 'Innodb_mutex_spin_rounds': 'innodb.mutex_spin_rounds', 31 | 'Innodb_mutex_os_waits': 'innodb.mutex_os_waits', 32 | 'Slow_queries': 'performance.slow_queries', 33 | 'Questions': 'performance.questions', 34 | 'Queries': 'performance.queries', 35 | 'Com_select': 'performance.com_select', 36 | 'Com_insert': 'performance.com_insert', 37 | 'Com_update': 'performance.com_update', 38 | 'Com_delete': 'performance.com_delete', 39 | 'Com_insert_select': 'performance.com_insert_select', 40 | 'Com_update_multi': 'performance.com_update_multi', 41 | 'Com_delete_multi': 'performance.com_delete_multi', 42 | 'Com_replace_select': 'performance.com_replace_select', 43 | 'Qcache_hits': 'performance.qcache_hits', 44 | 'Created_tmp_tables': 'performance.created_tmp_tables', 45 | 'Created_tmp_disk_tables': 'performance.created_tmp_disk_tables', 46 | 'Created_tmp_files': 'performance.created_tmp_files', 47 | } 48 | 49 | SLOW_QUERIES = """ 50 | SELECT 51 | mysql.slow_log.query_time, 52 | mysql.slow_log.rows_sent, 53 | mysql.slow_log.rows_examined, 54 | mysql.slow_log.lock_time, 55 | mysql.slow_log.db, 56 | mysql.slow_log.sql_text AS query, 57 | mysql.slow_log.start_time 58 | FROM 59 | mysql.slow_log 60 | WHERE 61 | mysql.slow_log.query_time > 1 62 | ORDER BY 63 | start_time DESC 64 | LIMIT 30 65 | """ 66 | 67 | SLOW_QUERIES_ROWS = ['query_time','rows_sent', 68 | 'rows_examined','lock_time', 'db', 69 | 'query', 'start_time'] 70 | 71 | 72 | TABLES_SIZE = """ 73 | SELECT table_name as 'table', 74 | table_schema as 'database', 75 | table_rows as rows, 76 | CONCAT(table_schema, '.', table_name) as full_name, 77 | data_length as size, 78 | index_length as indexes, 79 | (data_length + index_length) as total, 80 | ROUND(index_length / data_length, 2) as index_fraction 81 | FROM information_schema.TABLES 82 | WHERE table_schema NOT IN ('information_schema', 'performance_schema', 'mysql') 83 | ORDER BY data_length + index_length DESC; 84 | """ 85 | 86 | TABLES_SIZE_ROWS = ['table', 'database', 'rows', 'full_name', 'size', 'indexes', 'total', 'index_fraction'] 87 | 88 | 89 | def _connect(self): 90 | host = self.config.get('host', 'localhost') 91 | port = self.config.get('port', 3306) 92 | user = self.config.get('user') 93 | password = self.config.get('password') 94 | 95 | socket = self.config.get('socket') 96 | 97 | 98 | if socket: 99 | self.connection = MySQLdb.connect(unix_socket=socket, user=user, passwd=password) 100 | else: 101 | self.connection = MySQLdb.connect(host=host,port=port,user=user, passwd=password) 102 | 103 | self.log.debug("Connected to MySQL") 104 | 105 | 106 | def collect(self): 107 | self._connect() 108 | 109 | cursor = self.connection.cursor() 110 | cursor.execute("SHOW /*!50002 GLOBAL */ STATUS;") 111 | results = dict(cursor.fetchall()) 112 | 113 | for k, v in results.items(): 114 | 115 | if k in self.COUNTERS: 116 | key = self.COUNTERS[k] 117 | self.counter(key, v) 118 | 119 | if k in self.GAUGES: 120 | key = self.GAUGES[k] 121 | self.gauge(key, v) 122 | 123 | 124 | cursor.execute("SELECT VERSION();") 125 | 126 | try: 127 | mysql_version = cursor.fetchone()[0] 128 | except: 129 | mysql_version = None 130 | 131 | self.version(mysql=mysql_version, 132 | plugin=self.VERSION, 133 | mysqldb=MySQLdb.__version__) 134 | 135 | additional_checks = { 136 | 'slow_queries': 137 | { 138 | 'query': self.SLOW_QUERIES, 139 | 'headers': self.SLOW_QUERIES_ROWS 140 | }, 141 | 'tables_size': { 142 | 'query': self.TABLES_SIZE, 143 | 'headers': self.TABLES_SIZE_ROWS 144 | } 145 | 146 | } 147 | 148 | for check, values in additional_checks.items(): 149 | query = values.get('query') 150 | headers = values.get('headers') 151 | 152 | try: 153 | cursor.execute(query) 154 | result_cursor = cursor.fetchall() 155 | except: 156 | result_cursor = False # Can't fetch 157 | 158 | if result_cursor: 159 | result_dict = { 160 | 'headers': headers, 161 | 'data': [] 162 | } 163 | for r in result_cursor: 164 | normalized_row = map(self.normalize_row_value, r) 165 | result_dict['data'].append(normalized_row) 166 | 167 | self.result[check] = result_dict 168 | 169 | cursor.close() 170 | self.connection.close() 171 | del cursor -------------------------------------------------------------------------------- /mysql/mysql.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/mysql" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled" 7 | name: "mysql" 8 | password: "{{ lookup('password', '/tmp/passwordfile chars=ascii_letters') }}" 9 | vars_prompt: 10 | - name: "root_user" 11 | prompt: "Please enter your root username (This will be used to create a readonly amon user)" 12 | private: no 13 | default: "root" 14 | - name: "root_password" 15 | prompt: "Please enter your root password" 16 | private: no 17 | tasks: 18 | - 19 | name: Install python-devel 20 | yum: name=python-devel state=latest 21 | when: ansible_os_family == "RedHat" 22 | - 23 | name: Install MySQL-python 24 | yum: name=MySQL-python 25 | when: ansible_os_family == "RedHat" 26 | - 27 | name: Install python-dev 28 | apt: name=python-dev 29 | when: ansible_os_family == "Debian" 30 | - 31 | name: Install libmysqlclient 32 | apt: name=libmysqlclient-dev 33 | when: ansible_os_family == "Debian" 34 | - 35 | name: Install python-mysqldb 36 | apt: name=python-mysqldb 37 | when: ansible_os_family == "Debian" 38 | - 39 | template: src={{ plugins_path }}/{{ name }}.conf.example dest={{ enabled_plugins }}/{{ name }}.conf 40 | - 41 | name: Add Amon user 42 | mysql_user: login_user={{root_user}} login_password={{root_password}} name=amon password={{ password }} priv=*.*:"REPLICATION CLIENT,SELECT" state=present -------------------------------------------------------------------------------- /mysql/requirements.txt: -------------------------------------------------------------------------------- 1 | mysql-python -------------------------------------------------------------------------------- /nginx/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.3 2 | ======= 3 | 4 | * Collect specific request information - per url and not found 5 | 6 | 1.0.1 7 | ======= 8 | 9 | * CentOS specific installation -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amonbase:latest 2 | 3 | RUN apt-get install -y --force-yes amon-agent nginx 4 | 5 | 6 | RUN /etc/init.d/amon-agent status 7 | 8 | ADD hosts /etc/amonagent/hosts 9 | ADD nginx/nginx.yml /etc/amonagent/plugins/nginx/nginx.yml 10 | ADD nginx/nginx.conf.example /etc/amonagent/plugins/nginx/nginx.conf.example 11 | ADD nginx/status_template /etc/amonagent/plugins/nginx/status_template 12 | 13 | RUN ansible-playbook /etc/amonagent/plugins/nginx/nginx.yml -i /etc/amonagent/hosts -v 14 | 15 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /nginx/README.md: -------------------------------------------------------------------------------- 1 | # Nginx Plugin 2 | 3 | 4 | Monitors Nginx. Sends to Amon the the following metrics: 5 | 6 | - Connections - reading, writing, waiting 7 | - Requests per second 8 | 9 | ## Installing 10 | 11 | The Nginx plugin requires Amon agent version 0.8+. To check your agent version: `$ /etc/init.d/amon-agent status`. 12 | You also need to enable the `HttpStubStatusModule` - http://wiki.nginx.org/HttpStubStatusModule in your Nginx configuration files 13 | 14 | To install the plugin: 15 | 16 | 17 | $ cp /etc/amonagent/plugins/nginx/nginx.conf.example /etc/amonagent/plugins-enabled/nginx.conf 18 | 19 | 20 | ## Configuration 21 | 22 | * **status_url** - Your Nginx status url. 23 | 24 | 25 | ## Testing 26 | 27 | To test the installation, run the following: 28 | 29 | 30 | $ /etc/init.d/amon-agent plugins 31 | 32 | 33 | If everything works as expected, restart the agent and in a couple of minutes you will see the metrics in Amon 34 | -------------------------------------------------------------------------------- /nginx/nginx.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "status_url": "{{ status_url }}" 3 | } -------------------------------------------------------------------------------- /nginx/nginx.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | import sys 4 | import subprocess 5 | import tempfile 6 | import json 7 | 8 | from amonagent.modules.plugins import AmonPlugin 9 | 10 | class NginxPlugin(AmonPlugin): 11 | 12 | VERSION = '1.0.3' 13 | 14 | """Tracks basic nginx metrics via the status module 15 | * number of connections 16 | * number of requets per second 17 | 18 | Requires nginx to have the status option compiled. 19 | See http://wiki.nginx.org/HttpStubStatusModule for more details 20 | 21 | $ curl http://localhost/nginx_status/ 22 | Active connections: 8 23 | server accepts handled requests 24 | 1156958 1156958 4491319 25 | Reading: 0 Writing: 2 Waiting: 6 26 | 27 | """ 28 | 29 | 30 | def collect(self): 31 | status_url = self.config.get('status_url') 32 | log_file = self.config.get('log_file', "") 33 | 34 | 35 | try: 36 | response = requests.get(status_url, timeout=5) 37 | except Exception, e: 38 | self.error(e) 39 | 40 | 41 | whitelist = ['accepts','handled','requests'] 42 | 43 | 44 | try: 45 | status_code = response.status_code 46 | except: 47 | status_code = None 48 | 49 | if status_code == 200: 50 | status = response.text.splitlines() 51 | for line in status: 52 | stats_line = re.match('\s*(\d+)\s+(\d+)\s+(\d+)\s*$', line) 53 | if stats_line: 54 | result = {} 55 | for i, key in enumerate(whitelist): 56 | key = self.normalize(key) 57 | value = int(stats_line.group(i+1)) 58 | result[key] = value 59 | 60 | if len(result) > 0: 61 | requests_per_second = 0 62 | total_requests = result.get('requests', 0) 63 | handled = result.get('handled', 0) 64 | 65 | if total_requests > 0 and handled > 0: 66 | requests_per_second = total_requests/handled 67 | 68 | self.gauge('requests.per.second', requests_per_second) 69 | 70 | else: 71 | for (key,val) in re.findall('(\w+):\s*(\d+)', line): 72 | key = self.normalize(key, prefix='connections') 73 | self.gauge(key, int(val)) 74 | 75 | 76 | # Get detailed stats with goaccess 77 | configfile = tempfile.NamedTemporaryFile() 78 | log_content = """ 79 | date_format %d/%b/%Y 80 | log_format %h %^[%d:%t %^] "%r" %s %b "%R" "%u" 81 | time_format %H:%M:%S 82 | """ 83 | configfile.write(log_content) 84 | configfile.flush() 85 | 86 | command = ["goaccess", "-f", log_file, "-p", configfile.name, "-o", "json"] 87 | 88 | server_data = subprocess.Popen(command, stdout=subprocess.PIPE if format else None) 89 | out, err = server_data.communicate() 90 | 91 | try: 92 | json_result = json.loads(out) 93 | except: 94 | json_result = None 95 | 96 | if json_result: 97 | general = json_result.get('general') 98 | requests_result = json_result.get('requests') 99 | not_found_result = json_result.get('not_found') 100 | 101 | ignored_general_keys = ['date_time', 'log_path'] 102 | for key, value in general.items(): 103 | if key not in ignored_general_keys: 104 | name = self.normalize(key) 105 | self.counter(name, value) 106 | 107 | loop_over = {'requests': requests_result, 'not_found': not_found_result} 108 | for key, data in loop_over.items(): 109 | if data: 110 | result = {'data': []} 111 | 112 | try: 113 | headers = data[0].keys() 114 | except: 115 | headers = None 116 | 117 | if headers: 118 | result['headers'] = headers 119 | 120 | for r in data: 121 | result['data'].append(r.values()) 122 | 123 | self.result[key] = result 124 | 125 | -------------------------------------------------------------------------------- /nginx/nginx.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/nginx" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled" 7 | name: "nginx" 8 | status_url: "http://127.0.0.1:2464/nginx_status" 9 | goaccess_list_file: "/etc/apt/sources.list.d/goaccess.list" 10 | tasks: 11 | - name: Check Nginx directory structure 12 | stat: path=/etc/nginx/sites-enabled 13 | register: nginx_dir 14 | - 15 | template: src={{ plugins_path }}/{{ name }}.conf.example dest={{ enabled_plugins }}/{{ name }}.conf 16 | - 17 | template: src={{ plugins_path }}/status_template dest=/etc/nginx/sites-enabled/amon 18 | when: nginx_dir.stat.exists 19 | 20 | - 21 | template: src={{ plugins_path }}/status_template dest=/etc/nginx/conf.d/amon.conf 22 | when: not nginx_dir.stat.exists 23 | - 24 | service: name=nginx state=restarted 25 | 26 | 27 | - 28 | apt_key: url=http://deb.goaccess.io/gnugpg.key state=present 29 | when: ansible_os_family == "Debian" 30 | - 31 | name: Create Goaccess list file 32 | lineinfile: dest={{goaccess_list_file}} regexp='^' line='' state=present create=True 33 | when: ansible_os_family == "Debian" 34 | - 35 | name: Add Goaccess Repository. 36 | lineinfile: "dest={{goaccess_list_file}} line='deb http://deb.goaccess.io {{ ansible_distribution_release }} main'" 37 | when: ansible_os_family == "Debian" 38 | - 39 | apt: name=goaccess state=latest update_cache=yes 40 | when: ansible_os_family == "Debian" 41 | - 42 | yum: name=goaccess state=latest 43 | when: ansible_os_family == "RedHat" -------------------------------------------------------------------------------- /nginx/status_template: -------------------------------------------------------------------------------- 1 | server { 2 | listen 127.0.0.1:2464; 3 | server_name amon_monitoring; 4 | 5 | location /nginx_status { 6 | stub_status on; 7 | allow 127.0.0.1; 8 | deny all; 9 | } 10 | } -------------------------------------------------------------------------------- /postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amonbase:latest 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y --force-yes amon-agent postgresql 5 | 6 | ADD hosts /etc/amonagent/hosts 7 | ADD postgres/postgres.yml /etc/amonagent/plugins/postgres/postgres.yml 8 | ADD postgres/postgres.conf.example /etc/amonagent/plugins/postgres/postgres.conf.example 9 | 10 | RUN ansible-playbook /etc/amonagent/plugins/postgres/postgres.yml -i /etc/amonagent/hosts -v 11 | 12 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /postgres/README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL Plugin 2 | 3 | 4 | Monitors PostgreSQL. Parses the output from the `pg_stats_database` query http://www.postgresql.org/docs/9.2/static/monitoring-stats.html#PG-STAT-DATABASE-VIEW 5 | 6 | 7 | ## Installing 8 | 9 | The PostgreSQL plugin requires Amon agent version 0.8+. To check your agent version: `$ /etc/init.d/amon-agent status`. 10 | 11 | To install the plugin: 12 | 13 | 14 | $ cp /etc/amonagent/plugins/postgres/postgres.conf.example /etc/amonagent/plugins-enabled/postgres.conf 15 | $ sudo pip install -r /etc/amonagent/plugins/postgres/requirements.txt 16 | 17 | ## Configuration 18 | 19 | * **host** - Your PostgreSQL host, defaults to localhost 20 | * **port** - Your PostgreSQL port, defaults to 5432 21 | * **password** - Your PostgreSQL password, defined when creating the amon user 22 | * **user** - Your PostgreSQL user, defaults to amon 23 | * **database** - The database you want to monitor 24 | 25 | 26 | Create a read-only amon user with proper access to your PostgreSQL Server. 27 | 28 | $ psql 29 | $ create user amon with password 'your-desired-password'; 30 | $ grant SELECT ON pg_stat_database to amon; 31 | 32 | 33 | ## Testing 34 | 35 | To test the installation, run the following: 36 | 37 | 38 | $ /etc/init.d/amon-agent plugins 39 | 40 | 41 | If everything works as expected, restart the agent and in a couple of minutes you will see the metrics in Amon 42 | -------------------------------------------------------------------------------- /postgres/postgres.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "user": "amon", 3 | "password": "{{ password }}", 4 | "database": "{{ database }}" 5 | } -------------------------------------------------------------------------------- /postgres/postgres.py: -------------------------------------------------------------------------------- 1 | try: 2 | import psycopg2 3 | except ImportError: 4 | raise ImportError("psycopg2 library cannot be imported. Please check the installation instruction at https://github.com/amonapp/amon-plugins/tree/master/postgres.") 5 | 6 | from amonagent.modules.plugins import AmonPlugin 7 | 8 | 9 | class PostgresPlugin(AmonPlugin): 10 | 11 | 12 | VERSION = '1.1' 13 | 14 | GAUGES = { 15 | 'numbackends': 'connections' 16 | } 17 | 18 | COUNTERS = { 19 | "xact_commit": "xact.commits", 20 | "xact_rollback": "xact.rollbacks", 21 | "blks_read": 'performance.disk_read', 22 | "blks_hit": "performance.buffer_hit", 23 | "tup_returned": 'rows.returned', 24 | "tup_fetched": 'rows.fetched', 25 | "tup_inserted": 'rows.inserted', 26 | "tup_updated": 'rows.updated', 27 | "tup_deleted": 'rows.deleted' 28 | 29 | } 30 | 31 | DESCRIPTORS = [('datname', 'db') ], 32 | 33 | 34 | DATABASE_STATS_QUERY = """ 35 | SELECT datname, 36 | %s 37 | FROM pg_stat_database 38 | WHERE datname not ilike 'template%%' 39 | AND datname not ilike 'postgres' 40 | """ 41 | 42 | 43 | SLOW_QUERIES = """ 44 | SELECT * FROM 45 | (SELECT 46 | calls, 47 | round(total_time :: NUMERIC, 1) AS total, 48 | round( 49 | (total_time / calls) :: NUMERIC, 50 | 3 51 | ) AS per_call, 52 | regexp_replace(query, '[ \t\n]+', ' ', 'g') AS query 53 | FROM 54 | pg_stat_statements 55 | WHERE 56 | calls > 100 57 | ORDER BY 58 | total_time / calls DESC 59 | LIMIT 15 60 | ) AS inner_table 61 | WHERE 62 | per_call > 5 63 | """ 64 | 65 | SLOW_QUERIES_ROWS = ['calls','total','per_call','query'] 66 | 67 | MISSING_INDEXES_QUERY = """ 68 | SELECT 69 | relname AS table, 70 | CASE idx_scan 71 | WHEN 0 THEN 'Insufficient data' 72 | ELSE (100 * idx_scan / (seq_scan + idx_scan))::text 73 | END percent_of_times_index_used, 74 | n_live_tup rows_in_table 75 | FROM 76 | pg_stat_user_tables 77 | WHERE 78 | idx_scan > 0 79 | AND (100 * idx_scan / (seq_scan + idx_scan)) < 95 80 | AND n_live_tup >= 10000 81 | ORDER BY 82 | n_live_tup DESC, 83 | relname ASC 84 | """ 85 | MISSING_INDEXES_ROWS = ['table', 'percent_of_times_index_used', 'rows_in_table'] 86 | 87 | 88 | TABLES_SIZE_QUERY = """ 89 | SELECT 90 | C .relname AS NAME, 91 | CASE 92 | WHEN C .relkind = 'r' THEN 93 | 'table' 94 | ELSE 95 | 'index' 96 | END AS TYPE, 97 | pg_table_size(C .oid) AS SIZE 98 | FROM 99 | pg_class C 100 | LEFT JOIN pg_namespace n ON (n.oid = C .relnamespace) 101 | WHERE 102 | n.nspname NOT IN ( 103 | 'pg_catalog', 104 | 'information_schema' 105 | ) 106 | AND n.nspname !~ '^pg_toast' 107 | AND C .relkind IN ('r', 'i') 108 | ORDER BY 109 | pg_table_size (C .oid) DESC, 110 | NAME ASC 111 | """ 112 | 113 | TABLES_SIZE_ROWS = ['name','type','size'] 114 | 115 | # https://gist.github.com/mattsoldo/3853455 116 | INDEX_CACHE_HIT_RATE = """ 117 | -- Index hit rate 118 | WITH idx_hit_rate as ( 119 | SELECT 120 | relname as table_name, 121 | n_live_tup, 122 | round(100.0 * idx_scan / (seq_scan + idx_scan + 0.000001),2) as idx_hit_rate 123 | FROM pg_stat_user_tables 124 | ORDER BY n_live_tup DESC 125 | ), 126 | 127 | -- Cache hit rate 128 | cache_hit_rate as ( 129 | SELECT 130 | relname as table_name, 131 | heap_blks_read + heap_blks_hit as reads, 132 | round(100.0 * sum (heap_blks_read + heap_blks_hit) over (ORDER BY heap_blks_read + heap_blks_hit DESC) / sum(heap_blks_read + heap_blks_hit + 0.000001) over (),4) as cumulative_pct_reads, 133 | round(100.0 * heap_blks_hit / (heap_blks_hit + heap_blks_read + 0.000001),2) as cache_hit_rate 134 | FROM pg_statio_user_tables 135 | WHERE heap_blks_hit + heap_blks_read > 0 136 | ORDER BY 2 DESC 137 | ) 138 | 139 | SELECT 140 | idx_hit_rate.table_name, 141 | idx_hit_rate.n_live_tup as size, 142 | cache_hit_rate.reads, 143 | cache_hit_rate.cumulative_pct_reads, 144 | idx_hit_rate.idx_hit_rate, 145 | cache_hit_rate.cache_hit_rate 146 | FROM idx_hit_rate, cache_hit_rate 147 | WHERE idx_hit_rate.table_name = cache_hit_rate.table_name 148 | AND cumulative_pct_reads < 100.0 149 | ORDER BY reads DESC; 150 | """ 151 | 152 | INDEX_CACHE_HIT_RATE_ROWS = ['table_name','size','reads', 'cumulative_pct_reads', 'index_hit_rate', 'cache_hit_rate'] 153 | 154 | def _get_connection(self): 155 | 156 | 157 | host = self.config.get('host', 'localhost') 158 | port = self.config.get('port', 5432) 159 | user = self.config.get('user') 160 | password = self.config.get('password') 161 | 162 | # Used in collect 163 | self.database = self.config.get("database") 164 | 165 | 166 | self.connection = psycopg2.connect(host=host, user=user, password=password, database=self.database) 167 | 168 | 169 | try: 170 | self.connection.autocommit = True 171 | except AttributeError: 172 | # connection.autocommit was added in version 2.4.2 173 | from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT 174 | self.connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT) 175 | 176 | 177 | def collect(self): 178 | self._get_connection() 179 | 180 | cursor = self.connection.cursor() 181 | cols = self.GAUGES.keys() + self.COUNTERS.keys() 182 | 183 | query = self.DATABASE_STATS_QUERY % (", ".join(cols)) 184 | self.log.debug("Running query: %s" % query) 185 | cursor.execute(query) 186 | 187 | result = cursor.fetchall() 188 | 189 | field_names =[f[0] for f in cursor.description] 190 | 191 | for row in result: 192 | assert len(row) == len(field_names) 193 | 194 | 195 | values = dict(zip(field_names, list(row))) 196 | 197 | # Save the values only for the database defined in the config file 198 | datname = values.get('datname') 199 | 200 | if datname == self.database: 201 | for k,v in values.items(): 202 | 203 | if k in self.GAUGES: 204 | key = self.GAUGES[k] 205 | self.gauge(key, v) 206 | 207 | if k in self.COUNTERS: 208 | key = self.COUNTERS[k] 209 | 210 | self.counter(key, v) 211 | 212 | 213 | 214 | cursor.execute("SELECT version();") 215 | result = cursor.fetchone() 216 | 217 | cursor.execute("SELECT pg_database_size(current_database())") 218 | result = cursor.fetchone() 219 | 220 | try: 221 | db_size = result[0] 222 | except: 223 | db_size = False 224 | if db_size: 225 | self.gauge('dbsize', db_size) 226 | 227 | additional_checks = { 228 | 'slow_queries': 229 | { 230 | 'query': self.SLOW_QUERIES, 231 | 'headers': self.SLOW_QUERIES_ROWS 232 | }, 233 | 'tables_size': { 234 | 'query': self.TABLES_SIZE_QUERY, 235 | 'headers': self.TABLES_SIZE_ROWS 236 | }, 237 | 'index_hit_rate': { 238 | 'query': self.INDEX_CACHE_HIT_RATE, 239 | 'headers': self.INDEX_CACHE_HIT_RATE_ROWS 240 | } 241 | 242 | } 243 | 244 | 245 | for check, values in additional_checks.items(): 246 | query = values.get('query') 247 | headers = values.get('headers') 248 | 249 | try: 250 | cursor.execute(query) 251 | result_cursor = cursor.fetchall() 252 | except: 253 | result_cursor = False # Can't fetch 254 | 255 | if result_cursor: 256 | result_dict = { 257 | 'headers': headers, 258 | 'data': [] 259 | } 260 | for r in result_cursor: 261 | normalized_row = map(self.normalize_row_value, r) 262 | result_dict['data'].append(normalized_row) 263 | 264 | self.result[check] = result_dict 265 | 266 | if result: 267 | self.version(psycopg2=psycopg2.__version__, 268 | postgres=result, 269 | plugin=self.VERSION) 270 | 271 | cursor.close() 272 | self.connection.close() -------------------------------------------------------------------------------- /postgres/postgres.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/postgres" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled" 7 | name: "postgres" 8 | password: "{{ lookup('password', '/tmp/passwordfile chars=ascii_letters') }}" 9 | vars_prompt: 10 | - name: "database" 11 | prompt: "Please select the PostgreSQL database you wish to monitor" 12 | private: no 13 | tasks: 14 | - 15 | name: install Postgres Dependencies 16 | yum: pkg={{item}} state=installed 17 | with_items: 18 | - libpq-dev 19 | - python-devel 20 | - python-psycopg2 21 | - postgresql-contrib 22 | when: ansible_os_family == "RedHat" 23 | 24 | - name: install Postgres Dependencies 25 | apt: pkg={{item}} state=installed update_cache=yes 26 | with_items: 27 | - libpq-dev 28 | - python-dev 29 | - python-psycopg2 30 | - postgresql-contrib 31 | when: ansible_os_family == "Debian" 32 | 33 | - 34 | template: src={{ plugins_path }}/{{ name }}.conf.example dest={{ enabled_plugins }}/{{ name }}.conf 35 | - 36 | service: name=postgresql state=started 37 | - 38 | name: Add Amon user 39 | sudo: True 40 | sudo_user: postgres 41 | postgresql_user: name=amon db={{database}} password={{password}} priv=CONNECT 42 | - 43 | name: Test the config file 44 | command: cat {{ enabled_plugins }}/{{ name }}.conf 45 | - 46 | name: Grant SELECT privilegies 47 | sudo: True 48 | sudo_user: postgres 49 | postgresql_privs: 50 | database={{ database }} 51 | state=present 52 | objs=ALL_IN_SCHEMA 53 | privs=SELECT 54 | type=table 55 | roles=amon 56 | grant_option=yes 57 | 58 | - name: Ensure postgresql pg_stat_statements extension is created 59 | sudo: True 60 | sudo_user: postgres 61 | shell: "psql {{database}} -c 'CREATE EXTENSION pg_stat_statements;'" 62 | register: psql_result 63 | failed_when: > 64 | psql_result.rc != 0 and ("already exists" not in psql_result.stderr) 65 | changed_when: "psql_result.rc == 0" -------------------------------------------------------------------------------- /postgres/requirements.txt: -------------------------------------------------------------------------------- 1 | psycopg2 2 | -------------------------------------------------------------------------------- /redisdb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amonbase:latest 2 | 3 | RUN apt-get install -y --force-yes amon-agent redis-server 4 | 5 | 6 | RUN /etc/init.d/amon-agent status 7 | 8 | ADD hosts /etc/amonagent/hosts 9 | ADD redisdb/redisdb.yml /etc/amonagent/plugins/redisdb/redisdb.yml 10 | ADD redisdb/redisdb.conf.example /etc/amonagent/plugins/redisdb/redisdb.conf.example 11 | 12 | RUN ansible-playbook /etc/amonagent/plugins/redisdb/redisdb.yml -i /etc/amonagent/hosts -v 13 | 14 | CMD ["/bin/bash"] -------------------------------------------------------------------------------- /redisdb/README.md: -------------------------------------------------------------------------------- 1 | Monitors Redis parsing the output from the `INFO` command - http://redis.io/commands/INFO. Sends to Amon the the following metrics: 2 | 3 | 4 | ## Installing 5 | 6 | The Redis plugin requires Amon agent version 0.8+. To check your agent version: `$ /etc/init.d/amon-agent status` 7 | 8 | To install the plugin: 9 | 10 | 11 | $ cp /etc/amonagent/plugins/redisdb/redisdb.conf.example /etc/amonagent/plugins-enabled/redisdb.conf 12 | $ sudo pip install -r /etc/amonagent/plugins/redisdb/requirements.txt 13 | 14 | 15 | ## Configuration 16 | 17 | * **host** - Your Redis host, defaults to localhost 18 | * **port** - Your Redis port, defaults to 6379 19 | * **password** - Your Redis password (Optional) 20 | * **database** - (Optional, defaults to 0) 21 | 22 | ## Testing 23 | 24 | To test the installation, run the following: 25 | 26 | 27 | $ /etc/init.d/amon-agent plugins 28 | 29 | 30 | If everything works as expected, restart the agent and in a couple of minutes you will see the metrics in Amon 31 | -------------------------------------------------------------------------------- /redisdb/redisdb.conf.example: -------------------------------------------------------------------------------- 1 | { 2 | "url": "{{ url }}" 3 | } -------------------------------------------------------------------------------- /redisdb/redisdb.py: -------------------------------------------------------------------------------- 1 | import redis 2 | 3 | from amonagent.modules.plugins import AmonPlugin 4 | 5 | class RedisPlugin(AmonPlugin): 6 | 7 | VERSION = '1.1' 8 | 9 | GAUGES = { 10 | # Append-only metrics 11 | 'aof_last_rewrite_time_sec': 'aof.last_rewrite_time', 12 | 'aof_rewrite_in_progress': 'aof.rewrite', 13 | 'aof_current_size': 'aof.size', 14 | 'aof_buffer_length': 'aof.buffer_length', 15 | 16 | # Network 17 | 'connected_clients': 'net.clients', 18 | 'connected_slaves': 'net.slaves', 19 | 'rejected_connections': 'net.rejected', 20 | 21 | # clients 22 | 'blocked_clients': 'clients.blocked', 23 | 'client_biggest_input_buf': 'clients.biggest_input_buf', 24 | 'client_longest_output_list': 'clients.longest_output_list', 25 | 26 | # Keys 27 | 'evicted_keys': 'keys.evicted', 28 | 'expired_keys': 'keys.expired', 29 | 30 | # stats 31 | 'keyspace_hits': 'stats.keyspace_hits', 32 | 'keyspace_misses': 'stats.keyspace_misses', 33 | 'latest_fork_usec': 'perf.latest_fork_usec', 34 | 35 | # pubsub 36 | 'pubsub_channels': 'pubsub.channels', 37 | 'pubsub_patterns': 'pubsub.patterns', 38 | 39 | # rdb 40 | 'rdb_bgsave_in_progress': 'rdb.bgsave', 41 | 'rdb_changes_since_last_save': 'rdb.changes_since_last', 42 | 'rdb_last_bgsave_time_sec': 'rdb.last_bgsave_time', 43 | 44 | # memory 45 | 'mem_fragmentation_ratio': 'mem.fragmentation_ratio', 46 | 'used_memory': 'mem.used', 47 | 'used_memory_lua': 'mem.lua', 48 | 'used_memory_peak': 'mem.peak', 49 | 'used_memory_rss': 'mem.rss', 50 | 51 | # replication 52 | 'master_last_io_seconds_ago': 'replication.last_io_seconds_ago', 53 | 'master_sync_in_progress': 'replication.sync', 54 | 'master_sync_left_bytes': 'replication.sync_left_bytes', 55 | 56 | } 57 | 58 | 59 | def collect(self): 60 | url = self.config.get('url', 'redis://localhost:6379/0') 61 | 62 | self.conn = redis.StrictRedis.from_url(url=url) 63 | 64 | try: 65 | info = self.conn.info() 66 | except ValueError, e: 67 | raise Exception("""Unable to run the info command. This is probably an issue with your version of the python-redis library. 68 | Minimum required version: 2.4.11 69 | Your current version: %s 70 | Please upgrade to a newer version by running sudo easy_install redis""" % redis.__version__) 71 | 72 | 73 | for k in self.GAUGES: 74 | if k in info: 75 | self.gauge(self.GAUGES[k], info[k]) 76 | 77 | 78 | redis_version = info.get('redis_version') 79 | self.version(plugin=self.VERSION, redispy=redis.__version__, 80 | redis=redis_version) -------------------------------------------------------------------------------- /redisdb/redisdb.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | connection: local 3 | sudo: True 4 | vars: 5 | plugins_path: "/etc/amonagent/plugins/redisdb" 6 | enabled_plugins: "/etc/amonagent/plugins-enabled" 7 | name: "redisdb" 8 | vars_prompt: 9 | - name: "url" 10 | default: "redis://localhost:6379/0" 11 | prompt: "Please enter your Redis connection string:" 12 | private: no 13 | tasks: 14 | - template: src={{ plugins_path }}/{{ name }}.conf.example dest={{ enabled_plugins }}/{{ name }}.conf 15 | - name: Install Plugin requirements 16 | pip: requirements={{ plugins_path }}/requirements.txt 17 | - 18 | service: name=amon-agent state=restarted 19 | - 20 | service: name=redis-server state=started 21 | when: ansible_os_family == "Debian" 22 | - 23 | service: name=redis state=started 24 | when: ansible_os_family == "RedHat" -------------------------------------------------------------------------------- /redisdb/requirements.txt: -------------------------------------------------------------------------------- 1 | redis --------------------------------------------------------------------------------