devlogs 2.2.0__tar.gz → 2.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devlogs-2.2.0/src/devlogs.egg-info → devlogs-2.2.1}/PKG-INFO +1 -1
- {devlogs-2.2.0 → devlogs-2.2.1}/pyproject.toml +1 -1
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/cli.py +22 -4
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/mcp/server.py +95 -2
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/queries.py +30 -11
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/server.py +8 -2
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/static/devlogs.js +3 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/static/index.html +4 -0
- {devlogs-2.2.0 → devlogs-2.2.1/src/devlogs.egg-info}/PKG-INFO +1 -1
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_web.py +1 -1
- {devlogs-2.2.0 → devlogs-2.2.1}/LICENSE +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/MANIFEST.in +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/README.md +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/setup.cfg +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/__main__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/build_info.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/auth.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/cli.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/errors.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/forwarder.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/ingestor.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/schema.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/server.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/config.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/context.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/demo.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/devlogs_client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/formatting.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/handler.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/jenkins/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/jenkins/cli.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/jenkins/core.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/levels.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/mcp/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/indexing.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/mappings.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/retention.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/scrub.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/time_utils.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/version.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/static/devlogs.css +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/wrapper.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/SOURCES.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/dependency_links.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/entry_points.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/requires.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/top_level.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_build_info.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_cli.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_auth.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_config.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_schema.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_server.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_config.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_context.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_devlogs_client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_formatting.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_handler.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_indexing.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_levels.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_mappings.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_mcp_server.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_opensearch_client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_opensearch_queries.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_retention.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_scrub.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_time_utils.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_url_parsing.py +0 -0
|
@@ -650,8 +650,10 @@ def diagnose(
|
|
|
650
650
|
def tail(
|
|
651
651
|
operation_id: str = typer.Option(None, "--operation", "-o"),
|
|
652
652
|
area: str = typer.Option(None, "--area"),
|
|
653
|
+
component: str = typer.Option(None, "--component", "-c", help="Filter by component name"),
|
|
653
654
|
level: str = typer.Option(None, "--level"),
|
|
654
655
|
since: str = typer.Option(None, "--since"),
|
|
656
|
+
application: str = typer.Option(None, "--application", "-a", help="Filter by application name"),
|
|
655
657
|
limit: int = typer.Option(20, "--limit"),
|
|
656
658
|
follow: bool = typer.Option(False, "--follow", "-f"),
|
|
657
659
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
|
|
@@ -698,12 +700,18 @@ def tail(
|
|
|
698
700
|
color=typer.colors.YELLOW,
|
|
699
701
|
)
|
|
700
702
|
|
|
703
|
+
effective_application = application or cfg.application
|
|
704
|
+
|
|
701
705
|
if verbose:
|
|
702
706
|
parts = []
|
|
707
|
+
if effective_application:
|
|
708
|
+
parts.append(f"application={effective_application}")
|
|
703
709
|
if operation_id:
|
|
704
710
|
parts.append(f"operation={operation_id}")
|
|
705
711
|
if area:
|
|
706
712
|
parts.append(f"area={area}")
|
|
713
|
+
if component:
|
|
714
|
+
parts.append(f"component={component}")
|
|
707
715
|
if level:
|
|
708
716
|
parts.append(f"level={level}")
|
|
709
717
|
if since:
|
|
@@ -728,7 +736,8 @@ def tail(
|
|
|
728
736
|
since=since,
|
|
729
737
|
limit=limit,
|
|
730
738
|
search_after=search_after,
|
|
731
|
-
application=
|
|
739
|
+
application=effective_application,
|
|
740
|
+
component=component,
|
|
732
741
|
)
|
|
733
742
|
_verbose_echo(f"Received {len(docs)} docs, next cursor={search_after}")
|
|
734
743
|
if verbose and docs:
|
|
@@ -820,9 +829,11 @@ def tail(
|
|
|
820
829
|
def search(
|
|
821
830
|
q: str = typer.Option("", "--q", help="Search query"),
|
|
822
831
|
area: str = typer.Option(None, "--area"),
|
|
832
|
+
component: str = typer.Option(None, "--component", "-c", help="Filter by component name"),
|
|
823
833
|
level: str = typer.Option(None, "--level"),
|
|
824
834
|
operation_id: str = typer.Option(None, "--operation", "-o"),
|
|
825
835
|
since: str = typer.Option(None, "--since"),
|
|
836
|
+
application: str = typer.Option(None, "--application", "-a", help="Filter by application name"),
|
|
826
837
|
limit: int = typer.Option(50, "--limit"),
|
|
827
838
|
follow: bool = typer.Option(False, "--follow", "-f"),
|
|
828
839
|
utc: bool = typer.Option(False, "--utc", help="Display timestamps in UTC instead of local time"),
|
|
@@ -834,6 +845,7 @@ def search(
|
|
|
834
845
|
|
|
835
846
|
_apply_common_options(env, url)
|
|
836
847
|
client, cfg = require_opensearch()
|
|
848
|
+
effective_application = application or cfg.application
|
|
837
849
|
search_after = None
|
|
838
850
|
consecutive_errors = 0
|
|
839
851
|
max_errors = 3
|
|
@@ -852,7 +864,8 @@ def search(
|
|
|
852
864
|
since=since,
|
|
853
865
|
limit=limit,
|
|
854
866
|
search_after=search_after,
|
|
855
|
-
application=
|
|
867
|
+
application=effective_application,
|
|
868
|
+
component=component,
|
|
856
869
|
)
|
|
857
870
|
else:
|
|
858
871
|
docs = search_logs(
|
|
@@ -864,7 +877,8 @@ def search(
|
|
|
864
877
|
level=level,
|
|
865
878
|
since=since,
|
|
866
879
|
limit=limit,
|
|
867
|
-
application=
|
|
880
|
+
application=effective_application,
|
|
881
|
+
component=component,
|
|
868
882
|
)
|
|
869
883
|
entries = normalize_log_entries(docs, limit=limit)
|
|
870
884
|
consecutive_errors = 0
|
|
@@ -926,9 +940,11 @@ def search(
|
|
|
926
940
|
def last_error(
|
|
927
941
|
q: str = typer.Option("", "--q", help="Search query"),
|
|
928
942
|
area: str = typer.Option(None, "--area"),
|
|
943
|
+
component: str = typer.Option(None, "--component", "-c", help="Filter by component name"),
|
|
929
944
|
operation_id: str = typer.Option(None, "--operation", "-o"),
|
|
930
945
|
since: str = typer.Option(None, "--since"),
|
|
931
946
|
until: str = typer.Option(None, "--until"),
|
|
947
|
+
application: str = typer.Option(None, "--application", "-a", help="Filter by application name"),
|
|
932
948
|
limit: int = typer.Option(1, "--limit"),
|
|
933
949
|
utc: bool = typer.Option(False, "--utc", help="Display timestamps in UTC instead of local time"),
|
|
934
950
|
env: str = ENV_OPTION,
|
|
@@ -939,6 +955,7 @@ def last_error(
|
|
|
939
955
|
|
|
940
956
|
_apply_common_options(env, url)
|
|
941
957
|
client, cfg = require_opensearch()
|
|
958
|
+
effective_application = application or cfg.application
|
|
942
959
|
|
|
943
960
|
try:
|
|
944
961
|
docs = get_last_errors(
|
|
@@ -950,7 +967,8 @@ def last_error(
|
|
|
950
967
|
since=since,
|
|
951
968
|
until=until,
|
|
952
969
|
limit=limit,
|
|
953
|
-
application=
|
|
970
|
+
application=effective_application,
|
|
971
|
+
component=component,
|
|
954
972
|
)
|
|
955
973
|
entries = normalize_log_entries(docs, limit=limit)
|
|
956
974
|
except (ConnectionFailedError, urllib.error.URLError) as e:
|
|
@@ -135,6 +135,10 @@ async def main():
|
|
|
135
135
|
"type": "string",
|
|
136
136
|
"description": "Filter by application area (e.g., 'api', 'database', 'auth')",
|
|
137
137
|
},
|
|
138
|
+
"component": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "Filter by component name (e.g., 'web', 'worker', 'jenkins')",
|
|
141
|
+
},
|
|
138
142
|
"operation_id": {
|
|
139
143
|
"type": "string",
|
|
140
144
|
"description": "Filter by specific operation ID to see all logs for that operation",
|
|
@@ -161,6 +165,10 @@ async def main():
|
|
|
161
165
|
"items": {"type": ["string", "number"]},
|
|
162
166
|
"description": "Cursor from a previous response for pagination",
|
|
163
167
|
},
|
|
168
|
+
"application": {
|
|
169
|
+
"type": "string",
|
|
170
|
+
"description": "Filter by application name",
|
|
171
|
+
},
|
|
164
172
|
},
|
|
165
173
|
},
|
|
166
174
|
),
|
|
@@ -182,6 +190,10 @@ async def main():
|
|
|
182
190
|
"type": "string",
|
|
183
191
|
"description": "Filter by application area",
|
|
184
192
|
},
|
|
193
|
+
"component": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "Filter by component name",
|
|
196
|
+
},
|
|
185
197
|
"level": {
|
|
186
198
|
"type": "string",
|
|
187
199
|
"description": "Filter by log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
|
@@ -204,6 +216,10 @@ async def main():
|
|
|
204
216
|
"items": {"type": ["string", "number"]},
|
|
205
217
|
"description": "Cursor from a previous response for pagination",
|
|
206
218
|
},
|
|
219
|
+
"application": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"description": "Filter by application name",
|
|
222
|
+
},
|
|
207
223
|
},
|
|
208
224
|
},
|
|
209
225
|
),
|
|
@@ -217,6 +233,14 @@ async def main():
|
|
|
217
233
|
"type": "string",
|
|
218
234
|
"description": "The operation ID to summarize",
|
|
219
235
|
},
|
|
236
|
+
"component": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Filter by component name",
|
|
239
|
+
},
|
|
240
|
+
"application": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Filter by application name",
|
|
243
|
+
},
|
|
220
244
|
},
|
|
221
245
|
"required": ["operation_id"],
|
|
222
246
|
},
|
|
@@ -257,6 +281,14 @@ async def main():
|
|
|
257
281
|
"items": {"type": ["string", "number"]},
|
|
258
282
|
"description": "Cursor from a previous response for pagination",
|
|
259
283
|
},
|
|
284
|
+
"component": {
|
|
285
|
+
"type": "string",
|
|
286
|
+
"description": "Filter by component name",
|
|
287
|
+
},
|
|
288
|
+
"application": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"description": "Filter by application name",
|
|
291
|
+
},
|
|
260
292
|
},
|
|
261
293
|
"required": ["operation_id"],
|
|
262
294
|
},
|
|
@@ -271,6 +303,10 @@ async def main():
|
|
|
271
303
|
"type": "string",
|
|
272
304
|
"description": "Filter by application area",
|
|
273
305
|
},
|
|
306
|
+
"component": {
|
|
307
|
+
"type": "string",
|
|
308
|
+
"description": "Filter by component name",
|
|
309
|
+
},
|
|
274
310
|
"since": {
|
|
275
311
|
"type": "string",
|
|
276
312
|
"description": "ISO timestamp or relative duration like '1h' to filter operations after this time",
|
|
@@ -285,6 +321,10 @@ async def main():
|
|
|
285
321
|
"description": "Only show operations that had errors",
|
|
286
322
|
"default": False,
|
|
287
323
|
},
|
|
324
|
+
"application": {
|
|
325
|
+
"type": "string",
|
|
326
|
+
"description": "Filter by application name",
|
|
327
|
+
},
|
|
288
328
|
},
|
|
289
329
|
},
|
|
290
330
|
),
|
|
@@ -298,6 +338,10 @@ async def main():
|
|
|
298
338
|
"type": "string",
|
|
299
339
|
"description": "Filter by application area",
|
|
300
340
|
},
|
|
341
|
+
"component": {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": "Filter by component name",
|
|
344
|
+
},
|
|
301
345
|
"since": {
|
|
302
346
|
"type": "string",
|
|
303
347
|
"description": "ISO timestamp or relative duration like '1h' to filter operations after this time",
|
|
@@ -321,6 +365,10 @@ async def main():
|
|
|
321
365
|
"description": "Only show operations that had errors",
|
|
322
366
|
"default": False,
|
|
323
367
|
},
|
|
368
|
+
"application": {
|
|
369
|
+
"type": "string",
|
|
370
|
+
"description": "Filter by application name",
|
|
371
|
+
},
|
|
324
372
|
},
|
|
325
373
|
},
|
|
326
374
|
),
|
|
@@ -339,6 +387,14 @@ async def main():
|
|
|
339
387
|
"description": "Minimum number of operations an area must have to be included",
|
|
340
388
|
"default": 1,
|
|
341
389
|
},
|
|
390
|
+
"component": {
|
|
391
|
+
"type": "string",
|
|
392
|
+
"description": "Filter by component name",
|
|
393
|
+
},
|
|
394
|
+
"application": {
|
|
395
|
+
"type": "string",
|
|
396
|
+
"description": "Filter by application name",
|
|
397
|
+
},
|
|
342
398
|
},
|
|
343
399
|
},
|
|
344
400
|
),
|
|
@@ -356,6 +412,10 @@ async def main():
|
|
|
356
412
|
"type": "string",
|
|
357
413
|
"description": "Filter by application area",
|
|
358
414
|
},
|
|
415
|
+
"component": {
|
|
416
|
+
"type": "string",
|
|
417
|
+
"description": "Filter by component name",
|
|
418
|
+
},
|
|
359
419
|
"since": {
|
|
360
420
|
"type": "string",
|
|
361
421
|
"description": "ISO timestamp or relative duration like '1h' to filter logs after this time",
|
|
@@ -379,6 +439,10 @@ async def main():
|
|
|
379
439
|
"description": "Include logs missing the signature field",
|
|
380
440
|
"default": False,
|
|
381
441
|
},
|
|
442
|
+
"application": {
|
|
443
|
+
"type": "string",
|
|
444
|
+
"description": "Filter by application name",
|
|
445
|
+
},
|
|
382
446
|
},
|
|
383
447
|
},
|
|
384
448
|
),
|
|
@@ -396,6 +460,10 @@ async def main():
|
|
|
396
460
|
"type": "string",
|
|
397
461
|
"description": "Filter by application area",
|
|
398
462
|
},
|
|
463
|
+
"component": {
|
|
464
|
+
"type": "string",
|
|
465
|
+
"description": "Filter by component name",
|
|
466
|
+
},
|
|
399
467
|
"operation_id": {
|
|
400
468
|
"type": "string",
|
|
401
469
|
"description": "Filter by specific operation ID",
|
|
@@ -413,6 +481,10 @@ async def main():
|
|
|
413
481
|
"description": "Maximum number of error entries to return (default: 1, max: 100)",
|
|
414
482
|
"default": 1,
|
|
415
483
|
},
|
|
484
|
+
"application": {
|
|
485
|
+
"type": "string",
|
|
486
|
+
"description": "Filter by application name",
|
|
487
|
+
},
|
|
416
488
|
},
|
|
417
489
|
},
|
|
418
490
|
),
|
|
@@ -434,6 +506,10 @@ async def main():
|
|
|
434
506
|
"type": "string",
|
|
435
507
|
"description": "Filter by application area",
|
|
436
508
|
},
|
|
509
|
+
"component": {
|
|
510
|
+
"type": "string",
|
|
511
|
+
"description": "Filter by component name",
|
|
512
|
+
},
|
|
437
513
|
"query": {
|
|
438
514
|
"type": "string",
|
|
439
515
|
"description": "Text search query to match against log messages, logger names, and features",
|
|
@@ -452,6 +528,10 @@ async def main():
|
|
|
452
528
|
"description": "Number of entries after the anchor (default: 20)",
|
|
453
529
|
"default": 20,
|
|
454
530
|
},
|
|
531
|
+
"application": {
|
|
532
|
+
"type": "string",
|
|
533
|
+
"description": "Filter by application name",
|
|
534
|
+
},
|
|
455
535
|
},
|
|
456
536
|
"required": ["anchor_timestamp"],
|
|
457
537
|
},
|
|
@@ -467,10 +547,14 @@ async def main():
|
|
|
467
547
|
arguments = {}
|
|
468
548
|
|
|
469
549
|
try:
|
|
470
|
-
client, index,
|
|
550
|
+
client, index, config_application = _create_client_and_index()
|
|
471
551
|
except RuntimeError as e:
|
|
472
552
|
return _error_response(str(e), "InitializationError")
|
|
473
553
|
|
|
554
|
+
application = arguments.get("application") or config_application
|
|
555
|
+
|
|
556
|
+
component = arguments.get("component")
|
|
557
|
+
|
|
474
558
|
if name == "search_logs":
|
|
475
559
|
query = arguments.get("query")
|
|
476
560
|
area = arguments.get("area")
|
|
@@ -495,6 +579,7 @@ async def main():
|
|
|
495
579
|
cursor=cursor,
|
|
496
580
|
sort_order="desc",
|
|
497
581
|
application=application,
|
|
582
|
+
component=component,
|
|
498
583
|
)
|
|
499
584
|
entries = _normalize_entries(docs, limit=limit)
|
|
500
585
|
|
|
@@ -533,6 +618,7 @@ async def main():
|
|
|
533
618
|
limit=limit,
|
|
534
619
|
search_after=cursor,
|
|
535
620
|
application=application,
|
|
621
|
+
component=component,
|
|
536
622
|
)
|
|
537
623
|
entries = _normalize_entries(docs, limit=limit)
|
|
538
624
|
|
|
@@ -554,7 +640,7 @@ async def main():
|
|
|
554
640
|
return _error_response("operation_id is required", "ValidationError")
|
|
555
641
|
|
|
556
642
|
try:
|
|
557
|
-
summary = get_operation_summary(client, index, operation_id, application=application)
|
|
643
|
+
summary = get_operation_summary(client, index, operation_id, application=application, component=component)
|
|
558
644
|
|
|
559
645
|
if not summary:
|
|
560
646
|
return _json_response(
|
|
@@ -594,6 +680,7 @@ async def main():
|
|
|
594
680
|
limit=limit,
|
|
595
681
|
cursor=cursor,
|
|
596
682
|
application=application,
|
|
683
|
+
component=component,
|
|
597
684
|
)
|
|
598
685
|
entries = _normalize_entries(docs, limit=limit)
|
|
599
686
|
|
|
@@ -623,6 +710,7 @@ async def main():
|
|
|
623
710
|
limit=limit,
|
|
624
711
|
with_errors_only=with_errors_only,
|
|
625
712
|
application=application,
|
|
713
|
+
component=component,
|
|
626
714
|
)
|
|
627
715
|
|
|
628
716
|
return _json_response(
|
|
@@ -654,6 +742,7 @@ async def main():
|
|
|
654
742
|
order_by=order_by,
|
|
655
743
|
with_errors_only=with_errors_only,
|
|
656
744
|
application=application,
|
|
745
|
+
component=component,
|
|
657
746
|
)
|
|
658
747
|
|
|
659
748
|
return _json_response(
|
|
@@ -676,6 +765,7 @@ async def main():
|
|
|
676
765
|
since=since,
|
|
677
766
|
min_operations=min_operations,
|
|
678
767
|
application=application,
|
|
768
|
+
component=component,
|
|
679
769
|
)
|
|
680
770
|
|
|
681
771
|
return _json_response(
|
|
@@ -709,6 +799,7 @@ async def main():
|
|
|
709
799
|
min_count=min_count,
|
|
710
800
|
include_missing=include_missing,
|
|
711
801
|
application=application,
|
|
802
|
+
component=component,
|
|
712
803
|
)
|
|
713
804
|
return _json_response(
|
|
714
805
|
data={"signatures": signatures},
|
|
@@ -738,6 +829,7 @@ async def main():
|
|
|
738
829
|
until=until,
|
|
739
830
|
limit=limit,
|
|
740
831
|
application=application,
|
|
832
|
+
component=component,
|
|
741
833
|
)
|
|
742
834
|
entries = _normalize_entries(docs, limit=limit)
|
|
743
835
|
return _json_response(
|
|
@@ -775,6 +867,7 @@ async def main():
|
|
|
775
867
|
before=before,
|
|
776
868
|
after=after,
|
|
777
869
|
application=application,
|
|
870
|
+
component=component,
|
|
778
871
|
)
|
|
779
872
|
entries = _normalize_entries(docs)
|
|
780
873
|
return _json_response(
|
|
@@ -32,7 +32,7 @@ def _build_time_range(since: Optional[str], until: Optional[str], since_inclusiv
|
|
|
32
32
|
return {"range": {"timestamp": range_query}}
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _build_log_query(query=None, area=None, operation_id=None, level=None, since=None, until=None, since_inclusive: bool = True, until_inclusive: bool = True, application=None):
|
|
35
|
+
def _build_log_query(query=None, area=None, operation_id=None, level=None, since=None, until=None, since_inclusive: bool = True, until_inclusive: bool = True, application=None, component=None):
|
|
36
36
|
filters = [
|
|
37
37
|
{
|
|
38
38
|
"bool": {
|
|
@@ -48,6 +48,8 @@ def _build_log_query(query=None, area=None, operation_id=None, level=None, since
|
|
|
48
48
|
filters.append({"term": {"application": application}})
|
|
49
49
|
if area:
|
|
50
50
|
filters.append({"term": {"area": area}})
|
|
51
|
+
if component:
|
|
52
|
+
filters.append({"term": {"component": component}})
|
|
51
53
|
if operation_id:
|
|
52
54
|
filters.append({"term": {"operation_id": operation_id}})
|
|
53
55
|
level_terms = _normalize_level_terms(level)
|
|
@@ -109,6 +111,7 @@ def _normalize_entry(doc: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
109
111
|
"level": normalize_level(doc.get("level")),
|
|
110
112
|
"message": doc.get("message"),
|
|
111
113
|
"logger": doc.get("logger"),
|
|
114
|
+
"component": doc.get("component"),
|
|
112
115
|
"area": doc.get("area"),
|
|
113
116
|
"operation_id": doc.get("operation_id"),
|
|
114
117
|
"pathname": doc.get("pathname"),
|
|
@@ -128,7 +131,7 @@ def normalize_log_entries(docs: Iterable[Dict[str, Any]], limit: Optional[int] =
|
|
|
128
131
|
return entries
|
|
129
132
|
|
|
130
133
|
|
|
131
|
-
def search_logs(client, index, query=None, area=None, operation_id=None, level=None, since=None, until=None, limit=50, application=None):
|
|
134
|
+
def search_logs(client, index, query=None, area=None, operation_id=None, level=None, since=None, until=None, limit=50, application=None, component=None):
|
|
132
135
|
"""Search log entries with filters."""
|
|
133
136
|
body = {
|
|
134
137
|
"query": _build_log_query(
|
|
@@ -139,6 +142,7 @@ def search_logs(client, index, query=None, area=None, operation_id=None, level=N
|
|
|
139
142
|
since=since,
|
|
140
143
|
until=until,
|
|
141
144
|
application=application,
|
|
145
|
+
component=component,
|
|
142
146
|
),
|
|
143
147
|
"sort": [{"timestamp": "desc"}, {"_id": "desc"}],
|
|
144
148
|
"size": limit,
|
|
@@ -148,7 +152,7 @@ def search_logs(client, index, query=None, area=None, operation_id=None, level=N
|
|
|
148
152
|
return _hits_to_docs(hits)
|
|
149
153
|
|
|
150
154
|
|
|
151
|
-
def get_last_errors(client, index, query=None, area=None, operation_id=None, since=None, until=None, limit=1, application=None):
|
|
155
|
+
def get_last_errors(client, index, query=None, area=None, operation_id=None, since=None, until=None, limit=1, application=None, component=None):
|
|
152
156
|
"""Get the most recent error/critical log entries."""
|
|
153
157
|
base_query = _build_log_query(
|
|
154
158
|
query=query,
|
|
@@ -157,6 +161,7 @@ def get_last_errors(client, index, query=None, area=None, operation_id=None, sin
|
|
|
157
161
|
since=since,
|
|
158
162
|
until=until,
|
|
159
163
|
application=application,
|
|
164
|
+
component=component,
|
|
160
165
|
)
|
|
161
166
|
base_query.get("bool", {}).get("filter", []).append(
|
|
162
167
|
{"terms": {"level": ["error", "critical"]}}
|
|
@@ -191,6 +196,7 @@ def search_logs_page(
|
|
|
191
196
|
since_inclusive: bool = True,
|
|
192
197
|
until_inclusive: bool = True,
|
|
193
198
|
application=None,
|
|
199
|
+
component=None,
|
|
194
200
|
):
|
|
195
201
|
"""Search log entries with pagination support."""
|
|
196
202
|
body = {
|
|
@@ -204,6 +210,7 @@ def search_logs_page(
|
|
|
204
210
|
since_inclusive=since_inclusive,
|
|
205
211
|
until_inclusive=until_inclusive,
|
|
206
212
|
application=application,
|
|
213
|
+
component=component,
|
|
207
214
|
),
|
|
208
215
|
"sort": _build_sort(sort_order),
|
|
209
216
|
"size": limit,
|
|
@@ -217,7 +224,7 @@ def search_logs_page(
|
|
|
217
224
|
return docs, next_cursor
|
|
218
225
|
|
|
219
226
|
|
|
220
|
-
def get_operation_logs(client, index, operation_id, query=None, level=None, since=None, until=None, limit=100, cursor=None, application=None):
|
|
227
|
+
def get_operation_logs(client, index, operation_id, query=None, level=None, since=None, until=None, limit=100, cursor=None, application=None, component=None):
|
|
221
228
|
"""Get logs for an operation in chronological order."""
|
|
222
229
|
return search_logs_page(
|
|
223
230
|
client=client,
|
|
@@ -231,10 +238,11 @@ def get_operation_logs(client, index, operation_id, query=None, level=None, sinc
|
|
|
231
238
|
cursor=cursor,
|
|
232
239
|
sort_order="asc",
|
|
233
240
|
application=application,
|
|
241
|
+
component=component,
|
|
234
242
|
)
|
|
235
243
|
|
|
236
244
|
|
|
237
|
-
def tail_logs(client, index, query=None, operation_id=None, area=None, level=None, since=None, until=None, limit=20, search_after=None, application=None):
|
|
245
|
+
def tail_logs(client, index, query=None, operation_id=None, area=None, level=None, since=None, until=None, limit=20, search_after=None, application=None, component=None):
|
|
238
246
|
"""Tail log entries for an operation.
|
|
239
247
|
|
|
240
248
|
First call returns the most recent entries (newest first) and reverses for chronological display.
|
|
@@ -249,6 +257,7 @@ def tail_logs(client, index, query=None, operation_id=None, area=None, level=Non
|
|
|
249
257
|
since=since,
|
|
250
258
|
until=until,
|
|
251
259
|
application=application,
|
|
260
|
+
component=component,
|
|
252
261
|
),
|
|
253
262
|
"size": limit,
|
|
254
263
|
}
|
|
@@ -281,9 +290,11 @@ def tail_logs(client, index, query=None, operation_id=None, area=None, level=Non
|
|
|
281
290
|
return docs, next_search_after
|
|
282
291
|
|
|
283
292
|
|
|
284
|
-
def get_operation_summary(client, index, operation_id, application=None):
|
|
293
|
+
def get_operation_summary(client, index, operation_id, application=None, component=None):
|
|
285
294
|
"""Get summary for an operation using aggregations."""
|
|
286
295
|
op_query_filters = [{"term": {"operation_id": operation_id}}]
|
|
296
|
+
if component:
|
|
297
|
+
op_query_filters.append({"term": {"component": component}})
|
|
287
298
|
if application:
|
|
288
299
|
op_query_filters.append({"term": {"application": application}})
|
|
289
300
|
body = {
|
|
@@ -358,11 +369,13 @@ def get_operation_summary(client, index, operation_id, application=None):
|
|
|
358
369
|
}
|
|
359
370
|
|
|
360
371
|
|
|
361
|
-
def list_operations(client, index, area=None, since=None, limit=20, with_errors_only=False, application=None):
|
|
372
|
+
def list_operations(client, index, area=None, since=None, limit=20, with_errors_only=False, application=None, component=None):
|
|
362
373
|
"""List recent operations with summary stats."""
|
|
363
374
|
query_filters = []
|
|
364
375
|
if application:
|
|
365
376
|
query_filters.append({"term": {"application": application}})
|
|
377
|
+
if component:
|
|
378
|
+
query_filters.append({"term": {"component": component}})
|
|
366
379
|
if area:
|
|
367
380
|
query_filters.append({"term": {"area": area}})
|
|
368
381
|
if since:
|
|
@@ -434,9 +447,9 @@ def list_operations(client, index, area=None, since=None, limit=20, with_errors_
|
|
|
434
447
|
return operations
|
|
435
448
|
|
|
436
449
|
|
|
437
|
-
def list_recent_operations(client, index, area=None, since=None, until=None, limit=20, order_by: str = "last_activity", with_errors_only: bool = False, application=None):
|
|
450
|
+
def list_recent_operations(client, index, area=None, since=None, until=None, limit=20, order_by: str = "last_activity", with_errors_only: bool = False, application=None, component=None):
|
|
438
451
|
"""List recent operations ordered by last activity or error count."""
|
|
439
|
-
base_query = _build_log_query(area=area, since=since, until=until, application=application)
|
|
452
|
+
base_query = _build_log_query(area=area, since=since, until=until, application=application, component=component)
|
|
440
453
|
if order_by not in ("last_activity", "error_count"):
|
|
441
454
|
order_by = "last_activity"
|
|
442
455
|
|
|
@@ -551,13 +564,14 @@ def list_error_signatures(
|
|
|
551
564
|
min_count: int = 1,
|
|
552
565
|
include_missing: bool = False,
|
|
553
566
|
application=None,
|
|
567
|
+
component=None,
|
|
554
568
|
):
|
|
555
569
|
"""Aggregate error signatures by exception/message."""
|
|
556
570
|
if not field:
|
|
557
571
|
field = "exception"
|
|
558
572
|
field_name = field if field.endswith(".keyword") else f"{field}.keyword"
|
|
559
573
|
|
|
560
|
-
base_query = _build_log_query(area=area, since=since, until=until, application=application)
|
|
574
|
+
base_query = _build_log_query(area=area, since=since, until=until, application=application, component=component)
|
|
561
575
|
base_filters = base_query.get("bool", {}).get("filter", [])
|
|
562
576
|
base_filters.append({"terms": {"level": ["error", "critical"]}})
|
|
563
577
|
if not include_missing:
|
|
@@ -623,6 +637,7 @@ def get_error_context(
|
|
|
623
637
|
before: int = 20,
|
|
624
638
|
after: int = 20,
|
|
625
639
|
application=None,
|
|
640
|
+
component=None,
|
|
626
641
|
):
|
|
627
642
|
"""Fetch logs around an anchor timestamp."""
|
|
628
643
|
before_count = max(int(before or 0), 0)
|
|
@@ -641,6 +656,7 @@ def get_error_context(
|
|
|
641
656
|
sort_order="desc",
|
|
642
657
|
until_inclusive=True,
|
|
643
658
|
application=application,
|
|
659
|
+
component=component,
|
|
644
660
|
)
|
|
645
661
|
after_docs, _ = search_logs_page(
|
|
646
662
|
client=client,
|
|
@@ -654,17 +670,20 @@ def get_error_context(
|
|
|
654
670
|
sort_order="asc",
|
|
655
671
|
since_inclusive=False,
|
|
656
672
|
application=application,
|
|
673
|
+
component=component,
|
|
657
674
|
)
|
|
658
675
|
before_docs = list(reversed(before_docs))
|
|
659
676
|
|
|
660
677
|
return before_docs + after_docs
|
|
661
678
|
|
|
662
679
|
|
|
663
|
-
def list_areas(client, index, since=None, min_operations=1, application=None):
|
|
680
|
+
def list_areas(client, index, since=None, min_operations=1, application=None, component=None):
|
|
664
681
|
"""List all application areas with activity counts."""
|
|
665
682
|
query_filters = []
|
|
666
683
|
if application:
|
|
667
684
|
query_filters.append({"term": {"application": application}})
|
|
685
|
+
if component:
|
|
686
|
+
query_filters.append({"term": {"component": component}})
|
|
668
687
|
if since:
|
|
669
688
|
normalized_since = resolve_relative_time(since)
|
|
670
689
|
query_filters.append({"range": {"timestamp": {"gte": normalized_since}}})
|
|
@@ -34,11 +34,12 @@ def _try_client() -> Tuple[Optional[object], Optional[str]]:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
@app.get("/api/search")
|
|
37
|
-
def search(q: Optional[str] = None, area: Optional[str] = None, level: Optional[str] = None, operation_id: Optional[str] = None, since: Optional[str] = None, limit: int = 50):
|
|
37
|
+
def search(q: Optional[str] = None, area: Optional[str] = None, component: Optional[str] = None, level: Optional[str] = None, operation_id: Optional[str] = None, since: Optional[str] = None, application: Optional[str] = None, limit: int = 50):
|
|
38
38
|
client, error = _try_client()
|
|
39
39
|
if not client:
|
|
40
40
|
return {"results": [], "error": error}
|
|
41
41
|
cfg = load_config()
|
|
42
|
+
effective_application = application or cfg.application
|
|
42
43
|
docs = search_logs(
|
|
43
44
|
client,
|
|
44
45
|
cfg.index,
|
|
@@ -48,16 +49,19 @@ def search(q: Optional[str] = None, area: Optional[str] = None, level: Optional[
|
|
|
48
49
|
level=level,
|
|
49
50
|
since=since,
|
|
50
51
|
limit=limit,
|
|
52
|
+
application=effective_application,
|
|
53
|
+
component=component,
|
|
51
54
|
)
|
|
52
55
|
results = normalize_log_entries(docs, limit=limit)
|
|
53
56
|
return {"results": results}
|
|
54
57
|
|
|
55
58
|
@app.get("/api/tail")
|
|
56
|
-
def tail(operation_id: Optional[str] = None, area: Optional[str] = None, level: Optional[str] = None, since: Optional[str] = None, limit: int = 20):
|
|
59
|
+
def tail(operation_id: Optional[str] = None, area: Optional[str] = None, component: Optional[str] = None, level: Optional[str] = None, since: Optional[str] = None, application: Optional[str] = None, limit: int = 20):
|
|
57
60
|
client, error = _try_client()
|
|
58
61
|
if not client:
|
|
59
62
|
return {"results": [], "error": error}
|
|
60
63
|
cfg = load_config()
|
|
64
|
+
effective_application = application or cfg.application
|
|
61
65
|
docs, cursor = tail_logs(
|
|
62
66
|
client,
|
|
63
67
|
cfg.index,
|
|
@@ -66,6 +70,8 @@ def tail(operation_id: Optional[str] = None, area: Optional[str] = None, level:
|
|
|
66
70
|
level=level,
|
|
67
71
|
since=since,
|
|
68
72
|
limit=limit,
|
|
73
|
+
application=effective_application,
|
|
74
|
+
component=component,
|
|
69
75
|
)
|
|
70
76
|
results = normalize_log_entries(docs, limit=limit)
|
|
71
77
|
return {"results": results, "cursor": cursor}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const elements = {
|
|
3
3
|
search: document.getElementById('search'),
|
|
4
4
|
area: document.getElementById('area'),
|
|
5
|
+
component: document.getElementById('component'),
|
|
5
6
|
operation: document.getElementById('operation'),
|
|
6
7
|
level: document.getElementById('level'),
|
|
7
8
|
limit: document.getElementById('limit'),
|
|
@@ -240,6 +241,7 @@ async function fetchLogs({ append = false } = {}) {
|
|
|
240
241
|
const query = elements.search.value.trim();
|
|
241
242
|
if (query) params.set('q', query);
|
|
242
243
|
if (elements.area.value.trim()) params.set('area', elements.area.value.trim());
|
|
244
|
+
if (elements.component.value.trim()) params.set('component', elements.component.value.trim());
|
|
243
245
|
if (elements.operation.value.trim()) params.set('operation_id', elements.operation.value.trim());
|
|
244
246
|
if (elements.level.value) params.set('level', elements.level.value);
|
|
245
247
|
if (elements.limit.value) params.set('limit', elements.limit.value);
|
|
@@ -322,6 +324,7 @@ function setFollow(enabled) {
|
|
|
322
324
|
|
|
323
325
|
elements.search.addEventListener('input', () => fetchLogs());
|
|
324
326
|
elements.area.addEventListener('input', () => fetchLogs());
|
|
327
|
+
elements.component.addEventListener('input', () => fetchLogs());
|
|
325
328
|
elements.operation.addEventListener('input', () => fetchLogs());
|
|
326
329
|
elements.level.addEventListener('change', () => fetchLogs());
|
|
327
330
|
elements.limit.addEventListener('change', () => fetchLogs());
|
|
@@ -23,6 +23,10 @@
|
|
|
23
23
|
<label for="area">Area</label>
|
|
24
24
|
<input id="area" placeholder="web, jobs" />
|
|
25
25
|
</div>
|
|
26
|
+
<div class="field">
|
|
27
|
+
<label for="component">Component</label>
|
|
28
|
+
<input id="component" placeholder="worker, api" />
|
|
29
|
+
</div>
|
|
26
30
|
<div class="field">
|
|
27
31
|
<label for="operation">Operation</label>
|
|
28
32
|
<input id="operation" placeholder="operation id" />
|
|
@@ -9,7 +9,7 @@ from devlogs.web import server
|
|
|
9
9
|
|
|
10
10
|
def _set_client_ready(monkeypatch, index_name="devlogs-test"):
|
|
11
11
|
monkeypatch.setattr(server, "_try_client", lambda: (object(), None))
|
|
12
|
-
monkeypatch.setattr(server, "load_config", lambda: SimpleNamespace(index=index_name))
|
|
12
|
+
monkeypatch.setattr(server, "load_config", lambda: SimpleNamespace(index=index_name, application=None))
|
|
13
13
|
|
|
14
14
|
def test_search_endpoint(monkeypatch):
|
|
15
15
|
_set_client_ready(monkeypatch)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|