Skip to content

Commit

Permalink
Merge pull request #1308 from DataDog/leo/teamcity
Browse files Browse the repository at this point in the history
[teamcity] new check
  • Loading branch information
LeoCavaille committed Jan 28, 2015
2 parents 6f324ec + 56a7649 commit 9e309ed
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 0 deletions.
133 changes: 133 additions & 0 deletions checks.d/teamcity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# stdlib
import requests
import time

# project
from checks import AgentCheck
from config import _is_affirmative


class TeamCityCheck(AgentCheck):
HEADERS = {'Accept': 'application/json'}
DEFAULT_TIMEOUT = 10
NEW_BUILD_URL = "http://{server}/guestAuth/app/rest/builds/?locator=buildType:{build_conf},sinceBuild:id:{since_build},status:SUCCESS"
LAST_BUILD_URL = "http://{server}/guestAuth/app/rest/builds/?locator=buildType:{build_conf},count:1"

def __init__(self, name, init_config, agentConfig, instances=None):
AgentCheck.__init__(self, name, init_config, agentConfig, instances)

# Keep track of last build IDs per instance
self.last_build_ids = {}

def _initialize_if_required(self, instance_name, server, build_conf):
# Already initialized
if instance_name in self.last_build_ids:
return

self.log.debug("Initializing {0}".format(instance_name))
build_url = self.LAST_BUILD_URL.format(
server=server,
build_conf=build_conf
)
try:
resp = requests.get(build_url, timeout=self.DEFAULT_TIMEOUT, headers=self.HEADERS)
resp.raise_for_status()

last_build_id = resp.json().get('build')[0].get('id')
except requests.exceptions.HTTPError:
if resp.status_code == 401:
self.log.error("Access denied. You must enable guest authentication")
self.log.error(
"Failed to retrieve last build ID with code {0} for instance '{1}'"
.format(resp.status_code, instance_name)
)
raise
except Exception:
self.log.exception(
"Unhandled exception to get last build ID for instance '{0}'"
.format(instance_name)
)
raise

self.log.debug(
"Last build id for instance {0} is {1}."
.format(instance_name, last_build_id)
)
self.last_build_ids[instance_name] = last_build_id

def _build_and_send_event(self, new_build, instance_name, is_deployment, host, tags):
self.log.debug("Found new build with id {0}, saving and alerting.".format(new_build["id"]))
self.last_build_ids[instance_name] = new_build["id"]

event_dict = {
'timestamp': int(time.time()),
'source_type_name': 'teamcity',
'host': host,
'tags': [],
}
if is_deployment:
event_dict['event_type'] = 'teamcity_deployment'
event_dict['msg_title'] = "{0} deployed to {1}".format(instance_name, host)
event_dict['msg_text'] = "Build Number: {0}\n\nMore Info: {1}"\
.format(new_build["number"], new_build["webUrl"])
event_dict['tags'].append('deployment')
else:
event_dict['event_type'] = "build"
event_dict['msg_title'] = "Build for {0} successful".format(instance_name)

event_dict['msg_text'] = "Build Number: {0}\nDeployed To: {1}\n\nMore Info: {2}"\
.format(new_build["number"], host, new_build["webUrl"])
event_dict['tags'].append('build')

if tags:
event_dict["tags"].extend(tags)

self.event(event_dict)

def check(self, instance):
instance_name = instance.get('name')
if instance_name is None:
raise Exception("Each instance must have a unique name")

server = instance.get('server')
if 'server' is None:
raise Exception("Each instance must have a server")

build_conf = instance.get('build_configuration')
if build_conf is None:
raise Exception("Each instance must have a build configuration")

host = instance.get('host_affected') or self.hostname
tags = instance.get('tags')
is_deployment = _is_affirmative(instance.get('is_deployment', False))

self._initialize_if_required(instance_name, server, build_conf)

# Look for new successful builds
new_build_url = self.NEW_BUILD_URL.format(
server=server,
build_conf=build_conf,
since_build=self.last_build_ids[instance_name]
)

try:
resp = requests.get(new_build_url, timeout=self.DEFAULT_TIMEOUT, headers=self.HEADERS)
resp.raise_for_status()

new_builds = resp.json()

if new_builds["count"] == 0:
self.log.debug("No new builds found.")
else:
self._build_and_send_event(new_builds["build"][0], instance_name, is_deployment, host, tags)
except requests.exceptions.HTTPError:
self.log.exception(
"Couldn't fetch last build, got code {0}"
.format(resp.status_code)
)
raise
except Exception:
self.log.exception(
"Couldn't fetch last build, unhandled exception"
)
raise
29 changes: 29 additions & 0 deletions conf.d/teamcity.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
init_config:

