Skip to content

Commit 4ed700b

Browse files
author
Matthew Fisher
committed
feat(controller): pipe release notes to app logs
Whenever a release is created for an application, its logs will now also be seen in `deis logs`. The log pattern follows the same syntax as Heroku's logs: 2014-08-25T20:23:53.275002+00:00 heroku[api]: Enable Logplex by me@bacongobbler.com 2014-08-25T20:23:53.275063+00:00 heroku[api]: Release v2 created by me@bacongobbler.com The log syntax coming from the controller looks like so in the app logs: 2014-08-25 14:25:50 deis[api]: bacongobbler created initial release Which follows the same syntax as our app logs: 2014-08-25 14:25:50 island-larkspur[web.1]: listening on port 5000...
1 parent d7f7118 commit 4ed700b

3 files changed

Lines changed: 53 additions & 9 deletions

File tree

controller/api/models.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212
import re
1313
import subprocess
14+
import time
1415

1516
from celery.canvas import group
1617
from django.conf import settings
@@ -162,6 +163,19 @@ def __str__(self):
162163
def url(self):
163164
return self.id + '.' + self.cluster.domain
164165

166+
def log(self, message):
167+
"""Logs a message to the application's log file.
168+
169+
This is a workaround for how Django interacts with Python's logging module. Each app
170+
needs its own FileHandler instance so it can write to its own log file. That won't work in
171+
Django's case because logging is set up before you run the server and it disables all
172+
existing logging configurations.
173+
"""
174+
with open(os.path.join(settings.DEIS_LOG_DIR, self.id + '.log'), 'a') as f:
175+
f.write('{} deis[api]: {}\n'.format(
176+
time.strftime('%Y-%m-%d %H:%M:%S'),
177+
message))
178+
165179
def create(self, *args, **kwargs):
166180
config = Config.objects.create(owner=self.owner, app=self)
167181
build = Build.objects.create(owner=self.owner, app=self, image=settings.DEFAULT_BUILD)
@@ -209,7 +223,7 @@ def scale(self, **kwargs): # noqa
209223
if container_type not in available_process_types:
210224
raise EnvironmentError(
211225
'Container type {} does not exist in application'.format(container_type))
212-
msg = 'Containers scaled ' + ' '.join(
226+
msg = 'containers scaled ' + ' '.join(
213227
"{}={}".format(k, v) for k, v in requested_containers.items())
214228
# iterate and scale by container type (web, worker, etc)
215229
changed = False
@@ -245,6 +259,7 @@ def scale(self, **kwargs): # noqa
245259
subtasks.append(tasks.stop_containers.s(to_remove))
246260
group(*subtasks).apply_async().join()
247261
log_event(self, msg)
262+
self.log(msg)
248263
return changed
249264

250265
def logs(self):
@@ -258,7 +273,9 @@ def logs(self):
258273
def run(self, command):
259274
"""Run a one-off command in an ephemeral app container."""
260275
# TODO: add support for interactive shell
261-
log_event(self, "deis run '{}'".format(command))
276+
msg = "deis run '{}'".format(command)
277+
log_event(self, msg)
278+
self.log(msg)
262279
c_num = max([c.num for c in self.container_set.filter(type='admin')] or [0]) + 1
263280
c = Container.objects.create(owner=self.owner,
264281
app=self,
@@ -654,28 +671,36 @@ def __str__(self):
654671
def _log_build_created(**kwargs):
655672
if kwargs.get('created'):
656673
build = kwargs['instance']
657-
log_event(build.app, "Build {} created".format(build))
674+
log_event(build.app, "build {} created".format(build))
658675

659676

660677
def _log_release_created(**kwargs):
661678
if kwargs.get('created'):
662679
release = kwargs['instance']
663-
log_event(release.app, "Release {} created".format(release))
680+
log_event(release.app, "release {} created".format(release))
681+
# append release lifecycle logs to the app
682+
release.app.log(release.summary)
664683

665684

666685
def _log_config_updated(**kwargs):
667686
config = kwargs['instance']
668-
log_event(config.app, "Config {} updated".format(config))
687+
log_event(config.app, "config {} updated".format(config))
669688

670689

671690
def _log_domain_added(**kwargs):
672691
domain = kwargs['instance']
673-
log_event(domain.app, "Domain {} added".format(domain))
692+
msg = "domain {} added".format(domain)
693+
log_event(domain.app, msg)
694+
# adding a domain does not create a release, so we have to log here
695+
domain.app.log(msg)
674696

675697

676698
def _log_domain_removed(**kwargs):
677699
domain = kwargs['instance']
678-
log_event(domain.app, "Domain {} removed".format(domain))
700+
msg = "domain {} removed".format(domain)
701+
log_event(domain.app, msg)
702+
# adding a domain does not create a release, so we have to log here
703+
domain.app.log(msg)
679704

680705

681706
def _etcd_publish_key(**kwargs):

controller/api/tests/test_app.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ def test_app_actions(self):
7777
if not os.path.exists(settings.DEIS_LOG_DIR):
7878
os.mkdir(settings.DEIS_LOG_DIR)
7979
path = os.path.join(settings.DEIS_LOG_DIR, app_id + '.log')
80+
# HACK: remove app lifecycle logs
8081
if os.path.exists(path):
8182
os.remove(path)
8283
url = '/api/apps/{app_id}/logs'.format(**locals())
8384
response = self.client.post(url)
8485
self.assertEqual(response.status_code, 204)
8586
self.assertEqual(response.data, 'No logs for {}'.format(app_id))
8687
# write out some fake log data and try again
87-
with open(path, 'w') as f:
88+
with open(path, 'a') as f:
8889
f.write(FAKE_LOG_DATA)
8990
response = self.client.post(url)
9091
self.assertEqual(response.status_code, 200)
@@ -95,6 +96,23 @@ def test_app_actions(self):
9596
response = self.client.post(url, json.dumps(body), content_type='application/json')
9697
self.assertEqual(response.status_code, 200)
9798
self.assertEqual(response.data[0], 0)
99+
# delete file for future runs
100+
os.remove(path)
101+
102+
def test_app_release_notes_in_logs(self):
103+
"""Verifies that an app's release summary is dumped into the logs."""
104+
url = '/api/apps'
105+
body = {'cluster': 'autotest', 'id': 'autotest'}
106+
response = self.client.post(url, json.dumps(body), content_type='application/json')
107+
self.assertEqual(response.status_code, 201)
108+
app_id = response.data['id'] # noqa
109+
path = os.path.join(settings.DEIS_LOG_DIR, app_id + '.log')
110+
url = '/api/apps/{app_id}/logs'.format(**locals())
111+
response = self.client.post(url)
112+
self.assertIn('autotest created initial release', response.data)
113+
self.assertEqual(response.status_code, 200)
114+
# delete file for future runs
115+
os.remove(path)
98116

99117
def test_app_errors(self):
100118
cluster_id, app_id = 'autotest', 'autotest-errors'

tests/apps_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ func appsListTest(t *testing.T, params *utils.DeisTestConfig, notflag bool) {
7474

7575
func appsLogsTest(t *testing.T, params *utils.DeisTestConfig) {
7676
cmd := appsLogsCmd
77-
utils.Execute(t, cmd, params, true, "204 NO CONTENT")
77+
// test for application lifecycle logs
78+
utils.Execute(t, cmd, params, false, "204 NO CONTENT")
7879
if err := utils.Chdir(params.ExampleApp); err != nil {
7980
t.Fatal(err)
8081
}

0 commit comments

Comments
 (0)