Skip to content

Commit ca0e039

Browse files
author
Kent Rancourt
committed
fix(controller): write app event logs via logspout
1 parent 9ef640f commit ca0e039

4 files changed

Lines changed: 52 additions & 43 deletions

File tree

controller/api/models.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def _close_db_connections(*args, **kwargs):
6161
def log_event(app, msg, level=logging.INFO):
6262
# controller needs to know which app this log comes from
6363
logger.log(level, "{}: {}".format(app.id, msg))
64-
app.log(msg)
64+
app.log(msg, level)
6565

6666

6767
def validate_base64(value):
@@ -215,18 +215,15 @@ def _get_command(self, container_type):
215215
# handle special case for Dockerfile deployments
216216
return '' if container_type == 'cmd' else 'start {}'.format(container_type)
217217

218-
def log(self, message):
219-
"""Logs a message to the application's log file.
218+
def log(self, message, level=logging.INFO):
219+
"""Logs a message in the context of this application.
220220
221-
This is a workaround for how Django interacts with Python's logging module. Each app
222-
needs its own FileHandler instance so it can write to its own log file. That won't work in
223-
Django's case because logging is set up before you run the server and it disables all
224-
existing logging configurations.
221+
This prefixes log messages with an application "tag" that the customized deis-logspout will
222+
be on the lookout for. When it's seen, the message-- usually an application event of some
223+
sort like releasing or scaling, will be considered as "belonging" to the application
224+
instead of the controller and will be handled accordingly.
225225
"""
226-
with open(os.path.join(settings.DEIS_LOG_DIR, self.id + '.log'), 'a') as f:
227-
msg = "{} deis[api]: {}\n".format(time.strftime(settings.DEIS_DATETIME_FORMAT),
228-
message)
229-
f.write(msg.encode('utf-8'))
226+
logger.log(level, "[{}]: {}".format(self.id, message))
230227

231228
def create(self, *args, **kwargs):
232229
"""Create a new application with an initial config and release"""

controller/api/tests/test_app.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import logging
1011
import mock
1112
import os.path
1213
import requests
@@ -131,22 +132,18 @@ def test_app_actions(self):
131132
os.remove(path)
132133
# TODO: test run needs an initial build
133134

134-
def test_app_release_notes_in_logs(self):
135+
@mock.patch('api.models.logger')
136+
def test_app_release_notes_in_logs(self, mock_logger):
135137
"""Verifies that an app's release summary is dumped into the logs."""
136138
url = '/v1/apps'
137139
body = {'id': 'autotest'}
138140
response = self.client.post(url, json.dumps(body), content_type='application/json',
139141
HTTP_AUTHORIZATION='token {}'.format(self.token))
140142
self.assertEqual(response.status_code, 201)
141-
app_id = response.data['id'] # noqa
142-
path = os.path.join(settings.DEIS_LOG_DIR, app_id + '.log')
143-
url = '/v1/apps/{app_id}/logs'.format(**locals())
144-
response = self.client.get(url,
145-
HTTP_AUTHORIZATION='token {}'.format(self.token))
146-
self.assertIn('autotest created initial release', response.data)
147-
self.assertEqual(response.status_code, 200)
148-
# delete file for future runs
149-
os.remove(path)
143+
# check app logs
144+
exp_msg = "autotest created initial release"
145+
exp_log_call = mock.call(logging.INFO, exp_msg)
146+
mock_logger.log.has_calls(exp_log_call)
150147

151148
def test_app_errors(self):
152149
app_id = 'autotest-errors'
@@ -202,7 +199,8 @@ def test_app_structure_is_valid_json(self):
202199
self.assertEqual(response.data['structure'], {"web": 1})
203200

204201
@mock.patch('requests.post', mock_import_repository_task)
205-
def test_admin_can_manage_other_apps(self):
202+
@mock.patch('api.models.logger')
203+
def test_admin_can_manage_other_apps(self, mock_logger):
206204
"""Administrators of Deis should be able to manage all applications.
207205
"""
208206
# log in as non-admin user and create an app
@@ -213,18 +211,15 @@ def test_admin_can_manage_other_apps(self):
213211
body = {'id': app_id}
214212
response = self.client.post(url, json.dumps(body), content_type='application/json',
215213
HTTP_AUTHORIZATION='token {}'.format(token))
216-
app = App.objects.get(id=app_id)
217214
# log in as admin, check to see if they have access
218215
url = '/v1/apps/{}'.format(app_id)
219216
response = self.client.get(url,
220217
HTTP_AUTHORIZATION='token {}'.format(self.token))
221218
self.assertEqual(response.status_code, 200)
222219
# check app logs
223-
url = '/v1/apps/{app_id}/logs'.format(**locals())
224-
response = self.client.get(url,
225-
HTTP_AUTHORIZATION='token {}'.format(self.token))
226-
self.assertEqual(response.status_code, 200)
227-
self.assertIn('autotest2 created initial release', response.data)
220+
exp_msg = "autotest2 created initial release"
221+
exp_log_call = mock.call(logging.INFO, exp_msg)
222+
mock_logger.log.has_calls(exp_log_call)
228223
# TODO: test run needs an initial build
229224
# delete the app
230225
url = '/v1/apps/{}'.format(app_id)

controller/api/tests/test_config.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import unicode_literals
99

1010
import json
11+
import logging
1112
import mock
1213
import requests
1314

@@ -569,7 +570,8 @@ def test_app_healthcheck_good(self):
569570
@mock.patch('requests.get', mock_status_not_found)
570571
@mock.patch('api.models.get_etcd_client', lambda func: func)
571572
@mock.patch('time.sleep', lambda func: func)
572-
def test_app_healthcheck_bad(self):
573+
@mock.patch('api.models.logger')
574+
def test_app_healthcheck_bad(self, mock_logger):
573575
"""
574576
If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller
575577
should check that the application responds with a 200 OK. If it's down, the app should be
@@ -583,10 +585,16 @@ def test_app_healthcheck_bad(self):
583585
# check that only the build and initial release exist
584586
self.assertEqual(self.app.release_set.latest().version, 2)
585587
# assert that the reason why the containers failed was because
586-
# they failed the health check 4 times
587-
self.assertEqual(
588-
self.app.logs().count("app failed health check (got '404', expected: '200')"),
589-
4)
588+
# they failed the health check 4 times; we do this by looking
589+
# at logs-- there may be a better way
590+
exp_msg = "{}: app failed health check (got '404', expected: '200'); trying again in 0.0 \
591+
seconds".format(self.app.id)
592+
exp_log_call = mock.call(logging.WARNING, exp_msg)
593+
log_calls = mock_logger.log.mock_calls
594+
self.assertEqual(log_calls.count(exp_log_call), 3)
595+
exp_msg = "{}: app failed health check (got '404', expected: '200')".format(self.app.id)
596+
exp_log_call = mock.call(logging.WARNING, exp_msg)
597+
self.assertEqual(log_calls.count(exp_log_call), 1)
590598

591599
@mock.patch('requests.get', mock_status_not_found)
592600
@mock.patch('api.models.get_etcd_client', lambda func: func)

logspout/logspout.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func syslogStreamer(target Target, types []string, logstream chan *Log) {
6464
if typestr != ",," && !strings.Contains(typestr, logline.Type) {
6565
continue
6666
}
67-
tag, pid := getLogName(logline.Name)
67+
tag, pid, data := getLogParts(logline)
6868
var conn net.Conn
6969
if strings.EqualFold(target.Protocol, "tcp") {
7070
addr, err := net.ResolveTCPAddr("tcp", target.Addr)
@@ -89,25 +89,34 @@ func syslogStreamer(target Target, types []string, logstream chan *Log) {
8989
time.Now().Format(getopt("DATETIME_FORMAT", dtime.DeisDatetimeFormat)),
9090
tag,
9191
pid,
92-
logline.Data)
92+
data)
9393
assert(err, "syslog")
9494
}
9595
}
9696

97-
// getLogName returns a custom tag and PID for containers that
97+
// getLogParts returns a custom tag and PID for containers that
9898
// match Deis' specific application name format. Otherwise,
99-
// it returns the original name and 1 as the PID.
100-
func getLogName(name string) (string, string) {
99+
// it returns the original name and 1 as the PID. Additionally,
100+
// it returns log data. The function is also smart enough to
101+
// detect when a leading tag in the log data represents an attempt
102+
// by the controller to log an application event.
103+
func getLogParts(logline *Log) (string, string, string) {
101104
// example regex that should match: go_v2.web.1
102-
match := getMatch(`(^[a-z0-9-]+)_(v[0-9]+)\.([a-z-_]+\.[0-9]+)$`, name)
105+
match := getMatch(`(^[a-z0-9-]+)_(v[0-9]+)\.([a-z-_]+\.[0-9]+)$`, logline.Name)
103106
if match != nil {
104-
return match[1], match[3]
107+
return match[1], match[3], logline.Data
105108
}
106-
match = getMatch(`^k8s_([a-z0-9-]+)-[a-z]+\.[\da-f]+_[a-z0-9-]+-([a-z]+-[\da-z]*)_`, name)
109+
match = getMatch(`^k8s_([a-z0-9-]+)-[a-z]+\.[\da-f]+_[a-z0-9-]+-([a-z]+-[\da-z]*)_`, logline.Name)
107110
if match != nil {
108-
return match[1], match[2]
111+
return match[1], match[2], logline.Data
109112
}
110-
return name, "1"
113+
if logline.Name == "deis-controller" {
114+
data_match := getMatch(`^[A-Z]+ \[([a-z0-9-]+)\]: (.*)`, logline.Data)
115+
if data_match != nil {
116+
return data_match[1], "deis-controller", data_match[2]
117+
}
118+
}
119+
return logline.Name, "1", logline.Data
111120
}
112121

113122
func getMatch(regex string, name string) []string {

0 commit comments

Comments
 (0)