Skip to content

Commit 26a2272

Browse files
author
ci bot
committed
Merge branch 'OBS-1994' into 'enterprise'
feat(events): add case-insensitive message search to events page See merge request dkinternal/observability/dataops-observability!63
2 parents 9d0cdc8 + 4358753 commit 26a2272

File tree

16 files changed

+153
-6
lines changed

16 files changed

+153
-6
lines changed

common/entity_services/event_service.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from collections import defaultdict
44

5-
from peewee import JOIN
5+
from peewee import JOIN, fn
66

77
from common.entities import (
88
Component,
@@ -15,6 +15,7 @@
1515
RunTask,
1616
Task,
1717
)
18+
from common.entities.event import ApiEventType
1819

1920
from .helpers import ListRules, Page, ProjectEventFilters
2021

@@ -57,6 +58,20 @@ def get_events_with_rules(*, rules: ListRules, filters: ProjectEventFilters) ->
5758
filter_list.append(EventEntity.timestamp >= filters.date_range_start)
5859
if filters.date_range_end:
5960
filter_list.append(EventEntity.timestamp < filters.date_range_end)
61+
if filters.search:
62+
lowered_payload = fn.LOWER(EventEntity.v2_payload.cast("CHAR"))
63+
search_term = f"%{filters.search.lower()}%"
64+
json_search = lambda path: fn.JSON_SEARCH(lowered_payload, "one", search_term, None, path).is_null(False)
65+
filter_list.append(
66+
(
67+
(EventEntity.type == ApiEventType.MESSAGE_LOG)
68+
& (json_search("$.log_entries[*].message") | json_search("$.log_entries[*].message_details"))
69+
)
70+
| (
71+
(EventEntity.type == ApiEventType.METRIC_LOG)
72+
& (json_search("$.metric_entries[*].key") | json_search("$.metric_entries[*].value"))
73+
)
74+
)
6075

6176
query = query.where(*filter_list)
6277
page = Page[EventEntity].get_paginated_results(query, EventEntity.timestamp, rules)

common/entity_services/helpers/filter_rules.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
KEY_QUERY_NAME: str = "key"
4141

4242
# List events endpoints
43+
SEARCH_QUERY_NAME: str = "search"
4344
EVENT_TYPE_QUERY_NAME: str = "event_type"
4445
EVENT_ID_QUERY_NAME: str = "event_id"
4546
JOURNEY_ID_QUERY_NAME: str = "journey_id"
@@ -111,6 +112,7 @@ def _normalize_tools(params: MultiDict, field_name: str) -> list[str]:
111112
PROJECT_ID_QUERY_NAME: ParamConfig("project_ids", MultiDict.getlist),
112113
RUN_ID_QUERY_NAME: ParamConfig("run_ids", MultiDict.getlist),
113114
RUN_KEY_QUERY_NAME: ParamConfig("run_keys", MultiDict.getlist),
115+
SEARCH_QUERY_NAME: ParamConfig("search", MultiDict.get),
114116
START_RANGE_QUERY_NAME: ParamConfig("start_range", _date_or_none),
115117
START_RANGE_BEGIN_QUERY_NAME: ParamConfig("start_range_begin", _date_or_none),
116118
START_RANGE_END_QUERY_NAME: ParamConfig("start_range_end", _date_or_none),
@@ -149,6 +151,7 @@ class Filters:
149151
project_ids: list[str] = field(default_factory=list)
150152
run_ids: list[str] = field(default_factory=list)
151153
run_keys: list[str] = field(default_factory=list)
154+
search: Optional[str] = None
152155
start_range: Optional[datetime] = None
153156
start_range_begin: Optional[datetime] = None
154157
start_range_end: Optional[datetime] = None
@@ -331,6 +334,7 @@ def from_params(cls, params: MultiDict, project_ids: list[UUID] | None = None) -
331334
TASK_ID_QUERY_NAME,
332335
DATE_RANGE_START_QUERY_NAME,
333336
DATE_RANGE_END_QUERY_NAME,
337+
SEARCH_QUERY_NAME,
334338
],
335339
)
336340

