structlog-config 0.1.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. structlog_config-0.4.0/PKG-INFO +135 -0
  2. structlog_config-0.4.0/README.md +120 -0
  3. {structlog_config-0.1.0 → structlog_config-0.4.0}/pyproject.toml +8 -3
  4. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/__init__.py +23 -10
  5. structlog_config-0.4.0/structlog_config/constants.py +16 -0
  6. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/env_config.py +1 -1
  7. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/fastapi_access_logger.py +61 -3
  8. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/formatters.py +1 -0
  9. structlog_config-0.4.0/structlog_config/levels.py +35 -0
  10. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/stdlib_logging.py +70 -39
  11. structlog_config-0.4.0/structlog_config/trace.py +62 -0
  12. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/warnings.py +6 -16
  13. structlog_config-0.1.0/.copier-answers.yml +0 -2
  14. structlog_config-0.1.0/.envrc +0 -11
  15. structlog_config-0.1.0/.github/dependabot.yml +0 -12
  16. structlog_config-0.1.0/.github/workflows/build_and_publish.yml +0 -56
  17. structlog_config-0.1.0/.github/workflows/repo-sync.yml +0 -16
  18. structlog_config-0.1.0/.gitignore +0 -129
  19. structlog_config-0.1.0/.tool-versions +0 -3
  20. structlog_config-0.1.0/.vscode/settings.json +0 -36
  21. structlog_config-0.1.0/CHANGELOG.md +0 -30
  22. structlog_config-0.1.0/Makefile +0 -11
  23. structlog_config-0.1.0/PKG-INFO +0 -62
  24. structlog_config-0.1.0/README.md +0 -49
  25. structlog_config-0.1.0/copier.yml +0 -68
  26. structlog_config-0.1.0/structlog_config/constants.py +0 -9
  27. structlog_config-0.1.0/tests/__init__.py +0 -0
  28. structlog_config-0.1.0/tests/capture_utils.py +0 -53
  29. structlog_config-0.1.0/tests/conftest.py +0 -114
  30. structlog_config-0.1.0/tests/test_environment.py +0 -118
  31. structlog_config-0.1.0/tests/test_fastapi_access_logger.py +0 -189
  32. structlog_config-0.1.0/tests/test_import.py +0 -8
  33. structlog_config-0.1.0/tests/test_logging.py +0 -167
  34. structlog_config-0.1.0/tests/utils.py +0 -35
  35. structlog_config-0.1.0/uv.lock +0 -1680
  36. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/environments.py +0 -0
  37. {structlog_config-0.1.0 → structlog_config-0.4.0}/structlog_config/packages.py +0 -0
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.3
2
+ Name: structlog-config
3
+ Version: 0.4.0
4
+ Summary: A comprehensive structlog configuration with sensible defaults for development and production environments, featuring context management, exception formatting, and path prettification.
5
+ Keywords: logging,structlog,json-logging,structured-logging
6
+ Author: Michael Bianco
7
+ Author-email: Michael Bianco <mike@mikebian.co>
8
+ Requires-Dist: orjson>=3.10.15
9
+ Requires-Dist: python-decouple-typed>=3.11.0
10
+ Requires-Dist: python-ipware>=3.0.0
11
+ Requires-Dist: structlog>=25.2.0
12
+ Requires-Python: >=3.10
13
+ Project-URL: Repository, https://github.com/iloveitaly/structlog-config
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Opinionated Defaults for Structlog
17
+
18
+ Logging is really important. Getting logging to work well in python feels like black magic: there's a ton of configuration
19
+ across structlog, warnings, std loggers, fastapi + celery context, JSON logging in production, etc that requires lots of
20
+ fiddling and testing to get working. I finally got this working for me in my [project template](https://github.com/iloveitaly/python-starter-template) and extracted this out into a nice package.
21
+
22
+ Here are the main goals:
23
+
24
+ * High performance JSON logging in production
25
+ * All loggers, even plugin or system loggers, should route through the same formatter
26
+ * Structured logging everywhere
27
+ * Ability to easily set thread-local log context
28
+ * Nice log formatters for stack traces, ORM ([ActiveModel/SQLModel](https://github.com/iloveitaly/activemodel)), etc
29
+ * Ability to log level and output (i.e. file path) *by logger* for easy development debugging
30
+ * If you are using fastapi, structured logging for access logs
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ from structlog_config import configure_logger
36
+
37
+ log = configure_logger()
38
+
39
+ log.info("the log", key="value")
40
+ ```
41
+
42
+ ## TRACE Logging Level
43
+
44
+ This package adds support for a custom `TRACE` logging level (level 5) that's even more verbose than `DEBUG`. This is useful for extremely detailed debugging scenarios.
45
+
46
+ The `TRACE` level is automatically set up when you call `configure_logger()`. You can use it like any other logging level:
47
+
48
+ ```python
49
+ import logging
50
+ from structlog_config import configure_logger
51
+
52
+ log = configure_logger()
53
+
54
+ # Using structlog
55
+ log.info("This is info")
56
+ log.debug("This is debug")
57
+ log.trace("This is trace") # Most verbose
58
+
59
+ # Using stdlib logging
60
+ logging.trace("Module-level trace message")
61
+ logger = logging.getLogger(__name__)
62
+ logger.trace("Instance trace message")
63
+ ```
64
+
65
+ Set the log level to TRACE using the environment variable:
66
+
67
+ ```bash
68
+ LOG_LEVEL=TRACE
69
+ ```
70
+
71
+ ## Stdlib Log Management
72
+
73
+ By default, all stdlib loggers are:
74
+
75
+ 1. Given the same global logging level, with some default adjustments for noisy loggers (looking at you, `httpx`)
76
+ 2. Use a structlog formatter (you get structured logging, context, etc in any stdlib logger calls)
77
+ 3. The root processor is overwritten so any child loggers created after initialization will use the same formatter
78
+
79
+ You can customize loggers by name (i.e. the name used in `logging.getLogger(__name__)`) using ENV variables.
80
+
81
+ For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16):
82
+
83
+ * `LOG_LEVEL_OPENAI=DEBUG`
84
+ * `LOG_PATH_OPENAI=tmp/openai.log`
85
+ * `LOG_LEVEL_HTTPX=DEBUG`
86
+ * `LOG_PATH_HTTPX=tmp/openai.log`
87
+
88
+ ## FastAPI Access Logger
89
+
90
+ Structured, simple access log with request timing to replace the default fastapi access log. Why?
91
+
92
+ 1. It's less verbose
93
+ 2. Uses structured logging params instead of string interpolation
94
+ 3. debug level logs any static assets
95
+
96
+ Here's how to use it:
97
+
98
+ 1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
99
+ 2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
100
+
101
+ ## iPython
102
+
103
+ Often it's helpful to update logging level within an iPython session. You can do this and make sure all loggers pick up on it.
104
+
105
+ ```
106
+ %env LOG_LEVEL=DEBUG
107
+ from structlog_config import configure_logger
108
+ configure_logger()
109
+ ```
110
+
111
+ ## Related Projects
112
+
113
+ * https://github.com/underyx/structlog-pretty
114
+ * https://pypi.org/project/httpx-structlog/
115
+
116
+ ## References
117
+
118
+ General logging:
119
+
120
+ - https://github.com/replicate/cog/blob/2e57549e18e044982bd100e286a1929f50880383/python/cog/logging.py#L20
121
+ - https://github.com/apache/airflow/blob/4280b83977cd5a53c2b24143f3c9a6a63e298acc/task_sdk/src/airflow/sdk/log.py#L187
122
+ - https://github.com/kiwicom/structlog-sentry
123
+ - https://github.com/jeremyh/datacube-explorer/blob/b289b0cde0973a38a9d50233fe0fff00e8eb2c8e/cubedash/logs.py#L40C21-L40C42
124
+ - https://stackoverflow.com/questions/76256249/logging-in-the-open-ai-python-library/78214464#78214464
125
+ - https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16
126
+ - https://www.python-httpx.org/logging/
127
+
128
+ FastAPI access logger:
129
+
130
+ - https://github.com/iloveitaly/fastapi-logger/blob/main/fastapi_structlog/middleware/access_log.py#L70
131
+ - https://github.com/fastapiutils/fastapi-utils/blob/master/fastapi_utils/timing.py
132
+ - https://pypi.org/project/fastapi-structlog/
133
+ - https://pypi.org/project/asgi-correlation-id/
134
+ - https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
135
+ - https://github.com/sharu1204/fastapi-structlog/blob/master/app/main.py
@@ -0,0 +1,120 @@
1
+ # Opinionated Defaults for Structlog
2
+
3
+ Logging is really important. Getting logging to work well in python feels like black magic: there's a ton of configuration
4
+ across structlog, warnings, std loggers, fastapi + celery context, JSON logging in production, etc that requires lots of
5
+ fiddling and testing to get working. I finally got this working for me in my [project template](https://github.com/iloveitaly/python-starter-template) and extracted this out into a nice package.
6
+
7
+ Here are the main goals:
8
+
9
+ * High performance JSON logging in production
10
+ * All loggers, even plugin or system loggers, should route through the same formatter
11
+ * Structured logging everywhere
12
+ * Ability to easily set thread-local log context
13
+ * Nice log formatters for stack traces, ORM ([ActiveModel/SQLModel](https://github.com/iloveitaly/activemodel)), etc
14
+ * Ability to log level and output (i.e. file path) *by logger* for easy development debugging
15
+ * If you are using fastapi, structured logging for access logs
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from structlog_config import configure_logger
21
+
22
+ log = configure_logger()
23
+
24
+ log.info("the log", key="value")
25
+ ```
26
+
27
+ ## TRACE Logging Level
28
+
29
+ This package adds support for a custom `TRACE` logging level (level 5) that's even more verbose than `DEBUG`. This is useful for extremely detailed debugging scenarios.
30
+
31
+ The `TRACE` level is automatically set up when you call `configure_logger()`. You can use it like any other logging level:
32
+
33
+ ```python
34
+ import logging
35
+ from structlog_config import configure_logger
36
+
37
+ log = configure_logger()
38
+
39
+ # Using structlog
40
+ log.info("This is info")
41
+ log.debug("This is debug")
42
+ log.trace("This is trace") # Most verbose
43
+
44
+ # Using stdlib logging
45
+ logging.trace("Module-level trace message")
46
+ logger = logging.getLogger(__name__)
47
+ logger.trace("Instance trace message")
48
+ ```
49
+
50
+ Set the log level to TRACE using the environment variable:
51
+
52
+ ```bash
53
+ LOG_LEVEL=TRACE
54
+ ```
55
+
56
+ ## Stdlib Log Management
57
+
58
+ By default, all stdlib loggers are:
59
+
60
+ 1. Given the same global logging level, with some default adjustments for noisy loggers (looking at you, `httpx`)
61
+ 2. Use a structlog formatter (you get structured logging, context, etc in any stdlib logger calls)
62
+ 3. The root processor is overwritten so any child loggers created after initialization will use the same formatter
63
+
64
+ You can customize loggers by name (i.e. the name used in `logging.getLogger(__name__)`) using ENV variables.
65
+
66
+ For example, if you wanted to [mimic `OPENAI_LOG` functionality](https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16):
67
+
68
+ * `LOG_LEVEL_OPENAI=DEBUG`
69
+ * `LOG_PATH_OPENAI=tmp/openai.log`
70
+ * `LOG_LEVEL_HTTPX=DEBUG`
71
+ * `LOG_PATH_HTTPX=tmp/openai.log`
72
+
73
+ ## FastAPI Access Logger
74
+
75
+ Structured, simple access log with request timing to replace the default fastapi access log. Why?
76
+
77
+ 1. It's less verbose
78
+ 2. Uses structured logging params instead of string interpolation
79
+ 3. debug level logs any static assets
80
+
81
+ Here's how to use it:
82
+
83
+ 1. [Disable fastapi's default logging.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/main.py#L55-L56)
84
+ 2. [Add the middleware to your FastAPI app.](https://github.com/iloveitaly/python-starter-template/blob/f54cb47d8d104987f2e4a668f9045a62e0d6818a/app/routes/middleware/__init__.py#L63-L65)
85
+
86
+ ## iPython
87
+
88
+ Often it's helpful to update logging level within an iPython session. You can do this and make sure all loggers pick up on it.
89
+
90
+ ```
91
+ %env LOG_LEVEL=DEBUG
92
+ from structlog_config import configure_logger
93
+ configure_logger()
94
+ ```
95
+
96
+ ## Related Projects
97
+
98
+ * https://github.com/underyx/structlog-pretty
99
+ * https://pypi.org/project/httpx-structlog/
100
+
101
+ ## References
102
+
103
+ General logging:
104
+
105
+ - https://github.com/replicate/cog/blob/2e57549e18e044982bd100e286a1929f50880383/python/cog/logging.py#L20
106
+ - https://github.com/apache/airflow/blob/4280b83977cd5a53c2b24143f3c9a6a63e298acc/task_sdk/src/airflow/sdk/log.py#L187
107
+ - https://github.com/kiwicom/structlog-sentry
108
+ - https://github.com/jeremyh/datacube-explorer/blob/b289b0cde0973a38a9d50233fe0fff00e8eb2c8e/cubedash/logs.py#L40C21-L40C42
109
+ - https://stackoverflow.com/questions/76256249/logging-in-the-open-ai-python-library/78214464#78214464
110
+ - https://github.com/openai/openai-python/blob/de7c0e2d9375d042a42e3db6c17e5af9a5701a99/src/openai/_utils/_logs.py#L16
111
+ - https://www.python-httpx.org/logging/
112
+
113
+ FastAPI access logger:
114
+
115
+ - https://github.com/iloveitaly/fastapi-logger/blob/main/fastapi_structlog/middleware/access_log.py#L70
116
+ - https://github.com/fastapiutils/fastapi-utils/blob/master/fastapi_utils/timing.py
117
+ - https://pypi.org/project/fastapi-structlog/
118
+ - https://pypi.org/project/asgi-correlation-id/
119
+ - https://gist.github.com/nymous/f138c7f06062b7c43c060bf03759c29e
120
+ - https://github.com/sharu1204/fastapi-structlog/blob/master/app/main.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "structlog-config"
3
- version = "0.1.0"
3
+ version = "0.4.0"
4
4
  description = "A comprehensive structlog configuration with sensible defaults for development and production environments, featuring context management, exception formatting, and path prettification."
5
5
  keywords = ["logging", "structlog", "json-logging", "structured-logging"]
6
6
  readme = "README.md"
@@ -8,14 +8,19 @@ requires-python = ">=3.10"
8
8
  dependencies = [
9
9
  "orjson>=3.10.15",
10
10
  "python-decouple-typed>=3.11.0",
11
+ "python-ipware>=3.0.0",
11
12
  "structlog>=25.2.0",
12
13
  ]
13
14
  authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
14
15
  urls = { "Repository" = "https://github.com/iloveitaly/structlog-config" }
15
16
 
16
17
  [build-system]
17
- requires = ["hatchling"]
18
- build-backend = "hatchling.build"
18
+ requires = ["uv_build>=0.8.11,<0.9.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.uv.build-backend]
22
+ # avoids the src/ directory structure
23
+ module-root = ""
19
24
 
20
25
  [dependency-groups]
21
26
  debugging-extras = [
@@ -1,9 +1,10 @@
1
- import logging
2
- from typing import Protocol
1
+ from contextlib import _GeneratorContextManager
2
+ from typing import Generator, Protocol
3
3
 
4
4
  import orjson
5
5
  import structlog
6
6
  import structlog.dev
7
+ from decouple import config
7
8
  from structlog.processors import ExceptionRenderer
8
9
  from structlog.tracebacks import ExceptionDictTransformer
9
10
  from structlog.typing import FilteringBoundLogger
@@ -16,17 +17,19 @@ from structlog_config.formatters import (
16
17
  simplify_activemodel_objects,
17
18
  )
18
19
 
19
- from . import packages
20
- from .constants import NO_COLOR, PYTHON_LOG_PATH
20
+ from . import (
21
+ packages,
22
+ trace, # noqa: F401
23
+ )
24
+ from .constants import NO_COLOR, package_logger
21
25
  from .environments import is_production, is_pytest, is_staging
26
+ from .levels import get_environment_log_level_as_string
22
27
  from .stdlib_logging import (
23
- get_environment_log_level_as_string,
24
28
  redirect_stdlib_loggers,
25
29
  )
30
+ from .trace import setup_trace
26
31
  from .warnings import redirect_showwarnings
27
32
 
28
- package_logger = logging.getLogger(__name__)
29
-
30
33
 
31
34
  def log_processors_for_mode(json_logger: bool) -> list[structlog.types.Processor]:
32
35
  if json_logger:
@@ -99,11 +102,19 @@ def _logger_factory(json_logger: bool):
99
102
  In production, optimized for speed (https://www.structlog.org/en/stable/performance.html)
100
103
  """
101
104
 
105
+ # avoid a constant for this ENV so we can mutate within tests
106
+ python_log_path = config("PYTHON_LOG_PATH", default=None)
107
+
102
108
  if json_logger:
109
+ # TODO I guess we could support this, but the assumption is stdout is going to be used in prod environments
110
+ if python_log_path:
111
+ package_logger.warning(
112
+ "PYTHON_LOG_PATH is not supported with a JSON logger, forcing stdout"
113
+ )
103
114
  return structlog.BytesLoggerFactory()
104
115
 
105
- if PYTHON_LOG_PATH:
106
- python_log = open(PYTHON_LOG_PATH, "a", encoding="utf-8")
116
+ if python_log_path:
117
+ python_log = open(python_log_path, "a", encoding="utf-8")
107
118
  return structlog.PrintLoggerFactory(file=python_log)
108
119
 
109
120
  # Default case
@@ -118,7 +129,7 @@ class LoggerWithContext(FilteringBoundLogger, Protocol):
118
129
  want to replicate.
119
130
  """
120
131
 
121
- def context(self, *args, **kwargs) -> None:
132
+ def context(self, *args, **kwargs) -> _GeneratorContextManager[None, None, None]:
122
133
  "context manager to temporarily set and clear logging context"
123
134
  ...
124
135
 
@@ -158,6 +169,8 @@ def configure_logger(
158
169
  json_logger: Optional flag to use JSON logging. If None, defaults to
159
170
  production or staging environment sourced from PYTHON_ENV.
160
171
  """
172
+ setup_trace()
173
+
161
174
  # Reset structlog configuration to make sure we're starting fresh
162
175
  # This is important for tests where configure_logger might be called multiple times
163
176
  structlog.reset_defaults()
@@ -0,0 +1,16 @@
1
+ import logging
2
+ import os
3
+
4
+ from decouple import config
5
+
6
+ PYTHONASYNCIODEBUG = config("PYTHONASYNCIODEBUG", default=False, cast=bool)
7
+ "this is a builtin env var, we check for it to ensure we don't silence this log level"
8
+
9
+ NO_COLOR = "NO_COLOR" in os.environ
10
+ "support NO_COLOR standard https://no-color.org"
11
+
12
+ package_logger = logging.getLogger(__name__)
13
+ "strange name to not be confused with all of the log-related names floating around"
14
+
15
+ TRACE_LOG_LEVEL = 5
16
+ "Custom log level for trace logging, lower than DEBUG"
@@ -10,7 +10,7 @@ LOG_LEVEL_PATTERN = re.compile(r"^LOG_LEVEL_(.+)$")
10
10
  LOG_PATH_PATTERN = re.compile(r"^LOG_PATH_(.+)$")
11
11
 
12
12
 
13
- def get_custom_logger_configs() -> dict[str, dict[str, str]]:
13
+ def get_custom_logger_config() -> dict[str, dict[str, str]]:
14
14
  """
15
15
  Parse environment variables to extract custom logger configurations.
16
16
 
@@ -3,13 +3,16 @@ from urllib.parse import quote
3
3
 
4
4
  import structlog
5
5
  from fastapi import FastAPI
6
+ from python_ipware import IpWare
6
7
  from starlette.middleware.base import RequestResponseEndpoint
7
8
  from starlette.requests import Request
8
9
  from starlette.responses import Response
9
10
  from starlette.routing import Match, Mount
10
11
  from starlette.types import Scope
12
+ from starlette.websockets import WebSocket
11
13
 
12
14
  log = structlog.get_logger("access_log")
15
+ ipw = IpWare()
13
16
 
14
17
 
15
18
  def get_route_name(app: FastAPI, scope: Scope, prefix: str = "") -> str:
@@ -63,6 +66,42 @@ def get_client_addr(scope: Scope) -> str:
63
66
  return f"{ip}:{port}"
64
67
 
65
68
 
69
+ def client_ip_from_request(request: Request | WebSocket) -> str | None:
70
+ """
71
+ Get the client IP address from the request.
72
+
73
+ Headers are not case-sensitive.
74
+
75
+ Uses ipware library to properly extract client IP from various proxy headers.
76
+ Fallback to direct client connection if no proxy headers found.
77
+ """
78
+ headers = request.headers
79
+
80
+ # TODO this seems really inefficient, we should just rewrite the ipware into this repo :/
81
+ # Convert Starlette headers to format expected by ipware (HTTP_ prefixed)
82
+ # ipware expects headers in WSGI/Django-style meta format where HTTP headers
83
+ # are prefixed with "HTTP_" and dashes become underscores.
84
+ # See: https://github.com/un33k/python-ipware/blob/main/python_ipware/python_ipware.py#L33-L40
85
+ meta_dict = {}
86
+ for name, value in headers.items():
87
+ # Convert header name to HTTP_ prefixed format
88
+ meta_key = f"HTTP_{name.upper().replace('-', '_')}"
89
+ meta_dict[meta_key] = value
90
+
91
+ # Use ipware to extract IP from headers
92
+ ip, trusted_route = ipw.get_client_ip(meta=meta_dict)
93
+ if ip:
94
+ log.debug(
95
+ "extracted client IP from headers", ip=ip, trusted_route=trusted_route
96
+ )
97
+ return str(ip)
98
+
99
+ # Fallback to direct client connection
100
+ host = request.client.host if request.client else None
101
+
102
+ return host
103
+
104
+
66
105
  # TODO we should look at the static asset logic and pull the prefix path from tha
67
106
  def is_static_assets_request(scope: Scope) -> bool:
68
107
  """Check if the request is for static assets. Pretty naive check.
@@ -73,13 +112,32 @@ def is_static_assets_request(scope: Scope) -> bool:
73
112
  Returns:
74
113
  bool: True if the request is for static assets, False otherwise.
75
114
  """
76
- return scope["path"].endswith(".css") or scope["path"].endswith(".js")
115
+ return (
116
+ scope["path"].endswith(".css")
117
+ or scope["path"].endswith(".js")
118
+ # .map files are attempted when devtools are enabled
119
+ or scope["path"].endswith(".js.map")
120
+ or scope["path"].endswith(".ico")
121
+ or scope["path"].endswith(".png")
122
+ or scope["path"].endswith(".jpg")
123
+ or scope["path"].endswith(".jpeg")
124
+ or scope["path"].endswith(".gif")
125
+ )
77
126
 
78
127
 
79
128
  def add_middleware(
80
129
  app: FastAPI,
81
130
  ) -> None:
82
- """Use this method to add this middleware to your fastapi application."""
131
+ """
132
+ Add better access logging to fastapi:
133
+
134
+ >>> from structlog_config import fastapi_access_logger
135
+ >>> fastapi_access_logger.add_middleware(app)
136
+
137
+ You'll also want to disable the default uvicorn logs:
138
+
139
+ >>> uvicorn.run(..., log_config=None, access_log=False)
140
+ """
83
141
 
84
142
  @app.middleware("http")
85
143
  async def access_log_middleware(
@@ -108,7 +166,7 @@ def add_middleware(
108
166
  method=scope["method"],
109
167
  path=scope["path"],
110
168
  query=scope["query_string"].decode(),
111
- client_ip=get_client_addr(scope),
169
+ client_ip=client_ip_from_request(request),
112
170
  route=route_name,
113
171
  )
114
172
 
@@ -77,6 +77,7 @@ def pretty_traceback_exception_formatter(sio: TextIO, exc_info: ExcInfo) -> None
77
77
  from pretty_traceback.formatting import exc_to_traceback_str
78
78
 
79
79
  _, exc_value, traceback = exc_info
80
+ # TODO support local_stack_only env var support
80
81
  formatted_exception = exc_to_traceback_str(exc_value, traceback, color=not NO_COLOR)
81
82
  sio.write("\n" + formatted_exception)
82
83
 
@@ -0,0 +1,35 @@
1
+ import logging
2
+
3
+ from decouple import config
4
+
5
+
6
+ def get_environment_log_level_as_string() -> str:
7
+ level = config("LOG_LEVEL", default="INFO", cast=str).upper()
8
+
9
+ if not level.strip():
10
+ level = "INFO"
11
+
12
+ return level
13
+
14
+
15
+ def compare_log_levels(left: str, right: str) -> int:
16
+ """
17
+ Compare log levels using logging.getLevelNamesMapping for accurate int values.
18
+
19
+ Example:
20
+ >>> compare_log_levels("DEBUG", "INFO")
21
+ -1 # DEBUG is less than INFO
22
+
23
+ Asks the question "Is INFO higher than DEBUG?"
24
+ """
25
+ level_map = logging.getLevelNamesMapping()
26
+ left_level = level_map.get(left, left)
27
+ right_level = level_map.get(right, right)
28
+
29
+ # TODO should more gracefully fail here, but let's see what happens
30
+ if not isinstance(left_level, int) or not isinstance(right_level, int):
31
+ raise ValueError(
32
+ f"Invalid log level comparison: {left} ({type(left_level)}) vs {right} ({type(right_level)})"
33
+ )
34
+
35
+ return left_level - right_level