-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth_logger.py
More file actions
269 lines (223 loc) · 9.83 KB
/
Copy pathauth_logger.py
File metadata and controls
269 lines (223 loc) · 9.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# Copyright 2024-2025 Planet Labs PBC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import functools
import json
import logging
import importlib.metadata
from contextlib import suppress
from typing import Dict, Optional
from .events import AuthEvent
from planet_auth.auth_exception import AuthException, InvalidTokenException
# from planet_auth.oidc.token_validator import InvalidTokenException
# _lib_global_py_logger = logging.getLogger(__name__)
_lib_global_py_logger = logging.getLogger("planet_auth")
_lib_global_do_structured_logging = False
_lib_global_nested_logging_key = None
# Some services use the `json_logging` module which expects
# additional logging parameters to be stored under the 'props' key. This is
# the default for now so we don't break those dependant services' logging.
DEFAULT_NESTED_KEY = "props"
# class AuthLogger(logging.Logger): # TODO?: is this a good idea, or a better approach?
class AuthLogger:
"""
Class that wraps the Python logger so that all logs emitted from
this library may be logged with the same consistent JSON format.
This is being done so that dashboards may be built using structured
data over string reg-ex parsing.
"""
def __init__(self):
# self._py_logger = logging.getLogger(__name__)
# self._py_logger = logger
self._msg_prefix = "[planet-auth-python] "
self._auth_libraries = self._get_auth_libraries()
def _get_auth_libraries(self):
libs = {"planet-auth": importlib.metadata.version("planet-auth")}
for optional_lib in (
"planet-auth-config",
"planet-auth-django",
):
libs[optional_lib] = "N/A"
with suppress(importlib.metadata.PackageNotFoundError):
libs[optional_lib] = importlib.metadata.version(optional_lib)
return libs
def _get_py_logger(self):
# _py_logger is not simply a member so that users of our library can call
# setPyLoggerForAuthLogger and the library will work as expected.
# if self._py_logger:
# return self._py_logger
# pylint: disable=global-variable-not-assigned
global _lib_global_py_logger
return _lib_global_py_logger
# TODO: should log level be encapsulated by the AuthLogger class?
def log(
self,
level: int,
msg: str = "",
event: AuthEvent = None,
jwt_header_json: Dict = None,
jwt_body_json: Dict = None,
exception: Exception = None,
) -> None:
_logger = self._get_py_logger()
if not _logger:
return
if level < _logger.getEffectiveLevel():
return
if exception:
_log_msg = msg or str(exception)
if not event and isinstance(exception, AuthException):
event = exception.event()
# Note: This is a little hacky. The lib is designed to handle more than just JWTs and OAuth,
# but it is a very common use case and this makes for an ergonomic development experience,
# making it easy to have the raise pass context in the exception to a distant point
# in the code responsible for logging.
if isinstance(exception, InvalidTokenException):
if not jwt_header_json:
jwt_header_json = exception.jwt_header()
if not jwt_body_json:
jwt_body_json = exception.jwt_body()
else:
_log_msg = msg
_log_msg = self._msg_prefix + _log_msg
if not event:
event = AuthEvent.TRACE
log_json = {
# "msg": _log_msg,
"event": str(event),
"auth_libraries": self._auth_libraries,
}
# TODO: Is this actually right? The library is more general than OAuth and JWTs, but this
# is a common need when we log, so we've done this in our logger.
if jwt_header_json:
log_json["jwt_header"] = {"alg": jwt_header_json.get("alg")}
if jwt_body_json:
log_json["jwt_payload"] = {
"iss": jwt_body_json.get("iss"), # Standard claim
"cid": jwt_body_json.get("cid"), # Standard claim
# "sub": jwt_body_json.get("sub"), # Standard claim (may contain PII in some implementations)
"aud": jwt_body_json.get("aud"), # Standard claim
"scope": jwt_body_json.get("scope"), # RFC 8693, 9068 claim used for scope
"scp": jwt_body_json.get("scp"), # Okta claim used for scope
"pl_principal": jwt_body_json.get("pl_principal"), # Planet claim
"organization_id": jwt_body_json.get("organization_id"), # Planet claim
}
if exception:
log_json["error"] = str(exception)
# log_json["stack_trace"] =
if _lib_global_do_structured_logging:
final_log_msg = _log_msg
if _lib_global_nested_logging_key:
final_log_extra = {_lib_global_nested_logging_key: log_json}
else:
final_log_extra = log_json
else:
log_json["msg"] = _log_msg
final_log_msg = json.dumps(log_json)
final_log_extra = None
_logger.log(level=level, msg=final_log_msg, extra=final_log_extra)
def critical(self, **kwargs) -> None:
return self.log(level=logging.CRITICAL, **kwargs)
def error(self, **kwargs) -> None:
return self.log(level=logging.ERROR, **kwargs)
def warning(self, **kwargs) -> None:
return self.log(level=logging.WARNING, **kwargs)
def info(self, **kwargs) -> None:
return self.log(level=logging.INFO, **kwargs)
def debug(self, **kwargs) -> None:
return self.log(level=logging.DEBUG, **kwargs)
def log_exception(
self,
default_event: AuthEvent = AuthEvent.TRACE,
override_event: AuthEvent = None,
level: int = logging.WARNING,
exception_cls=Exception,
**params,
):
"""
Decorator to log exceptions.
Parameters:
default_event : Event to log when the exception does not include a more specific event.
override_event : Event to log regardless of whether or not the exception includes another event.
level : Log level
exception_cls : Exception class to catch and log.
Example:
```python
@AuthLogger.log_exception(level=logging.WARNING, event=AuthEvent.INVALID_TOKEN)
def raise_my_exception():
raise Some_Exception()
```
"""
# some_param = params.get("some_param", default_value)
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except exception_cls as e:
log_event = None
if override_event:
log_event = override_event
elif isinstance(e, AuthException):
log_event = e.event()
if not log_event:
log_event = default_event
self.log(event=log_event, level=level, exception=e)
raise e
return wrapper
return decorator
_default_auth_logger = AuthLogger()
# This is really only intended for use inside the library, not for use by library users.
def getAuthLogger():
# pylint: disable=global-variable-not-assigned
global _default_auth_logger
return _default_auth_logger
def setPyLoggerForAuthLogger(py_logger: logging.Logger):
"""
Set the python logger that should be used by the library.
Parameters:
py_logger: The python logger that the library should use.
Set this to None to completely mute logging.
"""
# pylint: disable=global-statement
global _lib_global_py_logger
_lib_global_py_logger = py_logger
def setStructuredLogging(nested_key: Optional[str] = DEFAULT_NESTED_KEY):
"""
Configure the library to emit structured log messages. When this
mode is set, logs will be emitted specifying information using the logger's
`extra` field.
Parameters:
nested_key: dict key in which to wrap the library's data logged under
the `extra` field. The default is to include all library logged
extra fields encapsulated inside a dictionary with the single key
`props`. This default was chosen to comform to the expectations of
the `json_logging` python module. For example, by default extra data
will be submitted to the logger as
`log(msg="log msg", extra={"props": {library_provided_extra_keys}})`.
Set this to `None` to forego wrapping.
"""
# pylint: disable=global-statement
global _lib_global_do_structured_logging
_lib_global_do_structured_logging = True
global _lib_global_nested_logging_key
_lib_global_nested_logging_key = nested_key
def setStringLogging():
"""
Configure the library to emit simple string log messages. When this
mode is set, logs will be emitted with all information in the log message,
ahd the logger's `extra` field will be set to None.
"""
# pylint: disable=global-statement
global _lib_global_do_structured_logging
_lib_global_do_structured_logging = False