Skip to content

Commit 8022c6f

Browse files
CalebCourierzsimjeenefertitirogersNefertiti  Rogers
authored
Capture logs (#485)
* start custom logger * initial logging support * test coverage * Validator refactor (#478) * refactor validators to multiple files * complete validator split * fix one-line validator docstring (#484) * docstring fix * lint --------- Co-authored-by: Nefertiti Rogers <[email protected]> * PR comments * lint fixes --------- Co-authored-by: zsimjee <[email protected]> Co-authored-by: Nefertiti Rogers <[email protected]> Co-authored-by: Nefertiti Rogers <[email protected]>
1 parent a1349e3 commit 8022c6f

32 files changed

+368
-113
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dist/*
2020
.cache
2121
scratch/
2222
.coverage*
23+
coverage.xml
2324
test.db
2425
test.index
2526
htmlcov

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ test-cov:
4747
view-test-cov:
4848
poetry run pytest tests/ --cov=./guardrails/ --cov-report html && open htmlcov/index.html
4949

50+
view-test-cov-file:
51+
poetry run pytest tests/unit_tests/test_logger.py --cov=./guardrails/ --cov-report html && open htmlcov/index.html
52+
5053
docs-serve:
5154
poetry run mkdocs serve -a $(MKDOCS_SERVE_ADDR)
5255

guardrails/classes/history/call.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,13 @@ def reask_instructions(self) -> Stack[str]:
134134
# # To allow chaining without getting AttributeErrors
135135
# return Outputs()
136136

137-
# TODO
138-
# @property
139-
# def logs(self) -> Stack[str]:
140-
# """Returns all logs from all iterations as a stack"""
137+
@property
138+
def logs(self) -> Stack[str]:
139+
"""Returns all logs from all iterations as a stack."""
140+
all_logs = []
141+
for i in self.iterations:
142+
all_logs.extend(i.logs)
143+
return Stack(*all_logs)
141144

142145
@property
143146
def tokens_consumed(self) -> Optional[int]:

guardrails/classes/history/iteration.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
from rich.pretty import pretty_repr
77
from rich.table import Table
88

9+
from guardrails.classes.generic.stack import Stack
910
from guardrails.classes.history.inputs import Inputs
1011
from guardrails.classes.history.outputs import Outputs
12+
from guardrails.logger import get_scope_handler
1113
from guardrails.prompt.prompt import Prompt
1214
from guardrails.utils.logs_utils import ValidatorLogs
13-
14-
# from guardrails.classes.stack import Stack
1515
from guardrails.utils.pydantic_utils import ArbitraryModel
1616
from guardrails.utils.reask_utils import ReAsk
1717

@@ -27,10 +27,13 @@ class Iteration(ArbitraryModel):
2727
description="The outputs from the iteration/step.", default_factory=Outputs
2828
)
2929

30-
# TODO
31-
# @property
32-
# def logs() -> Stack[str]:
33-
# """Returns the logs from this iteration as a stack"""
30+
@property
31+
def logs(self) -> Stack[str]:
32+
"""Returns the logs from this iteration as a stack."""
33+
scope = str(id(self))
34+
scope_handler = get_scope_handler()
35+
scoped_logs = scope_handler.get_logs(scope)
36+
return Stack(*[log.getMessage() for log in scoped_logs])
3437

3538
@property
3639
def tokens_consumed(self) -> Optional[int]:

guardrails/classes/history/outputs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class Outputs(ArbitraryModel):
1919
"as it was passed into validation.",
2020
default=None,
2121
)
22-
validation_output: Optional[Union[str, Dict, ReAsk]] = Field(
22+
validation_output: Optional[Union[str, ReAsk, Dict]] = Field(
2323
description="The output from the validation process.", default=None
2424
)
2525
validated_output: Optional[Union[str, Dict]] = Field(
@@ -48,6 +48,7 @@ def _all_empty(self) -> bool:
4848
return (
4949
self.llm_response_info is None
5050
and self.parsed_output is None
51+
and self.validation_output is None
5152
and self.validated_output is None
5253
and len(self.reasks) == 0
5354
and len(self.validator_logs) == 0
@@ -79,6 +80,7 @@ def status(self) -> str:
7980
all_reasks_have_fixes = all(
8081
list(fail.fix_value is not None for fail in all_fail_results)
8182
)
83+
8284
if self._all_empty() is True:
8385
return not_run_status
8486
elif self.error:

guardrails/datatypes.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import datetime
2-
import logging
32
import warnings
43
from dataclasses import dataclass
54
from types import SimpleNamespace
@@ -16,9 +15,6 @@
1615
from guardrails.validator_base import Validator, ValidatorSpec
1716
from guardrails.validatorsattr import ValidatorsAttr
1817

19-
logger = logging.getLogger(__name__)
20-
21-
2218
# TODO - deprecate these altogether
2319
deprecated_string_types = {"sql", "email", "url", "pythoncode"}
2420

guardrails/guard.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import contextvars
3-
import logging
43
from typing import (
54
Any,
65
Awaitable,
@@ -24,15 +23,14 @@
2423
from guardrails.classes.history import Call
2524
from guardrails.classes.history.call_inputs import CallInputs
2625
from guardrails.llm_providers import get_async_llm_ask, get_llm_ask
26+
from guardrails.logger import logger, set_scope
2727
from guardrails.prompt import Instructions, Prompt
2828
from guardrails.rail import Rail
2929
from guardrails.run import AsyncRunner, Runner
3030
from guardrails.schema import Schema
3131
from guardrails.validators import Validator
3232

33-
logger = logging.getLogger(__name__)
34-
actions_logger = logging.getLogger(f"{__name__}.actions")
35-
add_destinations(actions_logger.debug)
33+
add_destinations(logger.debug)
3634

3735

3836
class Guard(Generic[OT]):
@@ -61,6 +59,7 @@ def __init__(
6159
"""Initialize the Guard."""
6260
self.rail = rail
6361
self.num_reasks = num_reasks
62+
# TODO: Support a sink for history so that it is not solely held in memory
6463
self.history: Stack[Call] = Stack()
6564
self.base_model = base_model
6665

@@ -315,6 +314,7 @@ def __call__(
315314
kwargs=kwargs,
316315
)
317316
call_log = Call(inputs=call_inputs)
317+
set_scope(str(id(call_log)))
318318
self.history.push(call_log)
319319

320320
# If the LLM API is async, return a coroutine
@@ -553,6 +553,7 @@ def parse(
553553
kwargs=kwargs,
554554
)
555555
call_log = Call(inputs=call_inputs)
556+
set_scope(str(id(call_log)))
556557
self.history.push(call_log)
557558

558559
# If the LLM API is async, return a coroutine

guardrails/logger.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import logging
2+
import logging.config
3+
from logging import Handler, LogRecord
4+
from typing import Dict, List, Optional
5+
6+
# from src.modules.otel_logger import handler as otel_handler
7+
8+
name = "guardrails-ai"
9+
base_scope = "base"
10+
all_scopes = "all"
11+
12+
13+
class ScopeHandler(Handler):
14+
scope: str
15+
scoped_logs: Dict[str, List[LogRecord]]
16+
17+
def __init__(self, level=logging.NOTSET, scope=base_scope):
18+
super().__init__(level)
19+
self.scope = scope
20+
self.scoped_logs = {}
21+
22+
def emit(self, record: LogRecord) -> None:
23+
logs = self.scoped_logs.get(self.scope, [])
24+
logs.append(record)
25+
self.scoped_logs[self.scope] = logs
26+
27+
def set_scope(self, scope: str = base_scope):
28+
self.scope = scope
29+
30+
def get_all_logs(self) -> List[LogRecord]:
31+
all_logs = []
32+
for key in self.scoped_logs:
33+
logs = self.scoped_logs.get(key, [])
34+
all_logs.extend(logs)
35+
return all_logs
36+
37+
def get_logs(self, scope: Optional[str] = None) -> List[LogRecord]:
38+
scope = scope or self.scope
39+
if scope == all_scopes:
40+
return self.get_all_logs()
41+
logs = self.scoped_logs.get(scope, [])
42+
return logs
43+
44+
45+
class LoggerConfig:
46+
def __init__(self, config={}, level=logging.NOTSET, scope=base_scope):
47+
self.config = config
48+
self.level = level
49+
self.scope = scope
50+
51+
52+
_logger = logging.getLogger(name)
53+
handler = ScopeHandler()
54+
scoped_logs: Dict[str, List[LogRecord]] = {}
55+
logger_config = LoggerConfig()
56+
57+
58+
def get_logger():
59+
_setup_handler(logger_config.level, logger_config.scope)
60+
61+
if logger_config.config:
62+
logging.config.dictConfig(logger_config.config)
63+
64+
_logger.setLevel(logger_config.level)
65+
66+
get_scope_handler()
67+
68+
return _logger
69+
70+
71+
def set_config(config=None):
72+
if config is not None:
73+
logger_config.config = config
74+
logging.config.dictConfig(logger_config.config)
75+
76+
77+
def set_level(level=None):
78+
if level is not None:
79+
logger_config.level = level
80+
_logger.setLevel(level)
81+
82+
83+
def set_scope(scope: str = base_scope):
84+
logger_config.scope = scope
85+
scope_handler = get_scope_handler()
86+
scope_handler.set_scope(scope)
87+
88+
89+
def _setup_handler(log_level=logging.NOTSET, scope=base_scope) -> ScopeHandler:
90+
global handler
91+
if not handler:
92+
handler = ScopeHandler(log_level, scope)
93+
return handler
94+
95+
96+
def get_scope_handler() -> ScopeHandler:
97+
global _logger
98+
try:
99+
scope_handler: ScopeHandler = [
100+
h for h in _logger.handlers if isinstance(h, ScopeHandler)
101+
][0]
102+
return scope_handler
103+
except IndexError:
104+
hdlr = _setup_handler()
105+
_logger.addHandler(hdlr)
106+
return hdlr
107+
108+
109+
logger = get_logger()

guardrails/logging_utils.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
import logging
2-
import logging.config
1+
from guardrails.logger import set_config, set_level
32

43

4+
# TODO: Support a sink for logs so that they are not solely held in memory
55
def configure_logging(logging_config=None, log_level=None):
6-
if logging_config is not None:
7-
logging.config.dictConfig(logging_config)
8-
9-
if log_level is not None:
10-
logger = logging.getLogger(__name__)
11-
logger.setLevel(log_level)
6+
set_config(logging_config)
7+
set_level(log_level)

guardrails/run.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import copy
2-
import logging
32
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
43

54
from eliot import add_destinations, start_action
@@ -8,14 +7,13 @@
87
from guardrails.classes.history import Call, Inputs, Iteration, Outputs
98
from guardrails.datatypes import verify_metadata_requirements
109
from guardrails.llm_providers import AsyncPromptCallableBase, PromptCallableBase
10+
from guardrails.logger import logger, set_scope
1111
from guardrails.prompt import Instructions, Prompt
1212
from guardrails.schema import Schema
1313
from guardrails.utils.llm_response import LLMResponse
1414
from guardrails.utils.reask_utils import NonParseableReAsk, ReAsk, reasks_to_dict
1515

16-
logger = logging.getLogger(__name__)
17-
actions_logger = logging.getLogger(f"{__name__}.actions")
18-
add_destinations(actions_logger.debug)
16+
add_destinations(logger.debug)
1917

2018

2119
class Runner:
@@ -200,6 +198,7 @@ def step(
200198
)
201199
outputs = Outputs()
202200
iteration = Iteration(inputs=inputs, outputs=outputs)
201+
set_scope(str(id(iteration)))
203202
call_log.iterations.push(iteration)
204203

205204
try:

0 commit comments

Comments
 (0)