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.
Files changed (73) hide show
  1. {devlogs-2.2.0/src/devlogs.egg-info → devlogs-2.2.1}/PKG-INFO +1 -1
  2. {devlogs-2.2.0 → devlogs-2.2.1}/pyproject.toml +1 -1
  3. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/cli.py +22 -4
  4. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/mcp/server.py +95 -2
  5. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/queries.py +30 -11
  6. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/server.py +8 -2
  7. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/static/devlogs.js +3 -0
  8. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/static/index.html +4 -0
  9. {devlogs-2.2.0 → devlogs-2.2.1/src/devlogs.egg-info}/PKG-INFO +1 -1
  10. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_web.py +1 -1
  11. {devlogs-2.2.0 → devlogs-2.2.1}/LICENSE +0 -0
  12. {devlogs-2.2.0 → devlogs-2.2.1}/MANIFEST.in +0 -0
  13. {devlogs-2.2.0 → devlogs-2.2.1}/README.md +0 -0
  14. {devlogs-2.2.0 → devlogs-2.2.1}/setup.cfg +0 -0
  15. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/__init__.py +0 -0
  16. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/__main__.py +0 -0
  17. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/build_info.py +0 -0
  18. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/__init__.py +0 -0
  19. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/auth.py +0 -0
  20. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/cli.py +0 -0
  21. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/errors.py +0 -0
  22. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/forwarder.py +0 -0
  23. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/ingestor.py +0 -0
  24. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/schema.py +0 -0
  25. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/collector/server.py +0 -0
  26. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/config.py +0 -0
  27. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/context.py +0 -0
  28. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/demo.py +0 -0
  29. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/devlogs_client.py +0 -0
  30. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/formatting.py +0 -0
  31. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/handler.py +0 -0
  32. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/jenkins/__init__.py +0 -0
  33. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/jenkins/cli.py +0 -0
  34. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/jenkins/core.py +0 -0
  35. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/levels.py +0 -0
  36. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/mcp/__init__.py +0 -0
  37. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/__init__.py +0 -0
  38. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/client.py +0 -0
  39. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/indexing.py +0 -0
  40. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/opensearch/mappings.py +0 -0
  41. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/retention.py +0 -0
  42. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/scrub.py +0 -0
  43. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/time_utils.py +0 -0
  44. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/version.py +0 -0
  45. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/__init__.py +0 -0
  46. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/web/static/devlogs.css +0 -0
  47. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs/wrapper.py +0 -0
  48. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/SOURCES.txt +0 -0
  49. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/dependency_links.txt +0 -0
  50. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/entry_points.txt +0 -0
  51. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/requires.txt +0 -0
  52. {devlogs-2.2.0 → devlogs-2.2.1}/src/devlogs.egg-info/top_level.txt +0 -0
  53. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_build_info.py +0 -0
  54. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_cli.py +0 -0
  55. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_auth.py +0 -0
  56. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_config.py +0 -0
  57. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_schema.py +0 -0
  58. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_collector_server.py +0 -0
  59. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_config.py +0 -0
  60. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_context.py +0 -0
  61. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_devlogs_client.py +0 -0
  62. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_formatting.py +0 -0
  63. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_handler.py +0 -0
  64. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_indexing.py +0 -0
  65. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_levels.py +0 -0
  66. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_mappings.py +0 -0
  67. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_mcp_server.py +0 -0
  68. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_opensearch_client.py +0 -0
  69. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_opensearch_queries.py +0 -0
  70. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_retention.py +0 -0
  71. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_scrub.py +0 -0
  72. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_time_utils.py +0 -0
  73. {devlogs-2.2.0 → devlogs-2.2.1}/tests/test_url_parsing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlogs
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Developer-focused logging library for Python with OpenSearch integration.
5
5
  Author-email: Dan Driscoll <dan@thedandriscoll.org>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devlogs"
7
- version = "2.2.0"
7
+ version = "2.2.1"
8
8
  description = "Developer-focused logging library for Python with OpenSearch integration."
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -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=cfg.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=cfg.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=cfg.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=cfg.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, application = _create_client_and_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" />
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlogs
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Developer-focused logging library for Python with OpenSearch integration.
5
5
  Author-email: Dan Driscoll <dan@thedandriscoll.org>
6
6
  License: MIT License
@@ -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