77import logging
88import os
99import re
10+ import sys
1011import warnings
1112from pathlib import Path
13+ from typing import Literal , TypeAlias
1214
1315import structlog
1416
2426 structlog .stdlib .PositionalArgumentsFormatter (),
2527 structlog .processors .MaybeTimeStamper (),
2628]
29+ LogFormat : TypeAlias = Literal ["json" , "logfmt" , "text" ]
2730
2831_active_config : LoggingConfig | None = None
2932
@@ -64,8 +67,10 @@ class LoggingConfig: # pragma: nocover
6467 """
6568
6669 level : int = logging .INFO
70+ stream_json : bool = False
6771 file : Path | None = None
6872 file_level : int | None = None
73+ file_format : LogFormat = "json"
6974
7075 def __init__ (self ):
7176 # initialize configuration from environment variables
@@ -85,6 +90,12 @@ def effective_level(self) -> int:
8590 else :
8691 return self .level
8792
93+ def term_json (self , flag : bool = True ):
94+ """
95+ Configure logging to stream JSON lines to the stderr (useful for web services).
96+ """
97+ self .stream_json = flag
98+
8899 def set_verbose (self , verbose : bool | int = True ):
89100 """
90101 Enable verbose logging.
@@ -108,12 +119,15 @@ def set_verbose(self, verbose: bool | int = True):
108119 else :
109120 self .level = logging .INFO
110121
111- def log_file (self , path : os .PathLike [str ], level : int | None = None ):
122+ def log_file (
123+ self , path : os .PathLike [str ], level : int | None = None , format : LogFormat = "json"
124+ ):
112125 """
113126 Configure a log file.
114127 """
115128 self .file = Path (path )
116129 self .file_level = level
130+ self .file_format = format
117131
118132 def apply (self ):
119133 """
@@ -123,8 +137,15 @@ def apply(self):
123137
124138 setup_console ()
125139 root = logging .getLogger ()
126- term = ConsoleHandler ()
127- term .setLevel (self .level )
140+
141+ if self .stream_json :
142+ term = logging .StreamHandler (sys .stderr )
143+ term .setLevel (self .level )
144+ proc_fmt = structlog .processors .JSONRenderer ()
145+ else :
146+ term = ConsoleHandler ()
147+ term .setLevel (self .level )
148+ proc_fmt = structlog .dev .ConsoleRenderer (colors = term .supports_color )
128149
129150 eff_lvl = self .effective_level
130151 structlog .configure (
@@ -136,7 +157,7 @@ def apply(self):
136157 processors = [
137158 remove_internal ,
138159 format_timestamp ,
139- structlog . dev . ConsoleRenderer ( colors = term . supports_color ) ,
160+ proc_fmt ,
140161 ],
141162 foreign_pre_chain = CORE_PROCESSORS ,
142163 )
@@ -147,11 +168,19 @@ def apply(self):
147168 if self .file :
148169 file_level = self .file_level if self .file_level is not None else self .level
149170 file = logging .FileHandler (self .file , mode = "w" )
171+
172+ if self .file_format == "json" :
173+ proc_fmt = structlog .processors .JSONRenderer ()
174+ elif self .file_format == "logfmt" :
175+ proc_fmt = structlog .processors .LogfmtRenderer (key_order = ["event" , "timestamp" ])
176+ else :
177+ proc_fmt = structlog .processors .KeyValueRenderer (key_order = ["event" , "timestamp" ])
178+
150179 ffmt = structlog .stdlib .ProcessorFormatter (
151180 processors = [
152181 remove_internal ,
153182 structlog .processors .ExceptionPrettyPrinter (),
154- structlog . processors . JSONRenderer () ,
183+ proc_fmt ,
155184 ],
156185 foreign_pre_chain = CORE_PROCESSORS ,
157186 )
0 commit comments