Skip to content

Commit 9ec7682

Browse files
refactor(build): code analysis engine
changes: - file: configure.py area: config added: [_setup_stdlib_bridge, _wrap_sinks_with_env, _resolve_sinks, _wrap_sinks_with_llm, _read_env_config] modified: [configure] - file: decorators.py area: core removed: [_arg_types, _get_default_logger, decision_log, catch, decorator, log_call, +7 more] - file: _catch.py area: core added: [decorator, async_wrapper, catch, wrapper] - file: _core.py area: core added: [_module_of, _get_default_logger, _should_sample, set_default_logger, _arg_types] - file: _decision.py area: core added: [decision_log, async_wrapper, decorator, wrapper, _build_decision_extra] - file: _extract.py area: core added: [_maybe_extract] - file: _log_call.py area: core added: [async_wrapper, decorator, wrapper, log_call] - file: log_flow.py area: core added: [_extract_field, _first_present, _extract_trace_id] modified: [normalize_entry, LogFlowParser] dependencies: flow: "_catch→_core, _log_call→_core, _decision→_core" - _catch.py -> _core.py - _catch.py -> _extract.py - _decision.py -> _core.py - _log_call.py -> _core.py - _log_call.py -> _extract.py stats: lines: "+1177/-935 (net +242)" files: 16 complexity: "Large structural change (normalized)"
1 parent 90c4c30 commit 9ec7682

File tree

20 files changed

+1210
-938
lines changed

20 files changed

+1210
-938
lines changed

.github/workflows/ci.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master, develop]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e . || pip install -r requirements.txt || true
28+
29+
- name: Run tests
30+
run: |
31+
python -m pytest tests/ -v --tb=short
32+
33+
- name: Lint
34+
run: |
35+
pip install flake8 || true
36+
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics || true

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,33 @@
1+
## [0.2.18] - 2026-03-02
2+
3+
### Summary
4+
5+
refactor(build): code analysis engine
6+
7+
### Docs
8+
9+
- docs: update README
10+
- docs: update context.md
11+
12+
### Ci
13+
14+
- config: update ci.yml
15+
16+
### Other
17+
18+
- update nfo/configure.py
19+
- update nfo/decorators.py
20+
- update nfo/decorators/__init__.py
21+
- update nfo/decorators/_catch.py
22+
- update nfo/decorators/_core.py
23+
- update nfo/decorators/_decision.py
24+
- update nfo/decorators/_extract.py
25+
- update nfo/decorators/_log_call.py
26+
- update nfo/log_flow.py
27+
- update project/analysis.toon
28+
- ... and 3 more
29+
30+
131
## [0.2.16] - 2026-02-18
232

333
### Summary

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.2.16
1+
0.2.18

nfo/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def __getattr__(name: str):
125125
return FastAPIMiddleware
126126
raise AttributeError(f"module 'nfo' has no attribute {name!r}")
127127

128-
__version__ = "0.2.17"
128+
__version__ = "0.2.18"
129129

130130
__all__ = [
131131
"log_call",

nfo/configure.py

Lines changed: 168 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010

1111
import logging
1212
import 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

1715
from nfo.logger import Logger
1816
from 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+
120276
def 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

Comments
 (0)