Skip to content

Commit 099d604

Browse files
SNOW-2193769 Enable partial, case-insensitive matching for object names in snow logs (#2600)
* SNOW-2193769 Enable partial, case-insensitive matching for object names in `snow logs` * fix snapshot * added missing snapshots
1 parent 28105c6 commit 099d604

File tree

6 files changed

+192
-1
lines changed

6 files changed

+192
-1
lines changed

RELEASE-NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
## New additions
2222
* The `!edit` command for external editors was added to REPL
23+
* Added `--partial` flag to `snow logs` command for partial, case-insensitive object name matching
2324

2425
## Fixes and improvements
2526
* Fixed crashes with older x86_64 Intel CPUs

src/snowflake/cli/_plugins/logs/commands.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ def get_logs(
4848
"--log-level",
4949
help="The log level to filter by. If not provided, INFO will be used",
5050
),
51+
partial_match: bool = typer.Option(
52+
False,
53+
"--partial",
54+
help="Enable partial, case-insensitive matching for object names",
55+
),
5156
**options,
5257
):
5358
"""
@@ -75,6 +80,7 @@ def get_logs(
7580
refresh_time=refresh_time,
7681
event_table=event_table,
7782
log_level=log_level,
83+
partial_match=partial_match,
7884
)
7985
logs = itertools.chain(
8086
(MessageResult(log.log_message) for logs in logs_stream for log in logs)
@@ -87,6 +93,7 @@ def get_logs(
8793
to_time=to_time,
8894
event_table=event_table,
8995
log_level=log_level,
96+
partial_match=partial_match,
9097
)
9198
logs = (MessageResult(log.log_message) for log in logs_iterable) # type: ignore
9299

src/snowflake/cli/_plugins/logs/manager.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from snowflake.cli._plugins.object.commands import NameArgument, ObjectArgument
1313
from snowflake.cli.api.identifiers import FQN
14+
from snowflake.cli.api.project.util import escape_like_pattern
1415
from snowflake.cli.api.sql_execution import SqlExecutionMixin
1516
from snowflake.connector.cursor import SnowflakeCursor
1617

@@ -24,6 +25,7 @@ def stream_logs(
2425
from_time: Optional[datetime] = None,
2526
event_table: Optional[str] = None,
2627
log_level: Optional[str] = "INFO",
28+
partial_match: bool = False,
2729
) -> Iterable[List[LogsQueryRow]]:
2830
try:
2931
previous_end = from_time
@@ -36,6 +38,7 @@ def stream_logs(
3638
to_time=None,
3739
event_table=event_table,
3840
log_level=log_level,
41+
partial_match=partial_match,
3942
).fetchall()
4043

4144
if raw_logs:
@@ -56,6 +59,7 @@ def get_logs(
5659
to_time: Optional[datetime] = None,
5760
event_table: Optional[str] = None,
5861
log_level: Optional[str] = "INFO",
62+
partial_match: bool = False,
5963
) -> Iterable[LogsQueryRow]:
6064
"""
6165
Basic function to get a single batch of logs from the server
@@ -68,6 +72,7 @@ def get_logs(
6872
to_time=to_time,
6973
event_table=event_table,
7074
log_level=log_level,
75+
partial_match=partial_match,
7176
)
7277

7378
return sanitize_logs(logs)
@@ -80,10 +85,23 @@ def get_raw_logs(
8085
to_time: Optional[datetime] = None,
8186
event_table: Optional[str] = None,
8287
log_level: Optional[str] = "INFO",
88+
partial_match: bool = False,
8389
) -> SnowflakeCursor:
8490

8591
table = event_table if event_table else "SNOWFLAKE.TELEMETRY.EVENTS"
8692

