├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── jenkins_exporter.py ├── requirements.txt └── test.py /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: python 3 | python: 4 | - "2.7" 5 | - "3.6" 6 | 7 | # command to install dependencies 8 | install: "pip install -r requirements.txt" 9 | # command to run tests 10 | script: python test.py 11 | # use new travis-ci container-based infrastructure 12 | sudo: false 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2-slim 2 | 3 | RUN mkdir -p /usr/src/app 4 | WORKDIR /usr/src/app 5 | 6 | COPY requirements.txt /usr/src/app 7 | RUN pip install --no-cache-dir -r requirements.txt 8 | 9 | COPY jenkins_exporter.py /usr/src/app 10 | 11 | EXPOSE 9118 12 | ENV JENKINS_SERVER=http://jenkins:8080 VIRTUAL_PORT=9118 DEBUG=0 13 | 14 | ENTRYPOINT [ "python", "-u", "./jenkins_exporter.py" ] 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, LOVOO GmbH 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of LOVOO GmbH nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGENAME?=lovoo/jenkins_exporter 2 | TAG?=latest 3 | JENKINS_SERVER?=https://myjenkins 4 | 5 | debug: image 6 | docker run --rm -p 9118:9118 -e DEBUG=1 -e JENKINS_SERVER=$(JENKINS_SERVER) -e VIRTUAL_PORT=9118 $(IMAGENAME):$(TAG) 7 | 8 | image: 9 | docker build -t $(IMAGENAME):$(TAG) . 10 | 11 | push: image 12 | docker push $(IMAGENAME):$(TAG) 13 | 14 | 15 | .PHONY: image push debug 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Exporter 2 | 3 | [![Build Status](https://api.travis-ci.org/lovoo/jenkins_exporter.svg?branch=travis_setup)](https://travis-ci.org/lovoo/jenkins_exporter) 4 | 5 | Jenkins exporter for prometheus.io, written in python. 6 | 7 | This exporter is based on Robust Perception's python exporter example: 8 | For more information see (http://www.robustperception.io/writing-a-jenkins-exporter-in-python) 9 | 10 | ## Usage 11 | 12 | jenkins_exporter.py [-h] [-j jenkins] [--user user] [-k] 13 | [--password password] [-p port] 14 | 15 | optional arguments: 16 | -h, --help show this help message and exit 17 | -j jenkins, --jenkins jenkins 18 | server url from the jenkins api 19 | --user user jenkins api user 20 | --password password jenkins api password 21 | -p port, --port port Listen to this port 22 | -k, --insecure Allow connection to insecure Jenkins API 23 | 24 | #### Example 25 | 26 | docker run -d -p 9118:9118 lovoo/jenkins_exporter:latest -j http://jenkins:8080 -p 9118 27 | 28 | 29 | ## Installation 30 | 31 | git clone git@github.com:lovoo/jenkins_exporter.git 32 | cd jenkins_exporter 33 | pip install -r requirements.txt 34 | 35 | ## Contributing 36 | 37 | 1. Fork it! 38 | 2. Create your feature branch: `git checkout -b my-new-feature` 39 | 3. Commit your changes: `git commit -am 'Add some feature'` 40 | 4. Push to the branch: `git push origin my-new-feature` 41 | 5. Submit a pull request 42 | -------------------------------------------------------------------------------- /jenkins_exporter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import re 4 | import time 5 | import requests 6 | import argparse 7 | from pprint import pprint 8 | 9 | import os 10 | from sys import exit 11 | from prometheus_client import start_http_server, Summary 12 | from prometheus_client.core import GaugeMetricFamily, REGISTRY 13 | 14 | DEBUG = int(os.environ.get('DEBUG', '0')) 15 | 16 | COLLECTION_TIME = Summary('jenkins_collector_collect_seconds', 'Time spent to collect metrics from Jenkins') 17 | 18 | class JenkinsCollector(object): 19 | # The build statuses we want to export about. 20 | statuses = ["lastBuild", "lastCompletedBuild", "lastFailedBuild", 21 | "lastStableBuild", "lastSuccessfulBuild", "lastUnstableBuild", 22 | "lastUnsuccessfulBuild"] 23 | 24 | def __init__(self, target, user, password, insecure): 25 | self._target = target.rstrip("/") 26 | self._user = user 27 | self._password = password 28 | self._insecure = insecure 29 | 30 | def collect(self): 31 | start = time.time() 32 | 33 | # Request data from Jenkins 34 | jobs = self._request_data() 35 | 36 | self._setup_empty_prometheus_metrics() 37 | 38 | for job in jobs: 39 | name = job['fullName'] 40 | if DEBUG: 41 | print("Found Job: {}".format(name)) 42 | pprint(job) 43 | self._get_metrics(name, job) 44 | 45 | for status in self.statuses: 46 | for metric in self._prometheus_metrics[status].values(): 47 | yield metric 48 | 49 | duration = time.time() - start 50 | COLLECTION_TIME.observe(duration) 51 | 52 | def _request_data(self): 53 | # Request exactly the information we need from Jenkins 54 | url = '{0}/api/json'.format(self._target) 55 | jobs = "[fullName,number,timestamp,duration,actions[queuingDurationMillis,totalDurationMillis," \ 56 | "skipCount,failCount,totalCount,passCount]]" 57 | tree = 'jobs[fullName,url,{0}]'.format(','.join([s + jobs for s in self.statuses])) 58 | params = { 59 | 'tree': tree, 60 | } 61 | 62 | def parsejobs(myurl): 63 | # params = tree: jobs[name,lastBuild[number,timestamp,duration,actions[queuingDurationMillis... 64 | if self._user and self._password: 65 | response = requests.get(myurl, params=params, auth=(self._user, self._password), verify=(not self._insecure)) 66 | else: 67 | response = requests.get(myurl, params=params, verify=(not self._insecure)) 68 | if DEBUG: 69 | pprint(response.text) 70 | if response.status_code != requests.codes.ok: 71 | raise Exception("Call to url %s failed with status: %s" % (myurl, response.status_code)) 72 | result = response.json() 73 | if DEBUG: 74 | pprint(result) 75 | 76 | jobs = [] 77 | for job in result['jobs']: 78 | if job['_class'] == 'com.cloudbees.hudson.plugins.folder.Folder' or \ 79 | job['_class'] == 'jenkins.branch.OrganizationFolder' or \ 80 | job['_class'] == 'org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject': 81 | jobs += parsejobs(job['url'] + '/api/json') 82 | else: 83 | jobs.append(job) 84 | return jobs 85 | 86 | return parsejobs(url) 87 | 88 | def _setup_empty_prometheus_metrics(self): 89 | # The metrics we want to export. 90 | self._prometheus_metrics = {} 91 | for status in self.statuses: 92 | snake_case = re.sub('([A-Z])', '_\\1', status).lower() 93 | self._prometheus_metrics[status] = { 94 | 'number': 95 | GaugeMetricFamily('jenkins_job_{0}'.format(snake_case), 96 | 'Jenkins build number for {0}'.format(status), labels=["jobname"]), 97 | 'duration': 98 | GaugeMetricFamily('jenkins_job_{0}_duration_seconds'.format(snake_case), 99 | 'Jenkins build duration in seconds for {0}'.format(status), labels=["jobname"]), 100 | 'timestamp': 101 | GaugeMetricFamily('jenkins_job_{0}_timestamp_seconds'.format(snake_case), 102 | 'Jenkins build timestamp in unixtime for {0}'.format(status), labels=["jobname"]), 103 | 'queuingDurationMillis': 104 | GaugeMetricFamily('jenkins_job_{0}_queuing_duration_seconds'.format(snake_case), 105 | 'Jenkins build queuing duration in seconds for {0}'.format(status), 106 | labels=["jobname"]), 107 | 'totalDurationMillis': 108 | GaugeMetricFamily('jenkins_job_{0}_total_duration_seconds'.format(snake_case), 109 | 'Jenkins build total duration in seconds for {0}'.format(status), labels=["jobname"]), 110 | 'skipCount': 111 | GaugeMetricFamily('jenkins_job_{0}_skip_count'.format(snake_case), 112 | 'Jenkins build skip counts for {0}'.format(status), labels=["jobname"]), 113 | 'failCount': 114 | GaugeMetricFamily('jenkins_job_{0}_fail_count'.format(snake_case), 115 | 'Jenkins build fail counts for {0}'.format(status), labels=["jobname"]), 116 | 'totalCount': 117 | GaugeMetricFamily('jenkins_job_{0}_total_count'.format(snake_case), 118 | 'Jenkins build total counts for {0}'.format(status), labels=["jobname"]), 119 | 'passCount': 120 | GaugeMetricFamily('jenkins_job_{0}_pass_count'.format(snake_case), 121 | 'Jenkins build pass counts for {0}'.format(status), labels=["jobname"]), 122 | } 123 | 124 | def _get_metrics(self, name, job): 125 | for status in self.statuses: 126 | if status in job.keys(): 127 | status_data = job[status] or {} 128 | self._add_data_to_prometheus_structure(status, status_data, job, name) 129 | 130 | def _add_data_to_prometheus_structure(self, status, status_data, job, name): 131 | # If there's a null result, we want to pass. 132 | if status_data.get('duration', 0): 133 | self._prometheus_metrics[status]['duration'].add_metric([name], status_data.get('duration') / 1000.0) 134 | if status_data.get('timestamp', 0): 135 | self._prometheus_metrics[status]['timestamp'].add_metric([name], status_data.get('timestamp') / 1000.0) 136 | if status_data.get('number', 0): 137 | self._prometheus_metrics[status]['number'].add_metric([name], status_data.get('number')) 138 | actions_metrics = status_data.get('actions', [{}]) 139 | for metric in actions_metrics: 140 | if metric.get('queuingDurationMillis', False): 141 | self._prometheus_metrics[status]['queuingDurationMillis'].add_metric([name], metric.get('queuingDurationMillis') / 1000.0) 142 | if metric.get('totalDurationMillis', False): 143 | self._prometheus_metrics[status]['totalDurationMillis'].add_metric([name], metric.get('totalDurationMillis') / 1000.0) 144 | if metric.get('skipCount', False): 145 | self._prometheus_metrics[status]['skipCount'].add_metric([name], metric.get('skipCount')) 146 | if metric.get('failCount', False): 147 | self._prometheus_metrics[status]['failCount'].add_metric([name], metric.get('failCount')) 148 | if metric.get('totalCount', False): 149 | self._prometheus_metrics[status]['totalCount'].add_metric([name], metric.get('totalCount')) 150 | # Calculate passCount by subtracting fails and skips from totalCount 151 | passcount = metric.get('totalCount') - metric.get('failCount') - metric.get('skipCount') 152 | self._prometheus_metrics[status]['passCount'].add_metric([name], passcount) 153 | 154 | 155 | def parse_args(): 156 | parser = argparse.ArgumentParser( 157 | description='jenkins exporter args jenkins address and port' 158 | ) 159 | parser.add_argument( 160 | '-j', '--jenkins', 161 | metavar='jenkins', 162 | required=False, 163 | help='server url from the jenkins api', 164 | default=os.environ.get('JENKINS_SERVER', 'http://jenkins:8080') 165 | ) 166 | parser.add_argument( 167 | '--user', 168 | metavar='user', 169 | required=False, 170 | help='jenkins api user', 171 | default=os.environ.get('JENKINS_USER') 172 | ) 173 | parser.add_argument( 174 | '--password', 175 | metavar='password', 176 | required=False, 177 | help='jenkins api password', 178 | default=os.environ.get('JENKINS_PASSWORD') 179 | ) 180 | parser.add_argument( 181 | '-p', '--port', 182 | metavar='port', 183 | required=False, 184 | type=int, 185 | help='Listen to this port', 186 | default=int(os.environ.get('VIRTUAL_PORT', '9118')) 187 | ) 188 | parser.add_argument( 189 | '-k', '--insecure', 190 | dest='insecure', 191 | required=False, 192 | action='store_true', 193 | help='Allow connection to insecure Jenkins API', 194 | default=False 195 | ) 196 | return parser.parse_args() 197 | 198 | 199 | def main(): 200 | try: 201 | args = parse_args() 202 | port = int(args.port) 203 | REGISTRY.register(JenkinsCollector(args.jenkins, args.user, args.password, args.insecure)) 204 | start_http_server(port) 205 | print("Polling {}. Serving at port: {}".format(args.jenkins, port)) 206 | while True: 207 | time.sleep(1) 208 | except KeyboardInterrupt: 209 | print(" Interrupted") 210 | exit(0) 211 | 212 | 213 | if __name__ == "__main__": 214 | main() 215 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | prometheus-client==0.1.0 2 | requests==2.10.0 3 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import unittest 4 | from jenkins_exporter import JenkinsCollector 5 | 6 | 7 | class JenkinsCollectorTestCase(unittest.TestCase): 8 | # The build statuses we want to export about. 9 | # TODO: add more test cases 10 | 11 | def test_prometheus_metrics(self): 12 | exporter = JenkinsCollector('', '', '', False) 13 | self.assertFalse(hasattr(exporter, '_prometheus_metrics')) 14 | 15 | exporter._setup_empty_prometheus_metrics() 16 | self.assertTrue(hasattr(exporter, '_prometheus_metrics')) 17 | self.assertEqual(sorted(exporter._prometheus_metrics.keys()), sorted(JenkinsCollector.statuses)) 18 | 19 | 20 | if __name__ == "__main__": 21 | unittest.main() 22 | --------------------------------------------------------------------------------