# Add your different projects in here to monitor their build
# success with Datadog events
instances:
# A custom unique name per build configuration that will show
# in the events
- name: My Website

# Specify the server name of your teamcity instance here
# Guest authentication must be on if you want the check to be able to get data
server: teamcity.mycompany.com
# This is the internal build ID of the build configuration you wish to track.
# You can find it labelled as "Build configuration ID" when editing the configuration in question.
build_configuration: MyWebsite_Deploy

# Optional, if you wish to override the host that is affected by this build configuration.
# Defaults to the host that the agent is running on.
# host_affected: msicalweb6

# Optional, this changes the event message slightly to specify that TeamCity was used to deploy something
# rather than just that a successful build happened
# is_deployment: true

# Optional, any additional tags you'd like to add to the event
# tags:
# - test


90 changes: 90 additions & 0 deletions tests/test_teamcity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# stdlib
import unittest

# 3p
from mock import MagicMock, patch

# project
from tests.common import load_check

CONFIG = {
'init_config': {},
'instances': [
{
'name': 'One test build',
'server': 'localhost:8111',
'build_configuration': 'TestProject_TestBuild',
'host_affected': 'buildhost42.dtdg.co',
'is_deployment': False,
'tags': ['one:tag', 'one:test']
}
]
}

def get_mock_first_build(url, *args, **kwargs):
mock_resp = MagicMock()
if 'sinceBuild' in url:
# looking for new builds
json = {"count":0,"href":"/guestAuth/app/rest/builds/?locator=buildType:TestProject_TestBuild,sinceBuild:id:1,status:SUCCESS"}
else:
json = {"count":1,"href":"/guestAuth/app/rest/builds/?locator=buildType:TestProject_TestBuild,count:1","nextHref":"/guestAuth/app/rest/builds/?locator=buildType:TestProject_TestBuild,count:1,start:1","build":[{"id":1,"buildTypeId":"TestProject_TestBuild","number":"1","status":"SUCCESS","state":"finished","href":"/guestAuth/app/rest/builds/id:1","webUrl":"http://localhost:8111/viewLog.html?buildId=1&buildTypeId=TestProject_TestBuild"}]}

mock_resp.json.return_value = json
return mock_resp

def get_mock_one_more_build(url, *args, **kwargs):
mock_resp = MagicMock()
json = {}

if 'sinceBuild:id:1' in url:
json = {"count":1,"href":"/guestAuth/app/rest/builds/?locator=buildType:TestProject_TestBuild,sinceBuild:id:1,status:SUCCESS","build":[{"id":2,"buildTypeId":"TestProject_TestBuild","number":"2","status":"SUCCESS","state":"finished","href":"/guestAuth/app/rest/builds/id:2","webUrl":"http://localhost:8111/viewLog.html?buildId=2&buildTypeId=TestProject_TestBuild"}]}
elif 'sinceBuild:id:2' in url:
json = {"count":0,"href":"/guestAuth/app/rest/builds/?locator=buildType:TestProject_TestBuild,sinceBuild:id:2,status:SUCCESS"}

mock_resp.json.return_value = json
return mock_resp



class TeamCityCheckTest(unittest.TestCase):
"""
If you delete the cassettes at fixtures/teamcity*.
You can run the tests with a real TC server providing you
create a build configuration with the ID above in CONFIG
"""

def test_build_event(self):
agent_config = {
'version': '0.1',
'api_key': 'toto'
}
check = load_check('teamcity', CONFIG, agent_config)

with patch('requests.get', get_mock_first_build):
check.check(check.instances[0])

metrics = check.get_metrics()
self.assertEquals(len(metrics), 0)

events = check.get_events()
# Nothing should have happened because we only create events
# for newer builds
self.assertEquals(len(events), 0)

with patch('requests.get', get_mock_one_more_build):
check.check(check.instances[0])

events = check.get_events()
self.assertEquals(len(events), 1)
self.assertEquals(events[0]['msg_title'], "Build for One test build successful")
self.assertEquals(events[0]['msg_text'], "Build Number: 2\nDeployed To: buildhost42.dtdg.co\n\nMore Info: http://localhost:8111/viewLog.html?buildId=2&buildTypeId=TestProject_TestBuild")
self.assertEquals(events[0]['tags'], ['build', 'one:tag', 'one:test'])
self.assertEquals(events[0]['host'], "buildhost42.dtdg.co")


# One more check should not create any more events
with patch('requests.get', get_mock_one_more_build):
check.check(check.instances[0])

events = check.get_events()
self.assertEquals(len(events), 0)

0 comments on commit 9e309ed

Please sign in to comment.