93+
# Escape single quotes in object_name to prevent SQL injection
94+
escaped_object_name = str(object_name).replace("'", "''")
95+
96+
# Build the object name condition based on partial_match flag
97+
if partial_match:
98+
# Use ILIKE for case-insensitive partial matching with wildcards
99+
escaped_pattern = escape_like_pattern(escaped_object_name)
100+
object_condition = f"object_name ILIKE '%{escaped_pattern}%'"
101+
else:
102+
# Use exact match (original behavior)
103+
object_condition = f"object_name = '{escaped_object_name}'"
104+
87105
query = dedent(
88106
f"""
89107
SELECT
@@ -96,7 +114,7 @@ def get_raw_logs(
96114
FROM {table}
97115
WHERE record_type = 'LOG'
98116
AND (record:severity_text IN ({parse_log_levels_for_query((log_level))}) or record:severity_text is NULL )
99-
AND object_name = '{object_name}'
117+
AND {object_condition}
100118
{get_timestamp_query(from_time, to_time)}
101119
ORDER BY timestamp;
102120
"""

tests/__snapshots__/test_help_messages.ambr

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9859,6 +9859,8 @@
98599859
| --log-level TEXT The log level to filter by. If not provided, |
98609860
| INFO will be used |
98619861
| [default: INFO] |
9862+
| --partial Enable partial, case-insensitive matching for |
9863+
| object names |
98629864
| --help -h Show this message and exit. |
98639865
+------------------------------------------------------------------------------+
98649866
+- Connection configuration ---------------------------------------------------+

tests/logs/__snapshots__/test_logs.ambr

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,116 @@
6464

6565
'''
6666
# ---
67+
# name: test_partial_match_query_construction[MyObject-False]
68+
'''
69+
SELECT
70+
timestamp,
71+
resource_attributes:"snow.database.name"::string as database_name,
72+
resource_attributes:"snow.schema.name"::string as schema_name,
73+
resource_attributes:"snow.table.name"::string as object_name,
74+
record:severity_text::string as log_level,
75+
value::string as log_message
76+
FROM SNOWFLAKE.TELEMETRY.EVENTS
77+
WHERE record_type = 'LOG'
78+
AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL )
79+
AND object_name = 'MyObject'
80+
AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02')
81+
AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02')
82+
83+
ORDER BY timestamp;
84+
'''
85+
# ---
86+
# name: test_partial_match_query_construction[MyObject-True]
87+
'''
88+
SELECT
89+
timestamp,
90+
resource_attributes:"snow.database.name"::string as database_name,
91+
resource_attributes:"snow.schema.name"::string as schema_name,
92+
resource_attributes:"snow.table.name"::string as object_name,
93+
record:severity_text::string as log_level,
94+
value::string as log_message
95+
FROM SNOWFLAKE.TELEMETRY.EVENTS
96+
WHERE record_type = 'LOG'
97+
AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL )
98+
AND object_name ILIKE '%MyObject%'
99+
AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02')
100+
AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02')
101+
102+
ORDER BY timestamp;
103+
'''
104+
# ---
105+
# name: test_partial_match_query_construction[test_obj-False]
106+
'''
107+
SELECT
108+
timestamp,
109+
resource_attributes:"snow.database.name"::string as database_name,
110+
resource_attributes:"snow.schema.name"::string as schema_name,
111+
resource_attributes:"snow.table.name"::string as object_name,
112+
record:severity_text::string as log_level,
113+
value::string as log_message
114+
FROM SNOWFLAKE.TELEMETRY.EVENTS
115+
WHERE record_type = 'LOG'
116+
AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL )
117+
AND object_name = 'test_obj'
118+
AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02')
119+
AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02')
120+
121+
ORDER BY timestamp;
122+
'''
123+
# ---
124+
# name: test_partial_match_query_construction[test_obj-True]
125+
'''
126+
SELECT
127+
timestamp,
128+
resource_attributes:"snow.database.name"::string as database_name,
129+
resource_attributes:"snow.schema.name"::string as schema_name,
130+
resource_attributes:"snow.table.name"::string as object_name,
131+
record:severity_text::string as log_level,
132+
value::string as log_message
133+
FROM SNOWFLAKE.TELEMETRY.EVENTS
134+
WHERE record_type = 'LOG'
135+
AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL )
136+
AND object_name ILIKE '%test\\_obj%'
137+
AND timestamp >= TO_TIMESTAMP_LTZ('2022-02-02T02:02:02')
138+
AND timestamp <= TO_TIMESTAMP_LTZ('2022-02-03T02:02:02')
139+
140+
ORDER BY timestamp;
141+
'''
142+
# ---
143+
# name: test_partial_match_with_like_wildcards[test_obj_with_percent]
144+
'''
145+
SELECT
146+
timestamp,
147+
resource_attributes:"snow.database.name"::string as database_name,
148+
resource_attributes:"snow.schema.name"::string as schema_name,
149+
resource_attributes:"snow.table.name"::string as object_name,
150+
record:severity_text::string as log_level,
151+
value::string as log_message
152+
FROM SNOWFLAKE.TELEMETRY.EVENTS
153+
WHERE record_type = 'LOG'
154+
AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL )
155+
AND object_name ILIKE '%test\\_obj\\_with\\_percent%'
156+
157+
ORDER BY timestamp;
158+
'''
159+
# ---
160+
# name: test_partial_match_with_like_wildcards[test_obj_with_underscore]
161+
'''
162+
SELECT
163+
timestamp,
164+
resource_attributes:"snow.database.name"::string as database_name,
165+
resource_attributes:"snow.schema.name"::string as schema_name,
166+
resource_attributes:"snow.table.name"::string as object_name,
167+
record:severity_text::string as log_level,
168+
value::string as log_message
169+
FROM SNOWFLAKE.TELEMETRY.EVENTS
170+
WHERE record_type = 'LOG'
171+
AND (record:severity_text IN ('INFO', 'WARN', 'ERROR', 'FATAL') or record:severity_text is NULL )
172+
AND object_name ILIKE '%test\\_obj\\_with\\_underscore%'
173+
174+
ORDER BY timestamp;
175+
'''
176+
# ---
67177
# name: test_providing_time_in_incorrect_format_causes_error[2024-11-03 12:00:00 UTC---from]
68178
'''
69179
+- Error ----------------------------------------------------------------------+

