1010
1111import logging
1212import os
13- import sys
14- from pathlib import Path
15- from typing import Any , Dict , List , Optional , Sequence , Union
13+ from typing import Any , List , Optional , Sequence , Union
1614
1715from nfo .logger import Logger
1816from nfo .sinks import CSVSink , MarkdownSink , SQLiteSink , Sink
@@ -117,6 +115,164 @@ def emit(self, record: logging.LogRecord) -> None:
117115 pass
118116
119117
118+ def _read_env_config (
119+ env_prefix : str ,
120+ level : str ,
121+ environment : Optional [str ],
122+ llm_model : Optional [str ],
123+ auto_extract_meta : bool ,
124+ meta_policy : Optional [Any ],
125+ ) -> tuple [str , Optional [str ], Optional [str ], bool , Optional [Any ], Optional [str ]]:
126+ """Read environment variable overrides for configuration.
127+
128+ Returns:
129+ Tuple of (level, environment, llm_model, auto_extract_meta, meta_policy, env_sinks)
130+ """
131+ env_level = os .environ .get (f"{ env_prefix } LEVEL" )
132+ if env_level :
133+ level = env_level .upper ()
134+
135+ env_env = os .environ .get (f"{ env_prefix } ENV" )
136+ if env_env :
137+ environment = env_env
138+
139+ env_llm = os .environ .get (f"{ env_prefix } LLM_MODEL" )
140+ if env_llm :
141+ llm_model = env_llm
142+
143+ env_meta_extract = os .environ .get (f"{ env_prefix } META_EXTRACT" , "" ).lower ()
144+ if env_meta_extract in ("true" , "1" , "yes" ):
145+ auto_extract_meta = True
146+
147+ env_meta_threshold = os .environ .get (f"{ env_prefix } META_THRESHOLD" )
148+ if env_meta_threshold :
149+ from nfo .meta import ThresholdPolicy
150+ threshold = int (env_meta_threshold )
151+ if meta_policy is None :
152+ meta_policy = ThresholdPolicy (max_arg_bytes = threshold , max_return_bytes = threshold )
153+ else :
154+ meta_policy .max_arg_bytes = threshold
155+ meta_policy .max_return_bytes = threshold
156+
157+ env_sinks = os .environ .get (f"{ env_prefix } SINKS" )
158+
159+ return level , environment , llm_model , auto_extract_meta , meta_policy , env_sinks
160+
161+
162+ def _resolve_sinks (
163+ sinks : Optional [Sequence [Union [str , Sink ]]],
164+ env_sinks : Optional [str ],
165+ ) -> List [Sink ]:
166+ """Build sink list from explicit specs or environment variable."""
167+ resolved : List [Sink ] = []
168+
169+ if sinks is not None :
170+ for s in sinks :
171+ if isinstance (s , str ):
172+ resolved .append (_parse_sink_spec (s ))
173+ else :
174+ resolved .append (s )
175+ elif env_sinks :
176+ for spec in env_sinks .split ("," ):
177+ spec = spec .strip ()
178+ if spec :
179+ resolved .append (_parse_sink_spec (spec ))
180+
181+ return resolved
182+
183+
184+ def _wrap_sinks_with_llm (
185+ sinks : List [Sink ],
186+ llm_model : Optional [str ],
187+ detect_injection : bool ,
188+ ) -> List [Sink ]:
189+ """Wrap sinks with LLM analysis if model specified or injection detection enabled."""
190+ if not sinks :
191+ return sinks
192+
193+ if llm_model :
194+ from nfo .llm import LLMSink
195+ return [
196+ LLMSink (
197+ model = llm_model ,
198+ delegate = sink ,
199+ async_mode = True ,
200+ detect_injection = detect_injection ,
201+ )
202+ for sink in sinks
203+ ]
204+ elif detect_injection :
205+ from nfo .llm import LLMSink
206+ return [
207+ LLMSink (
208+ model = "" ,
209+ delegate = sink ,
210+ async_mode = False ,
211+ detect_injection = True ,
212+ analyze_levels = [],
213+ )
214+ for sink in sinks
215+ ]
216+
217+ return sinks
218+
219+
220+ def _wrap_sinks_with_env (
221+ sinks : List [Sink ],
222+ environment : Optional [str ],
223+ version : Optional [str ],
224+ ) -> List [Sink ]:
225+ """Wrap sinks with environment tagging if environment or version specified."""
226+ if not sinks or not (environment or version ):
227+ return sinks
228+
229+ from nfo .env import EnvTagger
230+ return [
231+ EnvTagger (
232+ sink ,
233+ environment = environment ,
234+ version = version ,
235+ auto_detect = True ,
236+ )
237+ for sink in sinks
238+ ]
239+
240+
241+ def _setup_stdlib_bridge (
242+ logger : Logger ,
243+ level : str ,
244+ resolved_sinks : List [Sink ],
245+ bridge_stdlib : bool ,
246+ modules : Optional [Sequence [str ]],
247+ ) -> None :
248+ """Bridge stdlib loggers to nfo sinks (if sinks are configured)."""
249+ if not resolved_sinks or not (bridge_stdlib or modules ):
250+ return
251+
252+ bridge = _StdlibBridge (logger )
253+ bridge .setLevel (getattr (logging , level .upper (), logging .DEBUG ))
254+
255+ if bridge_stdlib :
256+ root = logging .getLogger ()
257+ if bridge not in root .handlers :
258+ root .addHandler (bridge )
259+
260+ if modules :
261+ # Sort so parents come before children (shorter names first).
262+ # Only attach bridge to a logger if no ancestor in the list
263+ # already has it — stdlib propagation handles children.
264+ bridged : set [str ] = set ()
265+ for mod in sorted (modules , key = len ):
266+ has_ancestor = any (
267+ mod .startswith (anc + "." ) for anc in bridged
268+ )
269+ mod_logger = logging .getLogger (mod )
270+ if not has_ancestor :
271+ if bridge not in mod_logger .handlers :
272+ mod_logger .addHandler (bridge )
273+ bridged .add (mod )
274+
275+
120276def configure (
121277 * ,
122278 name : str = "nfo" ,
@@ -204,90 +360,23 @@ def configure(
204360 if _configured and not force and _last_logger is not None :
205361 return _last_logger
206362
207- # Environment overrides
208- env_level = os .environ .get (f"{ env_prefix } LEVEL" )
209- if env_level :
210- level = env_level .upper ()
211-
212- env_sinks = os .environ .get (f"{ env_prefix } SINKS" )
213-
214- # Environment overrides for new features
215- env_env = os .environ .get (f"{ env_prefix } ENV" )
216- if env_env :
217- environment = env_env
218- env_llm = os .environ .get (f"{ env_prefix } LLM_MODEL" )
219- if env_llm :
220- llm_model = env_llm
221-
222- # Meta extraction env overrides
223- env_meta_extract = os .environ .get (f"{ env_prefix } META_EXTRACT" , "" ).lower ()
224- if env_meta_extract in ("true" , "1" , "yes" ):
225- auto_extract_meta = True
226- env_meta_threshold = os .environ .get (f"{ env_prefix } META_THRESHOLD" )
227- if env_meta_threshold :
228- from nfo .meta import ThresholdPolicy
229- threshold = int (env_meta_threshold )
230- if meta_policy is None :
231- meta_policy = ThresholdPolicy (max_arg_bytes = threshold , max_return_bytes = threshold )
232- else :
233- meta_policy .max_arg_bytes = threshold
234- meta_policy .max_return_bytes = threshold
363+ # Read environment overrides
364+ level , environment , llm_model , auto_extract_meta , meta_policy , env_sinks = _read_env_config (
365+ env_prefix , level , environment , llm_model , auto_extract_meta , meta_policy
366+ )
235367
236368 # Store global meta policy and auto_extract flag
237369 _global_meta_policy = meta_policy
238370 _global_auto_extract_meta = auto_extract_meta
239371
240372 # Build sink list
241- resolved_sinks : List [Sink ] = []
242- if sinks is not None :
243- for s in sinks :
244- if isinstance (s , str ):
245- resolved_sinks .append (_parse_sink_spec (s ))
246- else :
247- resolved_sinks .append (s )
248- elif env_sinks :
249- for spec in env_sinks .split ("," ):
250- spec = spec .strip ()
251- if spec :
252- resolved_sinks .append (_parse_sink_spec (spec ))
373+ resolved_sinks = _resolve_sinks (sinks , env_sinks )
253374
254375 # Wrap sinks with LLM analysis if model specified
255- if llm_model and resolved_sinks :
256- from nfo .llm import LLMSink
257- resolved_sinks = [
258- LLMSink (
259- model = llm_model ,
260- delegate = sink ,
261- async_mode = True ,
262- detect_injection = detect_injection ,
263- )
264- for sink in resolved_sinks
265- ]
266- elif detect_injection and resolved_sinks :
267- from nfo .llm import LLMSink
268- resolved_sinks = [
269- LLMSink (
270- model = "" ,
271- delegate = sink ,
272- async_mode = False ,
273- detect_injection = True ,
274- analyze_levels = [],
275- )
276- for sink in resolved_sinks
277- ]
376+ resolved_sinks = _wrap_sinks_with_llm (resolved_sinks , llm_model , detect_injection )
278377
279378 # Wrap sinks with env tagging if environment or version specified
280- if (environment or version ) and resolved_sinks :
281- from nfo .env import EnvTagger
282- resolved_sinks = [
283- EnvTagger (
284- sink ,
285- environment = environment ,
286- version = version ,
287- auto_detect = True ,
288- )
289- for sink in resolved_sinks
290- ]
379+ resolved_sinks = _wrap_sinks_with_env (resolved_sinks , environment , version )
291380
292381 # Create logger
293382 logger = Logger (
@@ -298,30 +387,8 @@ def configure(
298387 )
299388 set_default_logger (logger )
300389
301- # Bridge stdlib loggers to nfo sinks (if sinks are configured)
302- if resolved_sinks and (bridge_stdlib or modules ):
303- bridge = _StdlibBridge (logger )
304- bridge .setLevel (getattr (logging , level .upper (), logging .DEBUG ))
305-
306- if bridge_stdlib :
307- root = logging .getLogger ()
308- if bridge not in root .handlers :
309- root .addHandler (bridge )
310-
311- if modules :
312- # Sort so parents come before children (shorter names first).
313- # Only attach bridge to a logger if no ancestor in the list
314- # already has it — stdlib propagation handles children.
315- bridged : set [str ] = set ()
316- for mod in sorted (modules , key = len ):
317- has_ancestor = any (
318- mod .startswith (anc + "." ) for anc in bridged
319- )
320- mod_logger = logging .getLogger (mod )
321- if not has_ancestor :
322- if bridge not in mod_logger .handlers :
323- mod_logger .addHandler (bridge )
324- bridged .add (mod )
390+ # Bridge stdlib loggers to nfo sinks
391+ _setup_stdlib_bridge (logger , level , resolved_sinks , bridge_stdlib , modules )
325392
326393 _configured = True
327394 _last_logger = logger
0 commit comments