Skip to content

Commit 9bfa6a6

Browse files
Merge pull request #164 from askui/feat/filter-logs
feat(chat): filter logs using `ASKUI__CHAT_API__TELEMETRY__LOG__FILTERS`
2 parents 0f3f51a + 579e0d1 commit 9bfa6a6

File tree

5 files changed

+75
-3
lines changed

5 files changed

+75
-3
lines changed

src/askui/chat/api/settings.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from askui.chat.api.mcp_configs.models import McpConfig
88
from askui.chat.api.telemetry.integrations.fastapi.settings import TelemetrySettings
9+
from askui.chat.api.telemetry.logs.settings import LogFilter, LogSettings
910
from askui.utils.datetime_utils import now
1011

1112

@@ -66,5 +67,13 @@ class Settings(BaseSettings):
6667
),
6768
)
6869
telemetry: TelemetrySettings = Field(
69-
default_factory=TelemetrySettings,
70+
default_factory=lambda: TelemetrySettings(
71+
log=LogSettings(
72+
filters=[
73+
LogFilter(type="equals", key="path", value="/v1/health"),
74+
LogFilter(type="equals", key="path", value="/v1/metrics"),
75+
LogFilter(type="equals", key="method", value="OPTIONS"),
76+
],
77+
),
78+
),
7079
)

src/askui/chat/api/telemetry/integrations/fastapi/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ def instrument(
3232
app.add_middleware(AccessLoggingMiddleware)
3333
app.add_middleware(CorrelationIdMiddleware)
3434
app.add_middleware(RawContextMiddleware)
35-
Instrumentator().instrument(app).expose(app)
35+
Instrumentator().instrument(app).expose(app, endpoint="/v1/metrics")

src/askui/chat/api/telemetry/logs/settings.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import enum
2+
from typing import Literal
23

34
from pydantic import BaseModel
45

@@ -16,6 +17,16 @@ class LogFormat(str, enum.Enum):
1617
LOGFMT = "logfmt"
1718

1819

20+
class EqualsLogFilter(BaseModel):
21+
type: Literal["equals"]
22+
key: str
23+
value: str
24+
25+
26+
LogFilter = EqualsLogFilter
27+
28+
1929
class LogSettings(BaseModel):
2030
format: LogFormat = LogFormat.LOGFMT
2131
level: LogLevel = LogLevel.INFO
32+
filters: list[LogFilter] | None = None

src/askui/chat/api/telemetry/logs/structlog.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from .settings import LogFormat, LogLevel, LogSettings
66
from .structlog_processors import (
7+
create_filter_processor,
78
drop_color_message_key_processor,
89
flatten_dict_processor,
910
)
@@ -47,6 +48,7 @@ def get_shared_processors(settings: LogSettings) -> list[structlog.types.Process
4748
"""Returns a list of processors, i.e., a processor chain, that can be shared between
4849
structlog and stdlib loggers so that their content is consistent."""
4950
format_dependent_processors = get_format_dependent_processors(settings.format)
51+
filter_processor = create_filter_processor(settings.filters)
5052
return [
5153
structlog.contextvars.merge_contextvars,
5254
structlog.stdlib.add_logger_name,
@@ -58,6 +60,7 @@ def get_shared_processors(settings: LogSettings) -> list[structlog.types.Process
5860
structlog.processors.StackInfoRenderer(),
5961
*format_dependent_processors,
6062
structlog.processors.EventRenamer(EVENT_KEY),
63+
filter_processor,
6164
]
6265

6366

src/askui/chat/api/telemetry/logs/structlog_processors.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import logging
22

3-
from structlog.types import EventDict
3+
from structlog import DropEvent
4+
from structlog.types import EventDict, Processor
5+
6+
from askui.chat.api.telemetry.logs.settings import LogFilter
47

58
from .utils import flatten_dict
69

@@ -27,3 +30,49 @@ def drop_color_message_key_processor(
2730
"""
2831
event_dict.pop("color_message", None)
2932
return event_dict
33+
34+
35+
def null_processor(
36+
logger: logging.Logger, # noqa: ARG001
37+
method_name: str, # noqa: ARG001
38+
event_dict: EventDict,
39+
) -> EventDict:
40+
"""
41+
A processor that does nothing.
42+
"""
43+
return event_dict
44+
45+
46+
def create_filter_processor(filters: list[LogFilter] | None) -> Processor:
47+
"""
48+
Creates a structlog processor that filters out log lines based on field matches.
49+
50+
Args:
51+
filters (dict[str, Any] | None): Dictionary of field names to values to filter out.
52+
If a log line has a field with a matching value, it will be filtered out.
53+
54+
Returns:
55+
A structlog processor function that filters log lines.
56+
"""
57+
if not filters:
58+
return null_processor
59+
60+
def filter_processor(
61+
logger: logging.Logger, # noqa: ARG001
62+
method_name: str, # noqa: ARG001
63+
event_dict: EventDict,
64+
) -> EventDict:
65+
"""
66+
Filters out log lines where any field matches the filter values.
67+
Returns None to drop the log line, or the event_dict to keep it.
68+
"""
69+
for filter_ in filters:
70+
if filter_.type == "equals":
71+
if (
72+
filter_.key in event_dict
73+
and event_dict[filter_.key] == filter_.value
74+
):
75+
raise DropEvent
76+
return event_dict
77+
78+
return filter_processor

0 commit comments

Comments
 (0)