tests/logs/test_logs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,56 @@ def test_if_incorrect_log_level_causes_error(runner, snapshot):
9191
)
9292
assert result.exit_code == 1
9393
assert result.output == snapshot
94+
95+
96+
@pytest.mark.parametrize("partial_match", [True, False])
97+
@pytest.mark.parametrize("object_name", ["test_obj", "MyObject"])
98+
def test_partial_match_query_construction(
99+
mock_connect, mock_ctx, runner, snapshot, partial_match, object_name
100+
):
101+
"""Test that SQL queries are properly constructed for both exact and partial matching"""
102+
ctx = mock_ctx()
103+
mock_connect.return_value = ctx
104+
105+
args = [
106+
"logs",
107+
"table",
108+
object_name,
109+
"--from",
110+
"2022-02-02 02:02:02",
111+
"--to",
112+
"2022-02-03 02:02:02",
113+
]
114+
115+
if partial_match:
116+
args.append("--partial")
117+
118+
_ = runner.invoke(args)
119+
120+
queries = ctx.get_queries()
121+
assert len(queries) == 1
122+
assert queries[0] == snapshot
123+
124+
125+
@pytest.mark.parametrize(
126+
"object_name", ["test_obj_with_underscore", "test_obj_with_percent"]
127+
)
128+
def test_partial_match_with_like_wildcards(
129+
mock_connect, mock_ctx, runner, snapshot, object_name
130+
):
131+
"""Test that SQL LIKE wildcard characters in object names are properly escaped when using partial matching"""
132+
ctx = mock_ctx()
133+
mock_connect.return_value = ctx
134+
135+
args = [
136+
"logs",
137+
"table",
138+
object_name,
139+
"--partial",
140+
]
141+
142+
_ = runner.invoke(args)
143+
144+
queries = ctx.get_queries()
145+
assert len(queries) == 1
146+
assert queries[0] == snapshot

0 commit comments

Comments
 (0)