-
Notifications
You must be signed in to change notification settings - Fork 814
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1308 from DataDog/leo/teamcity
[teamcity] new check
- Loading branch information
Showing
3 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |