Skip to content

Commit cd25b41

Browse files
authored
Merge pull request #1153 from minrk/log-debug
demote logs of health/liveness checks to debug-level
2 parents 856589e + 1875bcb commit cd25b41

File tree

6 files changed

+189
-39
lines changed

6 files changed

+189
-39
lines changed

binderhub/app.py

+42-38
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from .builder import BuildHandler
3131
from .health import HealthHandler
3232
from .launcher import Launcher
33+
from .log import log_request
3334
from .registry import DockerRegistry
3435
from .main import MainHandler, ParameterizedMainHandler, LegacyRedirectHandler
3536
from .repoproviders import (GitHubRepoProvider, GitRepoProvider,
@@ -586,44 +587,47 @@ def initialize(self, *args, **kwargs):
586587
with open(schema_file) as f:
587588
self.event_log.register_schema(json.load(f))
588589

589-
self.tornado_settings.update({
590-
"push_secret": self.push_secret,
591-
"image_prefix": self.image_prefix,
592-
"debug": self.debug,
593-
'launcher': self.launcher,
594-
'appendix': self.appendix,
595-
"build_namespace": self.build_namespace,
596-
"build_image": self.build_image,
597-
'build_node_selector': self.build_node_selector,
598-
'build_pool': self.build_pool,
599-
"sticky_builds": self.sticky_builds,
600-
'log_tail_lines': self.log_tail_lines,
601-
'pod_quota': self.pod_quota,
602-
'per_repo_quota': self.per_repo_quota,
603-
'per_repo_quota_higher': self.per_repo_quota_higher,
604-
'repo_providers': self.repo_providers,
605-
'use_registry': self.use_registry,
606-
'registry': registry,
607-
'traitlets_config': self.config,
608-
'google_analytics_code': self.google_analytics_code,
609-
'google_analytics_domain': self.google_analytics_domain,
610-
'about_message': self.about_message,
611-
'banner_message': self.banner_message,
612-
'extra_footer_scripts': self.extra_footer_scripts,
613-
'jinja2_env': jinja_env,
614-
'build_memory_limit': self.build_memory_limit,
615-
'build_memory_request': self.build_memory_request,
616-
'build_docker_host': self.build_docker_host,
617-
'base_url': self.base_url,
618-
'badge_base_url': self.badge_base_url,
619-
"static_path": os.path.join(HERE, "static"),
620-
'static_url_prefix': url_path_join(self.base_url, 'static/'),
621-
'template_variables': self.template_variables,
622-
'executor': self.executor,
623-
'auth_enabled': self.auth_enabled,
624-
'event_log': self.event_log,
625-
'normalized_origin': self.normalized_origin
626-
})
590+
self.tornado_settings.update(
591+
{
592+
"log_function": log_request,
593+
"push_secret": self.push_secret,
594+
"image_prefix": self.image_prefix,
595+
"debug": self.debug,
596+
"launcher": self.launcher,
597+
"appendix": self.appendix,
598+
"build_namespace": self.build_namespace,
599+
"build_image": self.build_image,
600+
"build_node_selector": self.build_node_selector,
601+
"build_pool": self.build_pool,
602+
"sticky_builds": self.sticky_builds,
603+
"log_tail_lines": self.log_tail_lines,
604+
"pod_quota": self.pod_quota,
605+
"per_repo_quota": self.per_repo_quota,
606+
"per_repo_quota_higher": self.per_repo_quota_higher,
607+
"repo_providers": self.repo_providers,
608+
"use_registry": self.use_registry,
609+
"registry": registry,
610+
"traitlets_config": self.config,
611+
"google_analytics_code": self.google_analytics_code,
612+
"google_analytics_domain": self.google_analytics_domain,
613+
"about_message": self.about_message,
614+
"banner_message": self.banner_message,
615+
"extra_footer_scripts": self.extra_footer_scripts,
616+
"jinja2_env": jinja_env,
617+
"build_memory_limit": self.build_memory_limit,
618+
"build_memory_request": self.build_memory_request,
619+
"build_docker_host": self.build_docker_host,
620+
"base_url": self.base_url,
621+
"badge_base_url": self.badge_base_url,
622+
"static_path": os.path.join(HERE, "static"),
623+
"static_url_prefix": url_path_join(self.base_url, "static/"),
624+
"template_variables": self.template_variables,
625+
"executor": self.executor,
626+
"auth_enabled": self.auth_enabled,
627+
"event_log": self.event_log,
628+
"normalized_origin": self.normalized_origin,
629+
}
630+
)
627631
if self.auth_enabled:
628632
self.tornado_settings['cookie_secret'] = os.urandom(32)
629633

binderhub/base.py

+4
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ async def get(self):
118118

119119
class VersionHandler(BaseHandler):
120120
"""Serve information about versions running"""
121+
122+
# demote logging of 200 responses to debug-level
123+
log_success_debug = True
124+
121125
async def get(self):
122126
self.set_header("Content-type", "application/json")
123127
self.write(json.dumps(

binderhub/health.py

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ async def wrapper(*args, **kwargs):
8282
class HealthHandler(BaseHandler):
8383
"""Serve health status"""
8484

85+
# demote logging of 200 responses to debug-level
86+
# to avoid flooding logs with health checks
87+
log_success_debug = True
88+
8589
def initialize(self, hub_url=None):
8690
self.hub_url = hub_url
8791

binderhub/log.py

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""logging utilities"""
2+
3+
# copied from jupyterhub 1.1.0
4+
# Copyright (c) Jupyter Development Team.
5+
# Distributed under the terms of the Modified BSD License.
6+
7+
import json
8+
import traceback
9+
from http.cookies import SimpleCookie
10+
from urllib.parse import urlparse
11+
from urllib.parse import urlunparse
12+
13+
from tornado.log import access_log
14+
from tornado.log import LogFormatter
15+
from tornado.web import HTTPError
16+
from tornado.web import StaticFileHandler
17+
18+
19+
# url params to be scrubbed if seen
20+
# any url param that *contains* one of these
21+
# will be scrubbed from logs
22+
SCRUB_PARAM_KEYS = ("token", "auth", "key", "code", "state")
23+
24+
25+
def _scrub_uri(uri):
26+
"""scrub auth info from uri"""
27+
parsed = urlparse(uri)
28+
if parsed.query:
29+
# check for potentially sensitive url params
30+
# use manual list + split rather than parsing
31+
# to minimally perturb original
32+
parts = parsed.query.split("&")
33+
changed = False
34+
for i, s in enumerate(parts):
35+
if "=" in s:
36+
key, value = s.split("=", 1)
37+
for substring in SCRUB_PARAM_KEYS:
38+
if substring in key:
39+
parts[i] = key + "=[secret]"
40+
changed = True
41+
if changed:
42+
parsed = parsed._replace(query="&".join(parts))
43+
return urlunparse(parsed)
44+
return uri
45+
46+
47+
def _scrub_headers(headers):
48+
"""scrub auth info from headers"""
49+
headers = dict(headers)
50+
if "Authorization" in headers:
51+
auth = headers["Authorization"]
52+
if " " in auth:
53+
auth_type = auth.split(" ", 1)[0]
54+
else:
55+
# no space, hide the whole thing in case there was a mistake
56+
auth_type = ""
57+
headers["Authorization"] = "{} [secret]".format(auth_type)
58+
if "Cookie" in headers:
59+
c = SimpleCookie(headers["Cookie"])
60+
redacted = []
61+
for name in c.keys():
62+
redacted.append("{}=[secret]".format(name))
63+
headers["Cookie"] = "; ".join(redacted)
64+
return headers
65+
66+
67+
# log_request adapted from IPython (BSD)
68+
69+
70+
def log_request(handler):
71+
"""log a bit more information about each request than tornado's default
72+
73+
- move static file get success to debug-level (reduces noise)
74+
- get proxied IP instead of proxy IP
75+
- log referer for redirect and failed requests
76+
- log user-agent for failed requests
77+
- record per-request metrics in prometheus
78+
"""
79+
status = handler.get_status()
80+
request = handler.request
81+
if status == 304 or (
82+
status < 300
83+
and (
84+
isinstance(handler, StaticFileHandler)
85+
or getattr(handler, "log_success_debug", False)
86+
)
87+
):
88+
# static-file success and 304 Found are debug-level
89+
log_method = access_log.debug
90+
elif status < 400:
91+
log_method = access_log.info
92+
elif status < 500:
93+
log_method = access_log.warning
94+
else:
95+
log_method = access_log.error
96+
97+
uri = _scrub_uri(request.uri)
98+
headers = _scrub_headers(request.headers)
99+
100+
request_time = 1000.0 * handler.request.request_time()
101+
102+
try:
103+
user = handler.current_user
104+
except (HTTPError, RuntimeError):
105+
username = ""
106+
else:
107+
if user is None:
108+
username = ""
109+
elif isinstance(user, str):
110+
username = user
111+
elif isinstance(user, dict):
112+
username = user.get("name", "unknown")
113+
else:
114+
username = "unknown"
115+
116+
ns = dict(
117+
status=status,
118+
method=request.method,
119+
ip=request.remote_ip,
120+
uri=uri,
121+
request_time=request_time,
122+
user=username,
123+
location="",
124+
)
125+
msg = "{status} {method} {uri}{location} ({user}@{ip}) {request_time:.2f}ms"
126+
if status >= 500 and status not in {502, 503}:
127+
log_method(json.dumps(headers, indent=2))
128+
elif status in {301, 302}:
129+
# log redirect targets
130+
# FIXME: _headers is private, but there doesn't appear to be a public way
131+
# to get headers from tornado
132+
location = handler._headers.get("Location")
133+
if location:
134+
ns["location"] = " -> {}".format(_scrub_uri(location))
135+
log_method(msg.format(**ns))

binderhub/metrics.py

+3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44

55
class MetricsHandler(BaseHandler):
6+
# demote logging of 200 responses to debug-level
7+
log_success_debug = True
8+
69
async def get(self):
710
self.set_header("Content-Type", CONTENT_TYPE_LATEST)
811
self.write(generate_latest(REGISTRY))

helm-chart/binderhub/templates/deployment.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ spec:
117117
name: binder
118118
livenessProbe:
119119
httpGet:
120-
path: {{ default "/" .Values.config.BinderHub.base_url }}about
120+
path: {{ default "/" .Values.config.BinderHub.base_url }}versions
121121
port: binder
122122
initialDelaySeconds: 10
123123
periodSeconds: 5

0 commit comments

Comments
 (0)