DefEssential 0.1.6__tar.gz → 0.1.7__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 (183) hide show
  1. {defessential-0.1.6 → defessential-0.1.7}/DefEssential.egg-info/PKG-INFO +1 -1
  2. {defessential-0.1.6 → defessential-0.1.7}/DefEssential.egg-info/SOURCES.txt +5 -0
  3. {defessential-0.1.6 → defessential-0.1.7}/PKG-INFO +1 -1
  4. defessential-0.1.7/issues/feature/3/IMPLEMENTS.md +118 -0
  5. defessential-0.1.7/issues/feature/3/ISSUE.md +9 -0
  6. defessential-0.1.7/issues/feature/3/README.md +48 -0
  7. defessential-0.1.7/issues/feature/3/REQUIREMENTS.md +326 -0
  8. {defessential-0.1.6 → defessential-0.1.7}/samples/Sample.py +70 -22
  9. {defessential-0.1.6 → defessential-0.1.7}/samples/runners/Requests.py +8 -19
  10. {defessential-0.1.6 → defessential-0.1.7}/samples/runners/Streaming.py +8 -19
  11. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Gunicorn/Server.py +19 -1
  12. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Python/Server.py +44 -16
  13. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Uvicorn/Server.py +38 -3
  14. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Uvicorn/WebSocket.py +3 -1
  15. defessential-0.1.7/src/Adapter/RequestHandler.py +109 -0
  16. {defessential-0.1.6 → defessential-0.1.7}/src/Application.py +86 -8
  17. {defessential-0.1.6 → defessential-0.1.7}/src/Extension.py +9 -2
  18. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/File.py +7 -1
  19. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/FileSystem.py +13 -2
  20. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Metrics.py +18 -8
  21. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Ping.py +4 -0
  22. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Specifications.py +15 -1
  23. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Status.py +7 -1
  24. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Swagger.py +7 -1
  25. {defessential-0.1.6 → defessential-0.1.7}/src/IO/WebSocket/WebSocket.py +6 -2
  26. {defessential-0.1.6 → defessential-0.1.7}/src/Response.py +3 -1
  27. {defessential-0.1.6 → defessential-0.1.7}/src/Runners/RunExtension.py +18 -3
  28. defessential-0.1.7/src/Runners/RunRequest.py +198 -0
  29. {defessential-0.1.6 → defessential-0.1.7}/tests/test_adapters.py +39 -6
  30. {defessential-0.1.6 → defessential-0.1.7}/tests/test_builtin_factories.py +14 -0
  31. defessential-0.1.7/tests/test_logging.py +835 -0
  32. {defessential-0.1.6 → defessential-0.1.7}/tests/test_request_runner_hooks.py +30 -1
  33. {defessential-0.1.6 → defessential-0.1.7}/tests/test_samples.py +34 -17
  34. {defessential-0.1.6 → defessential-0.1.7}/tests/test_server.py +1 -1
  35. {defessential-0.1.6 → defessential-0.1.7}/tests/test_writers.py +6 -2
  36. defessential-0.1.6/src/Adapter/RequestHandler.py +0 -70
  37. defessential-0.1.6/src/Runners/RunRequest.py +0 -121
  38. {defessential-0.1.6 → defessential-0.1.7}/.github/ISSUE_TEMPLATE/01.feature.yml +0 -0
  39. {defessential-0.1.6 → defessential-0.1.7}/.github/ISSUE_TEMPLATE/02.bug.yml +0 -0
  40. {defessential-0.1.6 → defessential-0.1.7}/.github/ISSUE_TEMPLATE/03.fix.yml +0 -0
  41. {defessential-0.1.6 → defessential-0.1.7}/.github/ISSUE_TEMPLATE/04.task.yml +0 -0
  42. {defessential-0.1.6 → defessential-0.1.7}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  43. {defessential-0.1.6 → defessential-0.1.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  44. {defessential-0.1.6 → defessential-0.1.7}/.github/workflows/ci.yml +0 -0
  45. {defessential-0.1.6 → defessential-0.1.7}/.gitignore +0 -0
  46. {defessential-0.1.6 → defessential-0.1.7}/DefEssential.egg-info/dependency_links.txt +0 -0
  47. {defessential-0.1.6 → defessential-0.1.7}/DefEssential.egg-info/requires.txt +0 -0
  48. {defessential-0.1.6 → defessential-0.1.7}/DefEssential.egg-info/top_level.txt +0 -0
  49. {defessential-0.1.6 → defessential-0.1.7}/LICENSE +0 -0
  50. {defessential-0.1.6 → defessential-0.1.7}/README.md +0 -0
  51. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/2/ISSUE.md +0 -0
  52. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/2/README.md +0 -0
  53. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/2/REQUIREMENTS.md +0 -0
  54. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/4/IMPLEMENTS.md +0 -0
  55. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/4/ISSUE.md +0 -0
  56. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/4/PLAN.md +0 -0
  57. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/4/README.md +0 -0
  58. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/4/REQUIREMENTS.md +0 -0
  59. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/5/IMPLEMENTS.md +0 -0
  60. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/5/ISSUE.md +0 -0
  61. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/5/PLAN.md +0 -0
  62. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/5/README.md +0 -0
  63. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/5/REQUIREMENTS.md +0 -0
  64. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/6/IMPLEMENTS.md +0 -0
  65. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/6/ISSUE.md +0 -0
  66. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/6/PLAN.md +0 -0
  67. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/6/README.md +0 -0
  68. {defessential-0.1.6 → defessential-0.1.7}/issues/feature/6/REQUIREMENTS.md +0 -0
  69. {defessential-0.1.6 → defessential-0.1.7}/issues/task/8/IMPLEMENTS.md +0 -0
  70. {defessential-0.1.6 → defessential-0.1.7}/issues/task/8/ISSUE.md +0 -0
  71. {defessential-0.1.6 → defessential-0.1.7}/issues/task/8/PLAN.md +0 -0
  72. {defessential-0.1.6 → defessential-0.1.7}/issues/task/8/README.md +0 -0
  73. {defessential-0.1.6 → defessential-0.1.7}/issues/task/8/REQUIREMENTS.md +0 -0
  74. {defessential-0.1.6 → defessential-0.1.7}/samples/README.md +0 -0
  75. {defessential-0.1.6 → defessential-0.1.7}/samples/__init__.py +0 -0
  76. {defessential-0.1.6 → defessential-0.1.7}/samples/res/css/sample.css +0 -0
  77. {defessential-0.1.6 → defessential-0.1.7}/samples/res/favicon.ico +0 -0
  78. {defessential-0.1.6 → defessential-0.1.7}/samples/res/html/welcome.html +0 -0
  79. {defessential-0.1.6 → defessential-0.1.7}/samples/res/images/sample.jpg +0 -0
  80. {defessential-0.1.6 → defessential-0.1.7}/samples/res/images/sample.png +0 -0
  81. {defessential-0.1.6 → defessential-0.1.7}/samples/runners/Authentication.py +0 -0
  82. {defessential-0.1.6 → defessential-0.1.7}/samples/runners/ContentValidation.py +0 -0
  83. {defessential-0.1.6 → defessential-0.1.7}/samples/runners/__init__.py +0 -0
  84. {defessential-0.1.6 → defessential-0.1.7}/samples/scripts/DescriptionToFile.py +0 -0
  85. {defessential-0.1.6 → defessential-0.1.7}/samples/scripts/__init__.py +0 -0
  86. {defessential-0.1.6 → defessential-0.1.7}/setup.cfg +0 -0
  87. {defessential-0.1.6 → defessential-0.1.7}/setup.py +0 -0
  88. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Gunicorn/Transport.py +0 -0
  89. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Gunicorn/__init__.py +0 -0
  90. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Python/__init__.py +0 -0
  91. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Uvicorn/HTTP.py +0 -0
  92. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Uvicorn/Portal.py +0 -0
  93. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/Uvicorn/__init__.py +0 -0
  94. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Implements/__init__.py +0 -0
  95. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/Server.py +0 -0
  96. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/WSGI.py +0 -0
  97. {defessential-0.1.6 → defessential-0.1.7}/src/Adapter/__init__.py +0 -0
  98. {defessential-0.1.6 → defessential-0.1.7}/src/Body.py +0 -0
  99. {defessential-0.1.6 → defessential-0.1.7}/src/Content.py +0 -0
  100. {defessential-0.1.6 → defessential-0.1.7}/src/Credentials.py +0 -0
  101. {defessential-0.1.6 → defessential-0.1.7}/src/CrossOriginResourceSharing.py +0 -0
  102. {defessential-0.1.6 → defessential-0.1.7}/src/Descriptor/DataObject.py +0 -0
  103. {defessential-0.1.6 → defessential-0.1.7}/src/Descriptor/Types.py +0 -0
  104. {defessential-0.1.6 → defessential-0.1.7}/src/Descriptor/Value.py +0 -0
  105. {defessential-0.1.6 → defessential-0.1.7}/src/Descriptor/__init__.py +0 -0
  106. {defessential-0.1.6 → defessential-0.1.7}/src/Error.py +0 -0
  107. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Status.html +0 -0
  108. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Swagger.html +0 -0
  109. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Utils/Routes.py +0 -0
  110. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/Utils/__init__.py +0 -0
  111. {defessential-0.1.6 → defessential-0.1.7}/src/Extensions/__init__.py +0 -0
  112. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Readers/ChunkedStream.py +0 -0
  113. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Readers/ContentReader.py +0 -0
  114. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Readers/ContentReaders.py +0 -0
  115. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Readers/__init__.py +0 -0
  116. {defessential-0.1.6 → defessential-0.1.7}/src/IO/RequestReader.py +0 -0
  117. {defessential-0.1.6 → defessential-0.1.7}/src/IO/ResponseWriter.py +0 -0
  118. {defessential-0.1.6 → defessential-0.1.7}/src/IO/WebSocket/WebSocketError.py +0 -0
  119. {defessential-0.1.6 → defessential-0.1.7}/src/IO/WebSocket/WebSocketFrame.py +0 -0
  120. {defessential-0.1.6 → defessential-0.1.7}/src/IO/WebSocket/WebSocketFrameReader.py +0 -0
  121. {defessential-0.1.6 → defessential-0.1.7}/src/IO/WebSocket/WebSocketHandshake.py +0 -0
  122. {defessential-0.1.6 → defessential-0.1.7}/src/IO/WebSocket/__init__.py +0 -0
  123. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Writers/ChunkedStreamWriter.py +0 -0
  124. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Writers/ServerSentEvents.py +0 -0
  125. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Writers/ServerSentEventsWriter.py +0 -0
  126. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Writers/StreamWriter.py +0 -0
  127. {defessential-0.1.6 → defessential-0.1.7}/src/IO/Writers/__init__.py +0 -0
  128. {defessential-0.1.6 → defessential-0.1.7}/src/IO/__init__.py +0 -0
  129. {defessential-0.1.6 → defessential-0.1.7}/src/Request.py +0 -0
  130. {defessential-0.1.6 → defessential-0.1.7}/src/RequestRunner.py +0 -0
  131. {defessential-0.1.6 → defessential-0.1.7}/src/ResponseSpecification.py +0 -0
  132. {defessential-0.1.6 → defessential-0.1.7}/src/Responses/ResponseFile.py +0 -0
  133. {defessential-0.1.6 → defessential-0.1.7}/src/Responses/__init__.py +0 -0
  134. {defessential-0.1.6 → defessential-0.1.7}/src/Route.py +0 -0
  135. {defessential-0.1.6 → defessential-0.1.7}/src/Router.py +0 -0
  136. {defessential-0.1.6 → defessential-0.1.7}/src/Runners/RunFactory.py +0 -0
  137. {defessential-0.1.6 → defessential-0.1.7}/src/Runners/RunOptions.py +0 -0
  138. {defessential-0.1.6 → defessential-0.1.7}/src/Runners/__init__.py +0 -0
  139. {defessential-0.1.6 → defessential-0.1.7}/src/Startup.py +0 -0
  140. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Configuration.py +0 -0
  141. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Connection.py +0 -0
  142. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/File.py +0 -0
  143. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Implements/FileSystem/Configuration.py +0 -0
  144. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Implements/FileSystem/Connection.py +0 -0
  145. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Implements/FileSystem/File.py +0 -0
  146. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Implements/FileSystem/__init__.py +0 -0
  147. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/Implements/__init__.py +0 -0
  148. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/FileSystemObject/__init__.py +0 -0
  149. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/Loader.py +0 -0
  150. {defessential-0.1.6 → defessential-0.1.7}/src/Utils/__init__.py +0 -0
  151. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Pattern.py +0 -0
  152. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Array.py +0 -0
  153. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Common.py +0 -0
  154. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Compare.py +0 -0
  155. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Condition.py +0 -0
  156. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/DataObject.py +0 -0
  157. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/DateTime.py +0 -0
  158. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Number.py +0 -0
  159. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Object.py +0 -0
  160. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Size.py +0 -0
  161. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/String.py +0 -0
  162. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/Type.py +0 -0
  163. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Patterns/__init__.py +0 -0
  164. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Validate.py +0 -0
  165. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/Validator.py +0 -0
  166. {defessential-0.1.6 → defessential-0.1.7}/src/Validator/__init__.py +0 -0
  167. {defessential-0.1.6 → defessential-0.1.7}/src/Validators.py +0 -0
  168. {defessential-0.1.6 → defessential-0.1.7}/src/__init__.py +0 -0
  169. {defessential-0.1.6 → defessential-0.1.7}/tests/test_application_hooks.py +0 -0
  170. {defessential-0.1.6 → defessential-0.1.7}/tests/test_content.py +0 -0
  171. {defessential-0.1.6 → defessential-0.1.7}/tests/test_core.py +0 -0
  172. {defessential-0.1.6 → defessential-0.1.7}/tests/test_exports.py +0 -0
  173. {defessential-0.1.6 → defessential-0.1.7}/tests/test_extension_hooks.py +0 -0
  174. {defessential-0.1.6 → defessential-0.1.7}/tests/test_extensions.py +0 -0
  175. {defessential-0.1.6 → defessential-0.1.7}/tests/test_features.py +0 -0
  176. {defessential-0.1.6 → defessential-0.1.7}/tests/test_files.py +0 -0
  177. {defessential-0.1.6 → defessential-0.1.7}/tests/test_independence.py +0 -0
  178. {defessential-0.1.6 → defessential-0.1.7}/tests/test_loading.py +0 -0
  179. {defessential-0.1.6 → defessential-0.1.7}/tests/test_sse.py +0 -0
  180. {defessential-0.1.6 → defessential-0.1.7}/tests/test_startup.py +0 -0
  181. {defessential-0.1.6 → defessential-0.1.7}/tests/test_transport_conformance.py +0 -0
  182. {defessential-0.1.6 → defessential-0.1.7}/tests/test_uvicorn_transport.py +0 -0
  183. {defessential-0.1.6 → defessential-0.1.7}/tests/test_websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DefEssential
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: The Modern Synchronous Python Web Application Framework
5
5
  Home-page: https://github.com/yong5eon/DefEssential
6
6
  Author: Heo Yongseon
@@ -17,6 +17,10 @@ DefEssential.egg-info/top_level.txt
17
17
  issues/feature/2/ISSUE.md
18
18
  issues/feature/2/README.md
19
19
  issues/feature/2/REQUIREMENTS.md
20
+ issues/feature/3/IMPLEMENTS.md
21
+ issues/feature/3/ISSUE.md
22
+ issues/feature/3/README.md
23
+ issues/feature/3/REQUIREMENTS.md
20
24
  issues/feature/4/IMPLEMENTS.md
21
25
  issues/feature/4/ISSUE.md
22
26
  issues/feature/4/PLAN.md
@@ -163,6 +167,7 @@ tests/test_features.py
163
167
  tests/test_files.py
164
168
  tests/test_independence.py
165
169
  tests/test_loading.py
170
+ tests/test_logging.py
166
171
  tests/test_request_runner_hooks.py
167
172
  tests/test_samples.py
168
173
  tests/test_server.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: DefEssential
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: The Modern Synchronous Python Web Application Framework
5
5
  Home-page: https://github.com/yong5eon/DefEssential
6
6
  Author: Heo Yongseon
@@ -0,0 +1,118 @@
1
+ # 로깅 지원 구현 기록
2
+
3
+ ## 1. 개요
4
+
5
+ [로깅 지원 이슈](./ISSUE.md)와 [요구사항 명세서](./REQUIREMENTS.md)에 따라 프레임워크 로그를 `DefEssential` 계층으로 정리하고 주요 실행 경로의 로그를 보강했다.
6
+
7
+ DefStream처럼 Python 표준 `logging`으로 로그를 기록하며, 출력 설정은 샘플의 Application 시작 훅에서 담당한다. 이 문서는 2026-10-25 기준 최종 구현과 앞선 구현 작업에서 확인한 검증 결과를 기록한다.
8
+
9
+ ## 2. 최종 구현 방식
10
+
11
+ - 클래스는 `logger = getLogger('DefEssential.…')`로 고정 이름을 선언하고 메서드에서 직접 사용한다.
12
+ - 공통 함수는 담당 경계의 모듈 로거를 사용한다. 로거를 전달하기 위한 공개 인자는 추가하지 않았다.
13
+ - 사용자 하위 클래스는 기존 로거를 상속하며 필요하면 명시적으로 재정의할 수 있다.
14
+ - 라이브러리는 핸들러·포매터·필터·레벨을 설정하거나 기존 사용자 설정을 제거하지 않는다. `basicConfig()`와 `NullHandler`도 추가하지 않았다.
15
+ - 로깅 때문에 공개 훅 계약이나 요청·응답 처리 순서를 바꾸지 않는다.
16
+
17
+ ### 2.1. 후속 검토에 따른 단순화
18
+
19
+ 초기 구현의 로깅 전용 구조를 다음과 같이 제거했다.
20
+
21
+ - 단계 로그용 `stage()` context manager와 자동 단계별 타이머를 제거하고 처리 지점의 직접 logger 호출로 바꿨다.
22
+ - 라우트·어댑터 전달용 `REQUEST_ROUTE`, `SERVER_ADAPTER` ContextVar와 설정·해제 코드를 제거했다.
23
+ - 클래스명·문자열 처리 함수도 표준 Python 속성과 표현으로 대체하고 별도 로깅 유틸 파일을 삭제했다.
24
+
25
+ 라우트는 Application의 등록·선택 지점에서, 어댑터는 각 서버의 생명주기 지점에서 기록한다. 요청 실행 단계와 HTTP 전송 결과는 지역 변수로 관리하며 로깅용 공유 상태를 두지 않는다.
26
+
27
+ ## 3. 기록 범위와 레벨
28
+
29
+ | 대상 | 로거 | 구현 내용 |
30
+ | --- | --- | --- |
31
+ | [Application](../../../src/Application.py) | `DefEssential.Application` | 시작·종료, 확장 연결·롤백·정리, 라우트 등록·선택, 훅 실행과 디스패치 |
32
+ | [요청 실행기](../../../src/Runners/RunRequest.py) | `DefEssential.RequestRunner` | 인증·검증·본문 처리·핸들러 생성·실행, 요청·응답·오류 훅과 직접 출력 단계 |
33
+ | [확장 실행기](../../../src/Runners/RunExtension.py) | `DefEssential.Extension` | 확장 호출과 요청·응답·오류 훅 |
34
+ | [공통 요청 처리기](../../../src/Adapter/RequestHandler.py) | `DefEssential.Server` | HTTP 수신, 오류 응답 선택, 최종 전송 결과와 본문 정리 실패 |
35
+ | [Python](../../../src/Adapter/Implements/Python/Server.py)·[Gunicorn](../../../src/Adapter/Implements/Gunicorn/Server.py)·[Uvicorn](../../../src/Adapter/Implements/Uvicorn/Server.py) 서버 | `DefEssential.Server` | 어댑터 식별, 실제 바인딩 후 준비 완료, 시작·종료·실패 |
36
+ | [WebSocket](../../../src/IO/WebSocket/WebSocket.py) | `DefEssential.IO.WebSocket` | 연결 상태, 종료 코드, 프로토콜 위반과 내부 실패 |
37
+ | [내장 확장](../../../src/Extensions) | `DefEssential.Extensions.*` | File·FileSystem·Ping·Metrics·Status·Specifications·Swagger의 주요 처리와 실패 |
38
+
39
+ | 레벨 | 기록 기준 |
40
+ | --- | --- |
41
+ | DEBUG | 등록·선택, 검증·실행·훅의 진입과 완료, 정상 분기 및 관찰 정보 |
42
+ | INFO | 애플리케이션·서버 생명주기, HTTP 전송 결과 요약 |
43
+ | WARNING | 클라이언트 연결 단절, WebSocket 프로토콜 위반 |
44
+ | ERROR | 예상하지 못한 내부 오류, 시작·전송·정리 실패 |
45
+
46
+ 일반적인 4xx 응답이라는 이유만으로 WARNING이나 ERROR로 승격하지 않는다. 실패한 단계 뒤에 정상 완료 로그를 남기지 않으며, 샘플의 훅 관찰 로그는 DEBUG로 구분한다.
47
+
48
+ ## 4. 요청 처리와 오류 경계
49
+
50
+ ### 4.1. 단계 로그
51
+
52
+ [RunRequest](../../../src/Runners/RunRequest.py)는 기존 실행 흐름 안에서 직접 로그를 기록한다. 지역 `phase` 값으로 실패 단계를 식별하고, 실제 호출이 끝난 뒤에만 완료를 기록한다. 로깅만을 위한 별도 실행 래퍼는 두지 않았다.
53
+
54
+ ### 4.2. HTTP 전송 결과
55
+
56
+ [공통 요청 처리기](../../../src/Adapter/RequestHandler.py)는 요청별 전송 요약을 INFO로 한 번 기록한다.
57
+
58
+ | 항목 | 의미 |
59
+ | --- | --- |
60
+ | `request_id` | 서버 경계에서 생성한 최초 요청의 ID. 생성 전 실패하면 `None` |
61
+ | `method` | 앞 32자를 `ascii()`로 이스케이프한 메서드. 요청 생성 전에는 `None` |
62
+ | `status` | writer가 실제 응답을 시작한 경우의 상태. 시작 전에는 `None` |
63
+ | `outcome` | `completed`, `failed`, `disconnected` |
64
+ | `duration` | 공통 처리 진입부터 전송 결과 기록 직전까지 `monotonic()`으로 측정한 초 단위 시간 |
65
+
66
+ 요약에는 원시 요청 경로·쿼리, 라우트나 어댑터 전달용 상태를 추가하지 않았다.
67
+
68
+ - 반환 응답 전송 또는 직접 출력 종료를 확인한 경우에만 `completed`로 기록한다.
69
+ - 쓰기·flush·iterable 소비 실패는 성공으로 처리하지 않는다. 연결 단절은 `disconnected`로 구분한다.
70
+ - 핸들러·응답 훅 완료는 실제 전송 완료가 아니다. 101 업그레이드 역시 WebSocket 세션 전체의 성공을 뜻하지 않는다.
71
+ - 전송 요약 뒤의 본문 정리 실패는 별도 ERROR로 기록한다. 전송 시간에는 이 후속 정리 시간이 포함되지 않는다.
72
+ - Python 기본 HTTP 처리기의 헤더 생성 시점 접근 로그는 DEBUG로 구분해 INFO 요약과 중복되지 않게 했다.
73
+
74
+ ### 4.3. 예외 처리
75
+
76
+ - 프레임워크의 HTTP 오류는 기존 오류 응답으로 변환한다.
77
+ - 응답 시작 전 예상하지 못한 예외는 최종 서버 경계에서 ERROR와 traceback을 기록하고 기존 500 응답으로 처리한다.
78
+ - 응답 시작 후 실패는 두 번째 오류 응답을 보내지 않고 기록 후 전파한다.
79
+ - 연결 단절은 WARNING으로 기록하며 예외 문자열과 traceback을 반복 출력하지 않는다.
80
+ - 중간 실행 계층과 샘플 훅은 같은 전파 예외의 traceback을 중복 기록하지 않는다. 독립적인 정리 실패는 별개 사건으로 기록한다.
81
+
82
+ ## 5. 샘플 출력 설정과 기록 제한
83
+
84
+ [샘플](../../../samples/Sample.py)의 `LoggingSetup`을 첫 Application 시작 훅으로 등록했다.
85
+
86
+ - 루트 레벨을 DEBUG로 설정하고 이름이 `Sample`인 콘솔 핸들러를 추가한다.
87
+ - 핸들러의 출처 필터로 `Sample`·`Sample.*`, `DefEssential`·`DefEssential.*`만 선택한다. 유사 접두 이름과 외부 서버 로거는 제외한다.
88
+ - 같은 이름의 핸들러는 재시작 시 중복 추가하지 않으며 기존 사용자 핸들러는 제거하지 않는다. 다른 핸들러의 출력까지 제한하지는 않는다.
89
+ - 현재 포맷은 `%(asctime)s %(levelname)-8s %(name)s %(message)s`이며 밀리초와 레벨별 색상을 표시한다. 색상 포매터는 LogRecord 복사본을 사용한다.
90
+ - 시작 훅 이전의 구성·등록 로그를 소급 출력하지 않으며 기존 시작 배너는 유지한다.
91
+ - [일반 요청](../../../samples/runners/Requests.py)·[스트리밍](../../../samples/runners/Streaming.py) 샘플의 훅 로그는 DEBUG로 기록한다.
92
+
93
+ 기본 문맥에서 원시 URI·경로·query, 인증정보, 요청·응답 본문, WebSocket payload·close reason을 제외했다. Application은 선택된 등록 라우트의 앞 256자를 `ascii()`로 이스케이프해 기록한다. 이 길이는 이스케이프 전 문자 수 기준이다.
94
+
95
+ traceback에는 예외 메시지와 소스 줄이 포함될 수 있으므로 완전한 비밀정보 차단을 보장하지 않는다. 사용자 예외 메시지와 등록 라우트에 민감정보를 넣지 않아야 하며, 추가 필터링은 애플리케이션 설정의 책임이다.
96
+
97
+ ## 6. 검증 기록
98
+
99
+ 아래는 최종 코드 단순화 후 수행한 검증 결과다. 이번 문서 작성 중 테스트를 다시 실행한 결과는 아니다.
100
+
101
+ - Windows의 Python 3.12에서 작업 트리 소스를 직접 로드해 전체 `unittest` 발견 테스트를 실행했다.
102
+ - 총 **243개 중 238개 통과, 5개 건너뜀**을 확인했다. 설치된 별도 패키지를 대상으로 한 결과가 아니다.
103
+ - [로깅 전용 테스트](../../../tests/test_logging.py) 21개로 로거 상속·재정의, 설정 비간섭, 단계·레벨, 전송 요약, 실패·단절·정리, 병렬·중첩 요청, 민감정보 제외, 샘플 설정의 중복 방지를 검증했다.
104
+ - 기존 훅·writer·스트리밍·SSE·WebSocket 회귀와 Python·Uvicorn의 실제 HTTP 전송을 포함했다.
105
+ - 포트 0의 실제 준비 완료 로그, Uvicorn의 실제 바인딩 실패, Python 바인딩 실패의 테스트 더블 검증을 포함했다.
106
+ - 소스·샘플·테스트의 Python 3.9 구문 분석과 `git diff --check`를 통과했다. 주요 변경 파일의 Pylance 타입 오류도 확인했다.
107
+ - 로깅 유틸 파일 삭제와 잔여 import·호출 제거를 확인했다.
108
+
109
+ ### 환경 제약
110
+
111
+ - 건너뛴 5개는 POSIX가 필요한 Gunicorn 실행·전송 적합성 테스트다. 실제 Gunicorn 실행은 Linux·WSL 또는 CI에서 추가 확인해야 한다.
112
+ - Python 3.9는 구문 분석만 수행했다. 해당 버전의 실제 런타임 테스트를 통과한 것으로 간주하지 않는다.
113
+ - 라이브러리 미설정 시 표준 `logging.lastResort` 동작을 유지한다. Gunicorn·Uvicorn 자체 로깅 설정과 외부 접근 로그의 출력 정책은 별도로 관리해야 한다.
114
+
115
+ ## 7. 관련 문서
116
+
117
+ - [요구사항 명세서](./REQUIREMENTS.md)
118
+ - [변경 로그](./README.md)
@@ -0,0 +1,9 @@
1
+ # 로깅 지원
2
+
3
+ ### 해결하려는 문제
4
+
5
+ 파편화된 로깅 정리
6
+
7
+ ### 제안하는 해결 방법
8
+
9
+ DefEssential 을 Master 키 로 하여 로그 이름을 설정하고, 부족한 로깅들을 풍부하게 추가
@@ -0,0 +1,48 @@
1
+ # 로깅 지원 변경 로그
2
+
3
+ ## 추가
4
+
5
+ - Application·요청 실행·확장·내장 확장·서버 어댑터·WebSocket의 주요 단계 로그를 보강했다.
6
+ - 정상 세부 흐름은 DEBUG, 생명주기와 HTTP 전송 결과는 INFO, 연결 단절·프로토콜 위반은 WARNING, 내부·전송·정리 실패는 ERROR로 구분했다.
7
+ - HTTP 전송 결과에 요청 ID·메서드·상태·결과·단조 시계 기반 소요 시간을 기록한다.
8
+ - [로깅 테스트](../../../tests/test_logging.py) 21개를 추가하고 관련 기존 회귀 테스트를 보강했다.
9
+
10
+ ## 변경
11
+
12
+ - 클래스별 고정 `logger`와 공통 함수의 모듈 로거를 사용해 `DefEssential` 하위 계층으로 기록 책임을 정리했다.
13
+ - 라이브러리는 출력 설정을 하지 않고 사용자 로깅 설정과 기본 전파를 유지한다.
14
+ - [샘플](../../../samples/Sample.py)의 첫 Application 시작 훅에서 콘솔 출력을 설정한다. 같은 핸들러의 중복 등록을 막고 기존 사용자 핸들러는 유지한다.
15
+ - 샘플 핸들러는 `Sample`·`DefEssential` 및 하위 계층만 출력한다. 시간·레벨·로거 이름·메시지와 밀리초·레벨별 색상을 표시한다.
16
+ - Application·[일반 요청](../../../samples/runners/Requests.py)·[스트리밍](../../../samples/runners/Streaming.py) 샘플의 훅 관찰 로그를 DEBUG로 구분하고 중복 traceback을 제거했다.
17
+ - Python 기본 HTTP 처리기의 헤더 생성 시점 접근 로그를 DEBUG로 분리했다. INFO 전송 요약은 공통 서버 경계에서 한 번 기록한다.
18
+
19
+ ## 단순화
20
+
21
+ - 초기 구현의 단계 로그용 context manager와 자동 단계별 타이머를 제거했다.
22
+ - 라우트·어댑터 전달용 ContextVar와 설정·해제 코드를 제거했다.
23
+ - 별도 로깅 유틸 파일을 삭제하고 각 처리 지점에서 logger와 표준 Python 표현을 직접 사용한다.
24
+ - 공개 훅 계약과 기존 요청·응답 처리 순서를 유지하며 로깅용 공유 상태나 새 설정 API는 추가하지 않았다.
25
+
26
+ ## 동작 및 사용 주의사항
27
+
28
+ - `completed`는 서버의 전송 처리 종료를 뜻하며 클라이언트 수신 확인을 보장하지 않는다.
29
+ - 응답 생성·훅 완료와 실제 전송 완료를 구분한다. 101 응답은 WebSocket 세션 전체 성공이 아니다.
30
+ - 전송 실패·연결 단절은 성공으로 기록하지 않는다. 응답 시작 후 실패 시 두 번째 오류 응답을 보내지 않는다.
31
+ - 본문 정리 실패는 전송 결과와 별개의 ERROR로 기록한다.
32
+ - 기본 로그 문맥에서 원시 URI·경로·쿼리, 인증정보, 본문, WebSocket payload·close reason을 제외한다. 라우팅 로그는 이스케이프한 등록 라우트만 사용한다.
33
+ - traceback에는 예외 메시지와 소스 줄이 포함될 수 있다. 사용자 예외와 등록 라우트에 민감정보를 넣지 않아야 한다.
34
+ - 시작 훅 이전 로그는 소급 출력하지 않는다. 출처 제한은 샘플 핸들러에만 적용하며 외부 서버 로그와 기존 사용자 핸들러는 별도로 설정한다.
35
+
36
+ ## 검증
37
+
38
+ - 최종 코드 변경 후 전체 회귀: **243개 중 238개 통과, 5개 건너뜀**.
39
+ - Windows·Python 3.12에서 작업 트리 소스를 직접 로드해 검증했다.
40
+ - 5개 건너뜀은 Gunicorn/POSIX 환경 제약으로 Linux·WSL 또는 CI에서 추가 검증이 필요하다.
41
+ - Python 3.9 구문 분석과 공백 검사를 통과했다. Python 3.9 실제 런타임 검증은 수행하지 않았다.
42
+ - 위 결과는 앞선 구현 작업의 검증 기록이며 이번 문서 작성 중 테스트를 재실행하지 않았다.
43
+
44
+ ## 관련 문서
45
+
46
+ - [이슈](./ISSUE.md)
47
+ - [요구사항 명세서](./REQUIREMENTS.md)
48
+ - [상세 구현 기록](./IMPLEMENTS.md)
@@ -0,0 +1,326 @@
1
+ # 로깅 지원 요구사항 명세서
2
+
3
+ ## 1. 목적과 문서 상태
4
+
5
+ [로깅 지원 이슈](./ISSUE.md)의 “파편화된 로깅 정리”를 다음 두 요구로 구체화한다.
6
+
7
+ 1. `DefEssential`을 최상위 로거 이름으로 사용하여 프레임워크 로그를 계층별로 선택할 수 있게 한다.
8
+ 2. 애플리케이션 시작·종료, 요청 실행, 응답 전송, 확장 및 연결 실패를 추적하는 데 부족한 로그를 추가한다.
9
+
10
+ 이 문서는 현재 소스와 DefStream의 로깅 방식을 분석하고 사용자 확인을 반영한 **구현 기준**이다. 로거 배치, 출력 설정과 레벨별 기록 정책을 소스·샘플·테스트에 반영한다. 실제 검증 결과와 환경 제약은 아래 검증 기록에 구분하여 남긴다.
11
+
12
+ 사용자 후속 의견에 따라 샘플의 출력 설정 위치는 Application 시작 훅으로 정한다. 출력 대상은 `Sample`·`DefEssential` 계층만이며, 포맷은 DefStream의 예제와 맞춘다.
13
+
14
+ ### 1.1. 참고 근거
15
+
16
+ - [DefStream 로깅 사용 안내](../../../../DefStream/README.md#로깅)
17
+ - [DefStream 로깅 요구사항](../../../../DefStream/issues/feature/8/REQUIREMENTS.md)
18
+ - [DefStream 로깅 테스트](../../../../DefStream/tests/test_logging.py)
19
+ - [Application](../../../src/Application.py), [요청 실행기](../../../src/Runners/RunRequest.py), [확장 실행기](../../../src/Runners/RunExtension.py)
20
+ - [공통 요청 처리기](../../../src/Adapter/RequestHandler.py), [응답 writer](../../../src/IO/ResponseWriter.py)
21
+ - [Python 서버](../../../src/Adapter/Implements/Python/Server.py), [Gunicorn 서버](../../../src/Adapter/Implements/Gunicorn/Server.py), [Uvicorn 서버](../../../src/Adapter/Implements/Uvicorn/Server.py)
22
+ - [기존 샘플](../../../samples/Sample.py)
23
+
24
+ DefStream 문서의 과거 상태 설명보다 현재 사용 안내와 실제 구현을 우선한다. Worker·Broker 전용 이벤트나 별도 배포 패키지에 관한 요구는 가져오지 않는다.
25
+
26
+ ## 2. 현황과 문제 분석
27
+
28
+ | 현재 상태 | 문제와 개선 방향 |
29
+ | --- | --- |
30
+ | 기존 로거는 이미 `DefEssential.Application`, `DefEssential.Server`, `DefEssential.RequestRunner`, `DefEssential.IO.WebSocket`, 일부 `DefEssential.Extensions.*` 이름을 사용한다. | 루트 이름이 전혀 없는 문제가 아니다. 기존 이름을 가능한 한 유지하면서 클래스 멤버와 책임 경계를 일관되게 정리한다. |
31
+ | 일부 클래스는 `logger` 멤버를 사용하고, 다른 곳은 오류 처리 중 `getLogger()`를 직접 호출한다. | 클래스의 프레임워크 로그는 명시적 클래스 멤버를 사용하고, 공통 함수는 담당 경계의 명시적 로거로 귀속한다. |
32
+ | Application은 주로 확장 정리 실패를 기록하며 시작·종료 성공, 라우팅, 요청 처리 단계는 충분히 관찰되지 않는다. | 생명주기는 INFO, 등록·라우팅·훅·실행 단계는 DEBUG로 보강한다. |
33
+ | Python 서버의 `log_message()`는 기본 HTTP 처리기의 메시지를 INFO로 출력한다. | 응답 헤더 생성 시점의 접근 기록을 실제 전송 완료로 오인할 수 있다. 공통 전송 결과와 중복되지 않게 정리하고 원시 요청 대상의 노출을 막는다. |
34
+ | 공통 요청 처리기는 예상하지 못한 요청 오류, 연결 단절, 전송·정리 실패를 기록한다. | 요청 식별자, 처리 단계, 응답 상태와 성공·실패의 대응을 보강한다. |
35
+ | 응답 본문 정리 공통 함수는 `DefEssential.RequestRunner`를 사용한다. | 기존 귀속을 유지하되 본문 정리 오류와 원래 처리 오류를 서로 다른 사건으로 구분한다. |
36
+ | 샘플은 `basicConfig()`와 `Sample` 로그를 사용하며, 요청 훅에서 원시 URI를 출력한다. | 샘플도 출처 필터와 안전한 요청 문맥을 사용하며, 훅 완료와 실제 전송 완료의 의미를 구분해야 한다. |
37
+ | 시작 배너는 별도 `print()`로 출력한다. | DefStream처럼 배너는 유지한다. 로깅 도입을 이유로 배너를 제거하거나 그대로 중복 기록하지 않는다. |
38
+
39
+ ## 3. 범위와 기본 원칙
40
+
41
+ ### LOG-01. 표준 logging과 설정 책임
42
+
43
+ - Python 표준 `logging`을 사용한다. 별도 로깅 라이브러리나 `LOG_*` 래퍼 API를 추가하지 않는다.
44
+ - 라이브러리는 로그만 기록한다. import, 인스턴스 생성, 시작·종료 중 `basicConfig()`, 핸들러, `NullHandler`, 포매터, 필터, 로그 레벨을 임의로 설정하지 않는다.
45
+ - 루트 로거로의 기본 전파를 막지 않는다. 애플리케이션이 이미 설정한 로거·핸들러·레벨·필터를 변경하거나 제거하지 않는다.
46
+ - 미설정 상태의 완전한 무출력은 보장하지 않는다. 핸들러가 없으면 표준 `logging.lastResort`에 의해 WARNING 이상이 stderr에 출력될 수 있다.
47
+ - Gunicorn·Uvicorn 자체 로그의 이름을 바꾸거나 강제로 `DefEssential`에 편입하지 않는다. 외부 서버의 로깅 설정은 기존 서버 옵션과 사용자 설정의 책임이다.
48
+
49
+ ### LOG-02. 포함·제외 범위
50
+
51
+ 포함 범위는 Application, 프레임워크가 호출하는 훅과 요청 실행, 내장 확장, 세 서버 어댑터, 응답 전송 및 WebSocket의 주요 상태 변화다. 공개 메서드·훅의 인자, 반환형, 호출 순서를 로깅 때문에 변경하지 않는다.
52
+
53
+ 다음은 제외한다.
54
+
55
+ - 로그 파일 저장·회전·보관 기간, 외부 수집기 전송, JSON 전용 포맷
56
+ - 새 로깅 설정 API, 요청 추적 시스템, 외부 trace ID 전파 프로토콜
57
+ - 요청·응답 본문 덤프, WebSocket 메시지 및 SSE 이벤트 내용 기록
58
+ - 모든 자료형·검증기·descriptor·프레임의 재귀적 세부 로깅
59
+ - 자동 로그 샘플링·빈도 제한 시스템, 새로운 비동기 로깅 큐
60
+ - 로깅과 무관한 서버·라우팅·훅·예외 처리 계약 변경
61
+
62
+ ## 4. 로거 이름과 소유권
63
+
64
+ ### LOG-03. 명명·상속 규칙
65
+
66
+ - 모든 프레임워크 로거 이름은 `DefEssential` 또는 `DefEssential.`으로 시작한다. `getLogger(__name__)`로 설치 경로나 사용자 모듈에 따라 이름이 달라지게 하지 않는다.
67
+ - 로깅을 담당하는 클래스는 `logger` 클래스 멤버에 고정 이름을 선언하고 공통 메서드는 `self.logger`를 사용한다.
68
+ - 사용자 하위 클래스는 기반 클래스의 이름을 상속한다. `type(self).__name__`으로 자동 변경하지 않으며, 사용자가 `logger`를 명시적으로 재정의하는 것은 허용한다.
69
+ - 실제 사용자 핸들러는 `모듈명.정규 클래스명`, 확장·훅은 정규 클래스명과 호출 순번으로 구분한다. 사용자 코드가 직접 호출하는 로깅은 변경하지 않는다.
70
+ - 함수에 로거를 전달하기 위한 새 공개 인자나 로그 이름 판별용 추상화를 추가하지 않는다.
71
+
72
+ ### LOG-04. 로거 배치
73
+
74
+ | 대상 | 로거 이름 | 기록 책임 |
75
+ | --- | --- | --- |
76
+ | [Application](../../../src/Application.py) | `DefEssential.Application` | 등록·로드, 시작·종료, 확장 연결·정리, 라우팅, Application 훅 |
77
+ | [RunRequest](../../../src/Runners/RunRequest.py) | `DefEssential.RequestRunner` | 검증·본문 처리·생성·핸들러 실행·응답 훅의 단계 |
78
+ | [확장 실행 공통 함수](../../../src/Runners/RunExtension.py) | `DefEssential.Extension` | 확장 요청·응답·오류 훅과 확장 호출 단계. 모듈의 고정 로거 사용 |
79
+ | [Python 서버와 요청 처리기](../../../src/Adapter/Implements/Python/Server.py), [Gunicorn 서버](../../../src/Adapter/Implements/Gunicorn/Server.py), [Uvicorn 서버](../../../src/Adapter/Implements/Uvicorn/Server.py) | `DefEssential.Server` | 서버 생명주기, 어댑터 고유 실패. 이름은 공유하고 `adapter` 문맥으로 구분 |
80
+ | [공통 요청 처리기](../../../src/Adapter/RequestHandler.py) | `DefEssential.Server` | HTTP 수신, 응답 전송 결과, 오류 응답 변환·전송·최종 정리. 모듈의 고정 로거 사용 |
81
+ | [WebSocket](../../../src/IO/WebSocket/WebSocket.py) | `DefEssential.IO.WebSocket` | 업그레이드, 연결 종료, 프로토콜 위반 |
82
+ | [File](../../../src/Extensions/File.py), [FileSystem](../../../src/Extensions/FileSystem.py), [Ping](../../../src/Extensions/Ping.py), [Metrics](../../../src/Extensions/Metrics.py), [Status](../../../src/Extensions/Status.py), [Specifications](../../../src/Extensions/Specifications.py), [Swagger](../../../src/Extensions/Swagger.py) | `DefEssential.Extensions.<클래스명>` | 확장 고유의 자원 연결·문서 생성·집계·렌더링 진단 |
83
+ | [응답 본문 정리 공통 함수](../../../src/Response.py) | `DefEssential.RequestRunner` | 기존대로 본문 정리의 추가 실패를 기록. 서버 최종 정리에서 전파된 오류는 공통 요청 처리기가 기록 |
84
+
85
+ RequestRunner와 Extension의 추상 훅 인터페이스에는 로깅을 위해 별도 동작을 추가하지 않는다. 호출 전후는 실행 주체가 기록한다. 응답 writer·reader, WSGI 전송·Uvicorn 연결 보조 객체의 모든 I/O에 독립 로그를 추가하지 않고, 공통 서버 경계에서 결과를 관찰한다.
86
+
87
+ `DefEssential`로 전체 로그를, `DefEssential.Extensions`로 내장 확장 로그를, 구체 이름으로 개별 구성요소를 선택할 수 있어야 한다.
88
+
89
+ ## 5. 이벤트와 로그 레벨
90
+
91
+ ### LOG-05. 공통 레벨
92
+
93
+ | 레벨 | 기준 |
94
+ | --- | --- |
95
+ | DEBUG | 요청별 실행 단계, 등록·라우팅·훅 호출, 확장 내부 처리 요약 |
96
+ | INFO | 실제 생명주기 변화와 HTTP 전송 결과 요약 |
97
+ | WARNING | 클라이언트 연결 단절, WebSocket 프로토콜 위반처럼 내부 장애와 구분할 주의 사건 |
98
+ | ERROR | 예상하지 못한 내부 처리, 서버 실행, 전송 또는 정리 실패 |
99
+
100
+ 일반적인 400·401·403·404·405 등 기존 HTTP 오류 응답은 상태 코드만으로 WARNING·ERROR로 승격하지 않는다. 5xx 응답 객체를 반환한 사실도 Python 예외 발생과 같지 않다. 모든 응답은 전송 결과에 상태를 남기고, 실제 내부 예외는 별도 오류 정책에 따라 기록한다.
101
+
102
+ ### 5.1. 풍부한 내부 진단과 레벨 선택 기준
103
+
104
+ 사용자 요구에 따라 내부 로그는 오류 지점에만 추가하지 않는다. 정상 실행에서도 DEBUG를 활성화하면 등록부터 라우팅·검증·훅·핸들러·응답 제공·전송·정리까지 실제로 실행한 흐름을 재구성할 수 있어야 한다.
105
+
106
+ - 기존 로그의 이름만 변경하는 작업으로 끝내지 않는다. LOG-06부터 LOG-10까지의 관찰 지점별로 정상·실패·분기 경로의 기록 누락을 확인하고 보강한다.
107
+ - 로그는 필요한 처리 지점에서 표준 logger 메서드를 직접 호출한다. 로깅만을 위한 context manager·데코레이터·범용 실행 래퍼를 추가하지 않으며, 기존 예외 처리 경계에서 실패를 기록한다.
108
+ - 각 클래스에서 고정 이름의 `logger = getLogger(...)`를 직접 지정한다. 로그 문맥을 전달하기 위한 `ContextVar`·전역 요청 상태·어댑터 전달용 설정은 추가하지 않는다.
109
+ - 별도 로깅 유틸 모듈을 만들지 않는다. 클래스명은 표준 속성으로 확인하고, 필요한 문자열 이스케이프는 기록 지점에서 직접 수행한다.
110
+ - 주요 단계는 진입과 종료 결과가 대응되도록 기록한다. 실패한 단계는 성공 완료 대신 실패 단계와 예외 타입을 남긴다.
111
+ - 실행하지 않은 단계는 실행한 것처럼 기록하지 않는다. 미설정 훅·검증기를 매 요청마다 모두 나열하지 않되, OPTIONS 처리, 라우트·메서드 불일치, 반환 응답·직접 출력 선택 등 실행 경로를 바꾸는 분기는 DEBUG로 식별할 수 있어야 한다.
112
+ - 한 줄에 “요청 처리 중”만 남기지 않는다. 기존 요청 식별자, 처리 단계, 선택한 대상, 결과 등 해당 경계에서 확인 가능한 안전한 문맥을 함께 기록한다.
113
+ - 훅이 여러 개라면 종류·순번·구현 클래스로 어느 훅이 시작·완료·실패했는지 구별한다. 사용자 훅 내부 동작까지 자동 추적하거나 훅 계약을 변경하지 않는다.
114
+ - 소요 시간은 요청 실행·문서 생성·전송처럼 의미 있는 경계에 기록한다. 모든 함수에 타이머를 추가하거나 필드 변환마다 로그를 남기는 방식으로 양만 늘리지 않는다.
115
+ - DEBUG는 정상 요청의 상세 진단에 사용한다. INFO는 상세 단계 없이도 서버·Application의 상태 변화와 요청별 전송 결과를 파악할 수 있는 운영 요약으로 유지한다.
116
+ - WARNING은 처리를 계속하거나 연결을 정리할 수 있지만 주의가 필요한 비정상 사건에 사용한다. 일반적인 라우팅 실패·입력 검증 거부·정상 종료를 경고로 부풀리지 않는다.
117
+ - ERROR는 실제 내부 처리·전송·정리 실패에 사용한다. 사용자가 기본 INFO 수준으로 운영해도 장애가 DEBUG에만 묻히지 않도록 최종 오류 경계에서 기록한다.
118
+ - 수준 선택은 사용자 설정으로 정한 임계값에 맡기되, 사건 자체의 로그 레벨은 라이브러리 내부에서 일관되게 지정한다. 풍부한 로그를 출력하기 위해 라이브러리가 `setLevel(DEBUG)`를 호출해서는 안 된다.
119
+ - 동일 사건을 여러 레벨로 복제하지 않는다. DEBUG 단계 실패, ERROR 최종 예외, INFO 전송 결과는 각기 다른 정보를 제공할 때만 함께 남긴다.
120
+ - 상세 로그에도 LOG-13의 민감정보 제외와 LOG-14의 비용·동시성 기준을 동일하게 적용한다.
121
+
122
+ ### LOG-06. Application과 서버 생명주기
123
+
124
+ - INFO: Application 시작 시도·완료, 종료 시작·완료. 완료는 해당 훅과 확장 처리를 실제로 마친 뒤에만 기록한다.
125
+ - DEBUG: 핸들러·확장 라우트 등록, 모듈 로드 요약, 확장 시작·정리 순서, 훅 호출 전후.
126
+ - ERROR: 시작 실패, 종료 중 각 독립적인 정리 실패. 시작 실패 후 정리 실패는 원래 시작 실패와 별도 사건으로 남긴다.
127
+ - INFO: 서버 실행 시도, 실제 수신 준비 완료, 종료 요청과 종료 결과. `adapter`, 호스트·포트를 포함한다.
128
+ - 바인딩 전의 설정 주소를 실제 수신 주소처럼 표현하지 않는다. 포트 `0`의 준비 완료 로그에는 확인된 실제 할당 포트를 사용한다.
129
+ - Gunicorn은 마스터 준비와 워커별 Application 시작·종료를 구분한다. 워커 로그는 표준 프로세스 문맥으로 구별할 수 있어야 한다.
130
+ - ERROR: 바인딩·서버 실행 실패. Application 생명주기에서 이미 기록한 예외를 서버가 같은 traceback으로 반복하지 않는다.
131
+ - 중복 종료 호출로 상태가 바뀌지 않았다면 새로운 “종료 완료”를 반복 기록하지 않는다.
132
+ - [시작 배너](../../../src/Startup.py)의 출력 내용·호출 계약과 색상 처리는 유지한다. 배너 출력만으로 서버 준비 완료를 판단하지 않는다.
133
+
134
+ ### LOG-07. 요청 실행과 훅
135
+
136
+ - DEBUG: Application 요청 진입, 선택한 라우트 템플릿과 핸들러, Application 요청·응답·오류 훅의 호출·종료·실패 단계.
137
+ - DEBUG: RunRequest의 CORS 검사, 인증·파라미터·쿼리·헤더 검증, 본문 처리, 핸들러 인스턴스 생성, 요청 훅, `run()`, 응답 훅, CORS 응답 후처리.
138
+ - DEBUG: 확장 실행기의 확장 요청·응답·오류 훅과 실제 확장 호출.
139
+ - 단계 실패에는 단계명과 예외 타입을 남기되, 상위 서버로 전파되는 동일 예외의 traceback을 계층마다 반복하지 않는다.
140
+ - RequestRunner의 응답 훅은 반환 응답 경로와 직접 출력 경로에서 기존 호출 시점 그대로 관찰한다. 로깅을 위해 훅을 다시 실행하지 않는다.
141
+ - 실행 완료·응답 제공은 DEBUG로 기록한다. 이를 네트워크 전송 성공 또는 스트림 소비 완료라고 표현하지 않는다.
142
+ - Application·RunRequest를 서버 밖에서 직접 호출하는 경우에도 DEBUG 단계 로그와 기존 예외 전파를 제공한다. 최종 오류 기록은 호출자 책임이며, 이를 위해 추가 서버 경계를 만들지 않는다.
143
+
144
+ ### LOG-08. HTTP 수신과 전송 결과
145
+
146
+ - 공통 요청 처리기가 요청 수신을 DEBUG로, 각 요청의 전송 결과 요약을 INFO로 한 번 기록한다. 내부 오류의 ERROR 로그와 결과 요약은 서로 다른 목적의 사건이다.
147
+ - 전송 결과에는 확인 가능한 요청 식별자, 메서드, 상태 코드, 결과, 전송 경계 소요 시간을 포함한다. 라우트는 Application의 라우트 선택 로그에서 기록하고 요청 ID로 연결한다. 공통 전송 요약에 경로·어댑터를 모으기 위한 문맥 전달 구조는 만들지 않는다.
148
+ - 결과는 적어도 `completed`, `failed`, `disconnected`를 구분한다. 상태가 정해지지 않았다면 가짜 `200`이나 writer의 초기값 `500`으로 채우지 않는다.
149
+ - `completed`는 헤더 전송 시점이 아니라 writer 종료까지 성공한 경우에만 사용한다. 본문 소비·쓰기·flush·종료 중 실패했다면 앞서 200을 시작했어도 `failed`다.
150
+ - 응답 반환, 직접 출력, iterable 응답, chunked 출력, SSE, HEAD와 본문 없는 응답에 같은 결과 기준을 적용한다.
151
+ - 응답 전송 완료는 프레임워크가 전송 계층으로 전달을 마쳤다는 뜻이다. WSGI·ASGI 서버의 소켓 전송 완료나 클라이언트 수신까지 보장한 것처럼 기록하지 않는다.
152
+ - 전송 경계 시간은 공통 처리 진입부터 writer 종료 또는 실패까지다. RunRequest 실행 시간, 후속 자원 정리 시간과 혼합하지 않으며 단조 시계를 사용한다.
153
+ - 최종 본문 정리 실패는 별도 ERROR다. 이미 완료한 전송을 미전송으로 바꾸지 않고, 정리까지 성공한 것처럼 표현하지 않는다.
154
+ - 응답 크기는 이번 필수 항목이 아니다. 나중에 기록한다면 Content-Length를 실제 송신량으로 오인하거나 측정하려고 iterable을 미리 소비해서는 안 된다.
155
+ - Python 기본 접근 로그와 새 공통 결과 로그가 같은 요청을 중복 요약하지 않도록 한다. 공통 처리 진입 전 HTTP 파싱 거부는 어댑터가 안전한 상태·사유 코드로 기록한다.
156
+ - Gunicorn·Uvicorn 자체 접근 로그는 별도 출처다. 라이브러리가 강제로 끄지 않고, 중복 출력을 원치 않는 사용자가 외부 접근 로그를 비활성화하는 기존 옵션을 문서화한다.
157
+
158
+ ### LOG-09. 내장 확장
159
+
160
+ | 확장 | 필요한 진단 | 주의사항 |
161
+ | --- | --- | --- |
162
+ | File | DEBUG: 파일 응답 준비와 결과 요약 | 파일 내용·로컬 절대 경로를 출력하지 않는다. 파일 없음·권한 거부의 기존 HTTP 변환을 유지한다. |
163
+ | FileSystem | DEBUG: 연결·정리 단계, 요청 파일 응답 준비 | 기반 디렉터리·사용자 입력 경로 전체를 출력하지 않는다. 원래 실패를 보존하는 추가 정리 실패는 ERROR로 기록한다. |
164
+ | Ping | DEBUG: 판정한 Application 실행 상태 | 정상 시작·종료 상태의 503을 매번 WARNING으로 기록하지 않는다. 새로운 probe나 상태 변화 추적기를 추가하지 않는다. |
165
+ | Metrics | DEBUG: 관찰 훅 연결·해제, 집계 초기화, 스냅샷·렌더링 요약 | 측정 대상 요청 실패를 다시 ERROR로 기록하거나 로그를 메트릭 집계 입력으로 삼지 않는다. |
166
+ | Status | DEBUG: 상태 페이지 생성 요약 | Metrics URI를 사용하는 현재 계약을 유지한다. 외부 지표 조회나 연결 확인 기능을 추가하지 않는다. |
167
+ | Specifications | DEBUG: 문서 생성 시작·완료, 라우트 수와 소요 시간 | 전체 스키마·예제·인증 설정을 덤프하지 않는다. |
168
+ | Swagger | DEBUG: 문서 UI 응답 생성 요약 | 외부 문서·스크립트 응답을 새로 수집하거나 출력하지 않는다. |
169
+
170
+ 공통 확장 등록·시작·종료는 Application이 기록한다. 각 확장은 고유 처리만 추가하고 공통 HTTP 접근 기록을 반복하지 않는다. 상위로 전파되는 확장 요청 실패의 최종 ERROR·traceback은 서버 경계가 담당한다.
171
+
172
+ ### LOG-10. 스트리밍과 WebSocket
173
+
174
+ - 스트림 시작·종료·실패를 관찰하되 chunk, SSE 이벤트, WebSocket frame마다 로그를 추가하지 않는다.
175
+ - WebSocket 업그레이드 성공과 정상 종료는 DEBUG, 프로토콜 위반은 WARNING으로 기록한다. 연결 식별에는 기존 요청 식별자를 사용한다.
176
+ - 101 상태와 업그레이드 성공을 WebSocket 세션 전체 성공으로 표현하지 않는다. 세션 종료·실패는 구분하여 남긴다.
177
+ - WebSocket 경로에서도 공통 서버 처리기의 결과 요약은 한 번만 기록한다. Uvicorn 연결 보조 계층은 이미 기록된 요청 예외를 다시 ERROR로 기록하지 않는다.
178
+ - 정상 close와 서버 종료에 따른 취소는 내부 장애로 승격하지 않는다. 예상하지 못한 연결 단절은 서버 경계에서 WARNING으로 기록한다.
179
+ - close code와 프레임워크가 정의한 안전한 사유 코드는 기록할 수 있지만, 상대가 보낸 close reason·메시지·ping payload는 기록하지 않는다.
180
+ - 프로토콜 위반을 경고로 기록한 뒤 오류 close 전송도 실패했다면 서로 다른 사건으로 구분한다.
181
+ - 일반 WSGI writer의 업그레이드 미지원 계약과 Gunicorn 전용 writer의 기존 업그레이드 지원을 각각 유지한다. 로깅을 위해 전송 기능을 추가하거나 제거하지 않는다.
182
+
183
+ ## 6. 오류 기록과 기존 동작 보존
184
+
185
+ ### LOG-11. 최종 오류의 기록 책임
186
+
187
+ | 사건 | 최종 기록 위치·수준 |
188
+ | --- | --- |
189
+ | 요청 실행 중 예상하지 못한 예외, 응답 시작 전 | 공통 요청 처리기 ERROR + traceback. 기존대로 500 응답을 시도 |
190
+ | 응답 시작 이후 실행·본문 소비·전송 실패 | 공통 요청 처리기 ERROR + traceback. 두 번째 오류 응답을 보내지 않음 |
191
+ | 요청·전송 중 연결 단절 | 공통 요청 처리기 WARNING. 내부 오류와 같은 사건으로 중복 ERROR 기록하지 않음 |
192
+ | 공통 처리기에 도달하지 못한 어댑터 내부 실패 | 해당 어댑터의 `DefEssential.Server` ERROR + traceback |
193
+ | Application 시작·종료 실패 | Application ERROR + traceback. 확장 내부의 동일 전파 예외와 중복하지 않음 |
194
+ | 처리 실패 중 추가 자원 정리 실패 | 원래 실패를 보존하는 정리 지점에서 별도의 ERROR + traceback |
195
+ | 직접 호출한 Application·RunRequest의 전파 예외 | DEBUG 단계 관찰 후 기존대로 전파. 최종 ERROR는 외부 호출자 책임 |
196
+
197
+ - 같은 예외가 확장 → RunRequest → Application → 서버로 전파되었다고 매번 ERROR·traceback을 남기지 않는다. 새 예외를 던진 오류 훅은 실패 단계와 예외 연결 관계로 구분한다.
198
+ - 중복 제거를 위해 예외 객체에 임의 속성을 붙이거나 전역 예외 캐시를 추가하지 않는다. 기록 경계를 배치하여 해결한다.
199
+ - 로깅을 위해 예외를 삼키거나 `None`, 더미 응답, 성공 상태로 대체하지 않는다. 기존의 예외 전파, 정리 순서, 첫 정리 오류 보존 계약을 유지한다.
200
+ - `KeyboardInterrupt`·`SystemExit` 등 BaseException 전체를 일반 요청 오류로 포착하지 않는다. 기존 종료 제어 흐름을 유지한다.
201
+ - 사용자 훅이 자체 기록한 로그나 외부 서버의 독립 오류 로그까지 중복 제거한다고 보장하지 않는다.
202
+
203
+ ## 7. 문맥·민감정보·비용
204
+
205
+ ### LOG-12. 로그 문맥
206
+
207
+ - 일반 포매터의 `%(name)s`, `%(levelname)s`, `%(message)s`만으로도 사건을 이해할 수 있게 한다. 사용자 정의 필드가 반드시 필요한 포매터를 강요하지 않는다.
208
+ - 요청 생성 후에는 기존 `Request.id`를 사용한다. 기존 식별자를 외부 헤더 값으로 교체하거나 새 추적 ID를 도입하지 않는다.
209
+ - Request 생성 전 파싱 실패처럼 없는 문맥은 생략한다. 존재하지 않는 요청 ID·상태·소요 시간을 정상 값으로 만들어 넣지 않는다.
210
+ - 요청 훅이 다른 Request를 반환할 수 있다는 기존 계약을 보존한다. 교체 전후 경계에서 관찰한 식별자를 구분하고, 한 서버 요청의 수신·결과 대응에는 최초 생성한 Request의 ID를 유지한다.
211
+ - 훅이 Request를 교체하면 DEBUG로 이전·새 ID의 대응을 남기되, 요청 객체를 변경하거나 새 공개 Context 인자를 추가하지 않는다.
212
+ - 단계 로그에는 단계명, 대상 핸들러·확장·훅의 정규 클래스명, 결과를 포함한다. 반복 훅은 종류와 순번으로 구분한다.
213
+ - 서버별 생명주기·고유 실패 로그에는 해당 지점에서 아는 어댑터 이름을 포함한다. 공통 요청 처리기로 어댑터 이름을 전파하지 않는다. 다중 워커 구분은 표준 `process`·`processName`과 `threadName`을 사용자가 포매터에 넣을 수 있게 안내한다.
214
+ - 새 구조화 로그 스키마나 로그 문구 전체를 공개 API로 확정하지 않는다. 테스트는 필요한 문맥과 의미를 검증한다.
215
+
216
+ ### LOG-13. 민감정보와 외부 입력
217
+
218
+ - DEBUG를 포함하여 요청·응답 본문, 인증 헤더, Cookie·Set-Cookie, 세션, 토큰, 비밀번호, 검증 대상 값, 파일 내용은 기록하지 않는다.
219
+ - 원시 URI·query string, 파라미터·헤더 전체, Request·Response·사용자 객체의 `repr()`를 출력하지 않는다.
220
+ - 원시 요청 경로·쿼리는 출력하지 않는다. Application의 선택 로그는 등록 라우트의 앞 256자를 `ascii()`로 이스케이프하여 사용한다. 매칭 전에는 요청 ID로만 식별한다.
221
+ - 길이 제한·이스케이프는 등록 경로의 비밀 값을 자동 제거하지 않는다. 등록 라우트와 URL 경로에 비밀 값을 넣지 말아야 함을 사용 안내에 명시한다.
222
+ - 사용자 입력을 포맷 문자열로 사용하지 않는다. 지연 포맷팅 인자로 전달하며, 한 사건이 개행·제어 문자로 다른 로그처럼 보이지 않게 한다.
223
+ - 오류의 일반 문맥에는 예외 타입과 안전한 단계·사유 코드를 사용하고 `str(error)`를 무조건 덧붙이지 않는다.
224
+ - 내부 오류 traceback에는 예외 메시지·연결된 예외·소스 줄이 포함될 수 있다. 이를 완전한 비밀정보 차단으로 설명하지 않으며, 사용자 예외 메시지의 민감정보 금지와 애플리케이션 측 필터링 책임을 문서화한다.
225
+ - traceback 진단을 제거하거나 숨기기 위한 임의의 광범위 마스킹 시스템은 이번 범위에 추가하지 않는다.
226
+
227
+ ### LOG-14. 비용과 동시성
228
+
229
+ - DEBUG 비활성 시 로그만을 위한 문서 생성, 집계 스냅샷, 객체 직렬화, 본문 읽기를 하지 않는다. 필요한 고비용 문맥은 레벨 확인 후 계산한다.
230
+ - 크기·시간 측정을 위해 스트림을 버퍼링·재소비하거나 요청·응답 전달 순서를 바꾸지 않는다.
231
+ - 병렬 요청의 ID·단계·타이머를 클래스나 모듈의 공유 가변 상태에 저장하지 않는다.
232
+ - 새 잠금·백그라운드 스레드·파일 I/O를 로깅 목적으로 추가하지 않는다.
233
+ - 라이브러리가 로깅 실패를 숨기는 광범위 예외 처리나 대체 출력 경로를 추가하지 않는다.
234
+
235
+ ## 8. 사용자 안내와 샘플
236
+
237
+ ### LOG-15. 설정 예제와 문서
238
+
239
+ 구현 시 [사용 안내](../../../README.md)에 로거 목록, 레벨, 기록 책임, 미설정 동작, 민감정보 한계와 다음 형태의 설정 예제를 추가한다. [샘플](../../../samples/Sample.py)은 `ApplicationStartup` 구현을 `on_startup`의 첫 번째 훅으로 등록하여 출력 설정을 수행한다. import 시점이나 실행 진입점의 `basicConfig()`로 설정하지 않는다.
240
+
241
+ ```python
242
+ from logging import DEBUG, Filter, Formatter, StreamHandler, getLogger
243
+
244
+ from DefEssential import Application, ApplicationStartup
245
+
246
+
247
+ class LoggingSetup(ApplicationStartup):
248
+ def __init__(self) -> None:
249
+ sources = (Filter('Sample'), Filter('DefEssential'))
250
+ self.handler = StreamHandler()
251
+ self.handler.set_name('Sample')
252
+ self.handler.setFormatter(Formatter(
253
+ '%(asctime)s %(levelname)-8s %(name)s %(message)s'
254
+ ))
255
+ self.handler.addFilter(
256
+ lambda record: any(source.filter(record) for source in sources)
257
+ )
258
+
259
+ def __call__(self) -> None:
260
+ root = getLogger()
261
+ root.setLevel(DEBUG)
262
+ if not any(handler.name == self.handler.name for handler in root.handlers):
263
+ root.addHandler(self.handler)
264
+
265
+
266
+ app = Application(on_startup=LoggingSetup())
267
+ ```
268
+
269
+ - 필터는 루트 로거가 아니라 핸들러에 적용한다. `Sample`·`Sample.*`와 `DefEssential`·`DefEssential.*`만 선택하고 `SampleExtra`, `DefEssentialExtra`, 외부 라이브러리 로거는 제외한다.
270
+ - 출력 포맷은 DefStream을 참고하되 최종 샘플 설정인 `%(asctime)s %(levelname)-8s %(name)s %(message)s`를 사용한다. 실제 샘플의 `LoggingFormatter`는 밀리초와 레벨별 색상을 표시한다. 위 예제는 출처 필터와 시작 훅 설정에 집중한 기본 형태다.
271
+ - 위 예제는 독립 실행 애플리케이션용이다. 기존 핸들러를 강제로 제거하거나 `force=True`로 덮어쓰지 않는다. 출처 제한은 샘플이 추가한 핸들러에 대한 보장이며, 다른 기존 핸들러의 출력까지 차단하지 않는다.
272
+ - 동일 Application의 중지 후 재시작에도 같은 설정 훅이 핸들러를 중복 등록하지 않아야 한다. 기존 시작 관찰 훅 등 다른 시작 훅은 출력 설정 훅 뒤에 실행한다.
273
+ - 시작 훅 이전의 Application 구성·라우트 등록·시작 시도 로그는 이 설정으로 소급 출력할 수 없다. 해당 로그 수집을 위해 라이브러리의 시작 순서를 바꾸거나 버퍼를 추가하지 않는다. 시작 배너는 기존처럼 로깅과 별도로 출력된다.
274
+ - 샘플에는 `DefStream` 등 다른 출처를 추가하지 않는다. 두 프레임워크를 함께 수집하는 별도 사용자 설정과 구분한다.
275
+ - [샘플](../../../samples/Sample.py)은 `Sample`을 유지하되 출처 제한 설정을 사용하고 원시 URI·예외 메시지를 무조건 출력하지 않는다.
276
+ - 샘플의 훅 관찰 로그는 DEBUG로 두고 “훅 완료”로 표현하여 서버의 INFO 전송 결과와 의미·빈도가 겹치지 않게 한다.
277
+ - 새 필수 설치 의존성을 추가하지 않는다. Gunicorn 워커의 로깅은 기존 사용자 설정·서버 훅으로 구성하는 방법을 안내하고 부모 핸들러의 자동 전달을 보장하지 않는다.
278
+ - Uvicorn 자체 로그 설정을 보존하려면 기존 `log_config` 옵션을 어떻게 사용하는지, 외부 접근 로그 중복은 기존 `access_log`·`accesslog` 옵션으로 어떻게 제어하는지 어댑터별로 안내한다.
279
+
280
+ ## 9. 수용 기준과 검증
281
+
282
+ 구현 완료는 아래 항목을 테스트와 실제 실행 가능한 샘플로 확인한 상태를 뜻한다. 현재 문서 작성만으로 통과했다고 간주하지 않는다.
283
+
284
+ | ID | 검증 시나리오 | 기대 결과 |
285
+ | --- | --- | --- |
286
+ | AC-01 | import·생성·시작·종료 전후 로깅 설정 비교 | 라이브러리가 핸들러·레벨·필터·전파 설정을 변경하지 않음. NullHandler도 추가하지 않음 |
287
+ | AC-02 | 클래스 멤버, 하위 클래스 상속, 사용자 `logger` 재정의 | 명시된 기본 이름 유지 및 명시적 재정의 적용 |
288
+ | AC-03 | 상위·하위 이름별 핸들러 필터 | `DefEssential`, `DefEssential.Extensions`, 개별 확장 선택 가능. `DefEssentialExtra` 제외 |
289
+ | AC-04 | Application 정상 시작·종료, 중복 종료 | 실제 전환에 대응하는 INFO와 단계 DEBUG. 중복 성공 기록 없음 |
290
+ | AC-05 | 확장 시작 실패·롤백 실패·여러 정리 실패 | 각 독립 실패를 구분하고 원래 예외·정리 순서 보존 |
291
+ | AC-06 | 정상 HTTP 요청, 404·405, 검증 거부, 500 응답 객체 | 요청당 INFO 결과 요약 1회, 올바른 상태, 일반 HTTP 오류의 불필요한 경고 없음 |
292
+ | AC-07 | 핸들러·본문 처리·요청/응답/오류 훅의 내부 예외 | 단계 식별 가능, 서버 경계의 최종 ERROR·traceback은 동일 전파 예외당 1회 |
293
+ | AC-08 | 응답 시작 전 실패·시작 후 실패 | 전자는 기존 오류 응답 변환, 후자는 두 번째 응답 없이 실패 요약과 연결 종료 |
294
+ | AC-09 | bytes·iterable·직접 출력·chunked·SSE·HEAD·204/205/304 | writer 종료 기준 결과. 본문 사전 소비·중복 종료·본문 없는 응답의 오송신 없음 |
295
+ | AC-10 | write·flush·iterable 소비 실패, 클라이언트 단절 | 200을 시작했어도 성공 기록 없음. 실패·단절 분류와 기존 전파 보존 |
296
+ | AC-11 | 전송 성공 뒤 본문 정리 실패 | 전송 결과와 별도 정리 ERROR를 구분. 정리 실패를 숨기지 않음 |
297
+ | AC-12 | 정상/비정상 WebSocket, 업그레이드 거부, 연결 중 종료 | 101과 세션 결과 구분, 프로토콜 경고·내부 오류 분리, payload·close reason 제외 |
298
+ | AC-13 | query 토큰, Cookie, 본문, 경로 개행·긴 경로를 포함한 요청 | 기본 문맥에 비밀 본문·인증정보 없음. 원시 경로·쿼리 제외, 선택된 등록 라우트만 길이 제한 후 이스케이프 |
299
+ | AC-14 | 병렬 요청과 Request 교체 훅 | 서로 다른 요청 문맥 혼합 없음. 서버 수신·결과의 원래 ID 대응과 교체 ID 추적 가능 |
300
+ | AC-15 | 제어 가능한 단조 시계로 소요 시간 검증 | 정의한 경계의 실제 차이를 기록하며 실패 경로도 측정. 임의의 상수·벽시계 차이로 대체하지 않음 |
301
+ | AC-16 | DEBUG 비활성, 감시 가능한 직렬화·본문 객체 | 로그를 위한 고비용 계산·본문 읽기 없음 |
302
+ | AC-17 | Python·Gunicorn·Uvicorn에서 동등 요청 | 프레임워크 로거·결과 의미 일관성, 서버 고유 로그에서 어댑터 구별, 프레임워크 자체 접근 요약 중복 없음 |
303
+ | AC-18 | 포트 0, 서버 시작·바인딩 실패 | 준비 완료는 실제 바인딩 후 실제 포트로 기록. 실패를 정상 시작으로 기록하지 않음 |
304
+ | AC-19 | 샘플 실행 및 설정 없는 별도 프로세스 실행 | Application 첫 시작 훅에서 설정. Sample/DefEssential 및 하위 계층만 선택 출력하고 유사 접두 이름·외부 로거는 제외. DefStream과 같은 포맷, 라이브러리 배너 보존, 미설정 lastResort 동작을 임의 차단하지 않음 |
305
+ | AC-20 | Application·RunRequest 직접 실행 | DEBUG 단계와 기존 결과·예외 전파 유지. 서버 전송 완료로 잘못 기록하지 않음 |
306
+ | AC-21 | 샘플 Application 생성·시작·중지·재시작 | 생성만으로 전역 설정을 변경하지 않음. 설정 훅 이후 로그를 수신하며 같은 핸들러가 중복 등록되지 않음. 기존 사용자 핸들러를 제거하지 않음 |
307
+ | AC-22 | DEBUG 수준에서 정상·검증 거부·훅 실패·직접 출력 요청 추적 | 실행한 주요 단계의 진입과 완료/실패가 대응하고 분기·대상 식별 가능. 실패 뒤 성공 완료나 미실행 단계의 실행 기록 없음 |
308
+ | AC-23 | 같은 시나리오를 DEBUG·INFO·WARNING·ERROR 임계값으로 수집 | 각 사건의 지정 레벨을 LogRecord에서 검증. INFO에서는 상세 단계 없이 생명주기·전송 요약 수신, WARNING·ERROR 사건은 해당 임계값에서 누락되지 않음. 라이브러리의 레벨 강제 변경 없음 |
309
+
310
+ 검증은 [기존 테스트](../../../tests)의 실행 방식과 테스트 더블을 재사용한다. 요청·훅·writer·전송 적합성 테스트를 우선 실행하고 관련 회귀 테스트로 확대한다. Gunicorn 실제 실행은 POSIX/Linux 또는 WSL에서 확인하며, Windows의 미지원 또는 선택 의존성 부재를 성공으로 처리하지 않고 검증 제약으로 기록한다.
311
+
312
+ ### 9.1. 구현과 검증 기록
313
+
314
+ - Application·RunRequest·확장 실행·내장 확장·세 서버 어댑터·WebSocket에 명시적 로거와 단계별 기록을 연결했다.
315
+ - 후속 검토에 따라 로깅용 `stage()` context manager를 제거하고 각 처리 지점의 직접 logger 호출로 단순화했다. 모든 단계의 자동 타이머·예외 포착 대신 기존 실행·정리 경계와 필요한 요청·문서 생성 시간 측정만 유지한다.
316
+ - 추가 검토에서 남은 클래스명·문자열 유틸도 호출 지점의 표준 Python 표현으로 바꾸고 로깅 유틸 파일을 삭제했다. 사용자 원시 요청 경로는 기록하지 않는다.
317
+ - HTTP 전송 결과는 공통 서버 경계의 지역 변수만으로 한 번 기록한다. 후속 검토에 따라 라우트·어댑터 전달용 ContextVar와 설정·해제 코드를 제거했다. 라우트는 Application에서, 어댑터 이름은 각 서버에서 직접 기록하며, 전송 요약은 최초 요청 ID·메서드·상태·결과·소요 시간만 사용한다.
318
+ - 샘플의 첫 Application 시작 훅에서 `Sample` 핸들러를 설정한다. 같은 이름의 샘플 핸들러를 중복 추가하지 않으며, 기존 사용자 핸들러는 유지한다.
319
+ - 샘플의 Application·RequestRunner·스트리밍 관찰 훅은 DEBUG로 기록하며 원시 URI·예외 메시지·중복 traceback을 출력하지 않는다.
320
+ - [로깅 테스트](../../../tests/test_logging.py) 21개를 추가했다. 기존 훅·샘플·어댑터·writer 테스트도 새로운 레벨과 안전한 메시지 정책을 검증하도록 갱신했다.
321
+ - Windows의 Python 3.12 환경에서 현재 작업 트리의 패키지를 `importlib.util.spec_from_file_location()`으로 직접 로드한 뒤 `unittest` 전체 발견 테스트를 실행했다. 설치된 별도 패키지가 아니라 이번 소스를 대상으로 검증했다.
322
+ - 전체 테스트 243개 중 **238개 통과, 5개 건너뜀**. 건너뛴 항목은 POSIX가 필요한 Gunicorn 실행·전송 적합성 테스트이며, Gunicorn 실제 실행 검증은 Linux/WSL 또는 CI에서 추가로 필요하다.
323
+ - Python·Uvicorn의 실제 HTTP 전송, 포트 0의 준비 완료 로그, Uvicorn의 실제 바인딩 실패·정리, 스트리밍·SSE·WebSocket 관련 회귀 테스트를 포함했다. Python 바인딩 실패 로그는 Windows의 주소 재사용 차이에 의존하지 않는 테스트 더블로 별도 검증했다.
324
+ - 소스·샘플·테스트의 Python 3.9 구문 분석, 주요 변경 파일의 Pylance 타입 오류 확인, `git diff --check`와 변경 문서의 상대 링크·들여쓰기 검증을 수행했다. Python 3.9 구문 검사는 해당 버전의 실제 런타임 테스트를 대신하지 않는다.
325
+
326
+ 상세 변경 내용은 [구현 기록](./IMPLEMENTS.md), 사용자 관점의 요약은 [변경 로그](./README.md)에 기록한다.