common/events/converters.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ def handle_message_log_v2(self, event: v2.MessageLogUserEvent) -> bool:
231231
self.v1_event = v1.MessageLogEvent(
232232
log_level=message.level.name,
233233
message=message.message,
234+
message_details=message.message_details,
234235
event_type=v1.MessageLogEvent.__name__,
235236
**self._extract_common_attributes(event),
236237
**self._extract_component_data(
@@ -509,7 +510,11 @@ def handle_run_status(self, event: v1.RunStatusEvent) -> bool:
509510
def handle_message_log(self, event: v1.MessageLogEvent) -> bool:
510511
self.v2_event = v2.MessageLogUserEvent(
511512
event_payload=v2.MessageLog(
512-
log_entries=[v2.LogEntry(level=v2.LogLevel[event.log_level], message=event.message)],
513+
log_entries=[
514+
v2.LogEntry(
515+
level=v2.LogLevel[event.log_level], message=event.message, message_details=event.message_details
516+
)
517+
],
513518
component=self._extract_component_data(event),
514519
**self._extract_common_payload_attributes(event),
515520
),

common/events/v1/message_log_event.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class MessageLogEventBaseSchema(Schema):
3030
validate=not_empty(),
3131
metadata={"description": "Required. The body of the message to log.", "example": "The job has completed."},
3232
)
33+
message_details = Str(
34+
load_default=None,
35+
metadata={"description": "Optional. Additional details about the message."},
36+
)
3337

3438

3539
class MessageLogEventSchema(MessageLogEventBaseSchema, EventSchema):
@@ -46,6 +50,7 @@ class MessageLogEvent(Event):
4650

4751
log_level: str
4852
message: str
53+
message_details: str | None = None
4954

5055
__schema__ = MessageLogEventSchema
5156
__api_schema__ = MessageLogEventApiSchema

common/events/v2/message_log.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class LogLevel(std_Enum):
3131
class LogEntry:
3232
level: LogLevel
3333
message: str
34+
message_details: str | None = None
3435

3536

3637
@dataclass
@@ -50,6 +51,10 @@ class LogEntrySchema(Schema):
5051
validate=not_empty(),
5152
metadata={"description": "Required. The body of the message to log.", "example": "The job has completed."},
5253
)
54+
message_details = Str(
55+
load_default=None,
56+
metadata={"description": "Optional. Additional details about the message."},
57+
)
5358

5459
@post_load
5560
def to_dataclass(self, data: dict, **_: Any) -> LogEntry:

common/tests/unit/entity_services/helpers/test_filter_rules.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ def test_filters_invalid_instance_status():
167167
Filters.validate_instance_status(["RANDOM"])
168168

169169

170+
@pytest.mark.unit
171+
def test_project_event_filters_search():
172+
filters = ProjectEventFilters.from_params(MultiDict([("search", "some query")]))
173+
assert filters.search == "some query"
174+
175+
176+
@pytest.mark.unit
177+
def test_project_event_filters_search_empty():
178+
filters = ProjectEventFilters.from_params(MultiDict([]))
179+
assert filters.search is None
180+
181+
170182
@pytest.mark.unit
171183
def test_upcoming_instances_filters_valid():
172184
params = MultiDict(

common/tests/unit/events/test_converters.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,47 @@ def test_convert_message_log_events(base_fields, id_fields, component_fields):
123123
assert event == to_v1(to_v2(event))
124124

125125

126+
@pytest.mark.unit
127+
def test_convert_message_log_with_message_details(base_fields, id_fields, component_fields):
128+
"""Test that message_details round-trips through v1 -> v2 -> v1 conversion."""
129+
data = {
130+
**{k: v[0] for k, v in base_fields.items()},
131+
**{k: v[0] for k, v in id_fields.items()},
132+
**{k: v[0] for k, v in component_fields["batch"].items()},
133+
"log_level": MessageEventLogLevel.ERROR.name,
134+
"message": "test message",
135+
"message_details": "some extra details about the error",
136+
"event_type": MessageLogEvent.__name__,
137+
}
138+
event = instantiate_event_from_data(data)
139+
assert event.message_details == "some extra details about the error"
140+
v2_event = to_v2(event)
141+
assert v2_event.event_payload.log_entries[0].message_details == "some extra details about the error"
142+
roundtrip = to_v1(v2_event)
143+
assert roundtrip == event
144+
assert roundtrip.message_details == "some extra details about the error"
145+
146+
147+
@pytest.mark.unit
148+
def test_convert_message_log_without_message_details(base_fields, id_fields, component_fields):
149+
"""Test that None message_details round-trips correctly."""
150+
data = {
151+
**{k: v[0] for k, v in base_fields.items()},
152+
**{k: v[0] for k, v in id_fields.items()},
153+
**{k: v[0] for k, v in component_fields["batch"].items()},
154+
"log_level": MessageEventLogLevel.INFO.name,
155+
"message": "test message",
156+
"event_type": MessageLogEvent.__name__,
157+
}
158+
event = instantiate_event_from_data(data)
159+
assert event.message_details is None
160+
v2_event = to_v2(event)
161+
assert v2_event.event_payload.log_entries[0].message_details is None
162+
roundtrip = to_v1(v2_event)
163+
assert roundtrip == event
164+
assert roundtrip.message_details is None
165+
166+
126167
@pytest.mark.unit
127168
@pytest.mark.converters_slow
128169
def test_convert_metric_log_events(base_fields, id_fields, component_fields):

common/tests/unit/events/v1/test_message_log.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,24 @@ def test_message_log_min_length_message(message_log_event_data):
8080
MessageLogEvent.from_dict(message_log_event_data)
8181

8282

83+
@pytest.mark.unit
84+
def test_message_log_with_message_details(unidentified_message_log_data):
85+
unidentified_message_log_data["message_details"] = "Some extra details"
86+
event = MessageLogEvent.as_event_from_request(unidentified_message_log_data)
87+
assert event.message_details == "Some extra details"
88+
data = event.as_dict()
89+
assert data["message_details"] == "Some extra details"
90+
restored = MessageLogEvent.from_dict(data)
91+
assert restored.message_details == "Some extra details"
92+
93+
94+
@pytest.mark.unit
95+
def test_message_log_without_message_details(unidentified_message_log_data):
96+
assert "message_details" not in unidentified_message_log_data
97+
event = MessageLogEvent.as_event_from_request(unidentified_message_log_data)
98+
assert event.message_details is None
99+
100+
83101
@pytest.mark.unit
84102
def test_message_log_validate_default_values(message_log_event_data):
85103
message_log_event_data.pop("task_key", None)

common/tests/unit/events/v2/test_message_log.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ def test_log_entries(default_base_payload_dict, message_log_data, log_entry, def
4848
assert MessageLogSchema().load(message_log_data) == expected_result
4949

5050

51+
@pytest.mark.unit
52+
def test_log_entry_with_message_details():
53+
data = {"level": "WARNING", "message": "Something happened", "message_details": "Extra detail here"}
54+
entry = LogEntrySchema().load(data)
55+
assert entry == LogEntry(level=LogLevel.WARNING, message="Something happened", message_details="Extra detail here")
56+
57+
58+
@pytest.mark.unit
59+
def test_log_entry_without_message_details():
60+
data = {"level": "INFO", "message": "No details"}
61+
entry = LogEntrySchema().load(data)
62+
assert entry.message_details is None
63+
64+
5165
@pytest.mark.unit
5266
def test_message_log_empty(message_log_data):
5367
del message_log_data["log_entries"]

observability_api/endpoints/v1/projects.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,13 @@ def get(self, project_id: UUID) -> Response:
248248
type: string
249249
format: date
250250
required: false
251+
- in: query
252+
name: search
253+
schema:
254+
type: string
255+
required: false
256+
description: Optional. A case-insensitive search query. Matches MESSAGE_LOG events by message and
257+
message_details, and METRIC_LOG events by metric key and value.
251258
- in: query
252259
name: event_type
253260
description: Optional. If specified, the results will be limited to events with one of the specified event

0 commit comments

Comments
 (0)