devlogs 2.2.0__tar.gz → 2.2.2__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.2}/PKG-INFO +1 -1
- {devlogs-2.2.0 → devlogs-2.2.2}/pyproject.toml +1 -1
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/cli.py +37 -8
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/schema.py +5 -0
- devlogs-2.2.2/src/devlogs/formatting.py +71 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/mcp/server.py +95 -2
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/opensearch/queries.py +30 -11
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/web/server.py +8 -2
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/web/static/devlogs.js +3 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/web/static/index.html +4 -0
- {devlogs-2.2.0 → devlogs-2.2.2/src/devlogs.egg-info}/PKG-INFO +1 -1
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_web.py +1 -1
- devlogs-2.2.0/src/devlogs/formatting.py +0 -34
- {devlogs-2.2.0 → devlogs-2.2.2}/LICENSE +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/MANIFEST.in +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/README.md +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/setup.cfg +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/__main__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/build_info.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/auth.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/cli.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/errors.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/forwarder.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/ingestor.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/collector/server.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/config.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/context.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/demo.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/devlogs_client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/handler.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/jenkins/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/jenkins/cli.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/jenkins/core.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/levels.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/mcp/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/opensearch/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/opensearch/client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/opensearch/indexing.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/opensearch/mappings.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/retention.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/scrub.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/time_utils.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/version.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/web/__init__.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/web/static/devlogs.css +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs/wrapper.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs.egg-info/SOURCES.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs.egg-info/dependency_links.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs.egg-info/entry_points.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs.egg-info/requires.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/src/devlogs.egg-info/top_level.txt +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_build_info.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_cli.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_collector_auth.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_collector_config.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_collector_schema.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_collector_server.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_config.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_context.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_devlogs_client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_formatting.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_handler.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_indexing.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_levels.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_mappings.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_mcp_server.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_opensearch_client.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_opensearch_queries.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_retention.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_scrub.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_time_utils.py +0 -0
- {devlogs-2.2.0 → devlogs-2.2.2}/tests/test_url_parsing.py +0 -0
|
@@ -11,7 +11,7 @@ import typer
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from .config import load_config, set_dotenv_path, set_url, URLParseError, _parse_opensearch_url, parse_url, CollectorURLConfig
|
|
14
|
-
from .formatting import format_timestamp
|
|
14
|
+
from .formatting import format_timestamp, all_entries_today
|
|
15
15
|
from .opensearch.client import (
|
|
16
16
|
get_opensearch_client,
|
|
17
17
|
check_connection,
|
|
@@ -31,6 +31,7 @@ from .opensearch.mappings import (
|
|
|
31
31
|
from .opensearch.queries import normalize_log_entries, search_logs, tail_logs, get_last_errors
|
|
32
32
|
from .retention import cleanup_old_logs, get_retention_stats
|
|
33
33
|
from .jenkins.cli import jenkins_app
|
|
34
|
+
from .version import __version__
|
|
34
35
|
|
|
35
36
|
app = typer.Typer()
|
|
36
37
|
app.add_typer(jenkins_app, name="jenkins")
|
|
@@ -50,10 +51,17 @@ def _apply_common_options(env: str = None, url: str = None):
|
|
|
50
51
|
set_url(url)
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def _version_callback(value: bool):
|
|
55
|
+
if value:
|
|
56
|
+
typer.echo(f"devlogs {__version__}")
|
|
57
|
+
raise typer.Exit()
|
|
58
|
+
|
|
59
|
+
|
|
53
60
|
# Global callback to handle --env flag before any command runs (for backwards compatibility)
|
|
54
61
|
@app.callback(invoke_without_command=True)
|
|
55
62
|
def main_callback(
|
|
56
63
|
ctx: typer.Context,
|
|
64
|
+
version: bool = typer.Option(False, "--version", callback=_version_callback, is_eager=True, help="Show version and exit"),
|
|
57
65
|
env: str = ENV_OPTION,
|
|
58
66
|
url: str = URL_OPTION,
|
|
59
67
|
):
|
|
@@ -650,8 +658,10 @@ def diagnose(
|
|
|
650
658
|
def tail(
|
|
651
659
|
operation_id: str = typer.Option(None, "--operation", "-o"),
|
|
652
660
|
area: str = typer.Option(None, "--area"),
|
|
661
|
+
component: str = typer.Option(None, "--component", "-c", help="Filter by component name"),
|
|
653
662
|
level: str = typer.Option(None, "--level"),
|
|
654
663
|
since: str = typer.Option(None, "--since"),
|
|
664
|
+
application: str = typer.Option(None, "--application", "-a", help="Filter by application name"),
|
|
655
665
|
limit: int = typer.Option(20, "--limit"),
|
|
656
666
|
follow: bool = typer.Option(False, "--follow", "-f"),
|
|
657
667
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
|
|
@@ -698,12 +708,18 @@ def tail(
|
|
|
698
708
|
color=typer.colors.YELLOW,
|
|
699
709
|
)
|
|
700
710
|
|
|
711
|
+
effective_application = application or cfg.application
|
|
712
|
+
|
|
701
713
|
if verbose:
|
|
702
714
|
parts = []
|
|
715
|
+
if effective_application:
|
|
716
|
+
parts.append(f"application={effective_application}")
|
|
703
717
|
if operation_id:
|
|
704
718
|
parts.append(f"operation={operation_id}")
|
|
705
719
|
if area:
|
|
706
720
|
parts.append(f"area={area}")
|
|
721
|
+
if component:
|
|
722
|
+
parts.append(f"component={component}")
|
|
707
723
|
if level:
|
|
708
724
|
parts.append(f"level={level}")
|
|
709
725
|
if since:
|
|
@@ -728,7 +744,8 @@ def tail(
|
|
|
728
744
|
since=since,
|
|
729
745
|
limit=limit,
|
|
730
746
|
search_after=search_after,
|
|
731
|
-
application=
|
|
747
|
+
application=effective_application,
|
|
748
|
+
component=component,
|
|
732
749
|
)
|
|
733
750
|
_verbose_echo(f"Received {len(docs)} docs, next cursor={search_after}")
|
|
734
751
|
if verbose and docs:
|
|
@@ -791,9 +808,10 @@ def tail(
|
|
|
791
808
|
typer.echo(typer.style("No logs found.", dim=True), err=True)
|
|
792
809
|
first_poll = False
|
|
793
810
|
|
|
811
|
+
omit_date = all_entries_today(entries, use_utc=utc)
|
|
794
812
|
for entry_index, doc in enumerate(entries):
|
|
795
813
|
try:
|
|
796
|
-
timestamp = format_timestamp(doc.get("timestamp") or "", use_utc=utc)
|
|
814
|
+
timestamp = format_timestamp(doc.get("timestamp") or "", use_utc=utc, omit_date=omit_date)
|
|
797
815
|
entry_level = doc.get("level") or ""
|
|
798
816
|
entry_area = doc.get("area") or ""
|
|
799
817
|
entry_operation = doc.get("operation_id") or ""
|
|
@@ -820,9 +838,11 @@ def tail(
|
|
|
820
838
|
def search(
|
|
821
839
|
q: str = typer.Option("", "--q", help="Search query"),
|
|
822
840
|
area: str = typer.Option(None, "--area"),
|
|
841
|
+
component: str = typer.Option(None, "--component", "-c", help="Filter by component name"),
|
|
823
842
|
level: str = typer.Option(None, "--level"),
|
|
824
843
|
operation_id: str = typer.Option(None, "--operation", "-o"),
|
|
825
844
|
since: str = typer.Option(None, "--since"),
|
|
845
|
+
application: str = typer.Option(None, "--application", "-a", help="Filter by application name"),
|
|
826
846
|
limit: int = typer.Option(50, "--limit"),
|
|
827
847
|
follow: bool = typer.Option(False, "--follow", "-f"),
|
|
828
848
|
utc: bool = typer.Option(False, "--utc", help="Display timestamps in UTC instead of local time"),
|
|
@@ -834,6 +854,7 @@ def search(
|
|
|
834
854
|
|
|
835
855
|
_apply_common_options(env, url)
|
|
836
856
|
client, cfg = require_opensearch()
|
|
857
|
+
effective_application = application or cfg.application
|
|
837
858
|
search_after = None
|
|
838
859
|
consecutive_errors = 0
|
|
839
860
|
max_errors = 3
|
|
@@ -852,7 +873,8 @@ def search(
|
|
|
852
873
|
since=since,
|
|
853
874
|
limit=limit,
|
|
854
875
|
search_after=search_after,
|
|
855
|
-
application=
|
|
876
|
+
application=effective_application,
|
|
877
|
+
component=component,
|
|
856
878
|
)
|
|
857
879
|
else:
|
|
858
880
|
docs = search_logs(
|
|
@@ -864,7 +886,8 @@ def search(
|
|
|
864
886
|
level=level,
|
|
865
887
|
since=since,
|
|
866
888
|
limit=limit,
|
|
867
|
-
application=
|
|
889
|
+
application=effective_application,
|
|
890
|
+
component=component,
|
|
868
891
|
)
|
|
869
892
|
entries = normalize_log_entries(docs, limit=limit)
|
|
870
893
|
consecutive_errors = 0
|
|
@@ -905,8 +928,9 @@ def search(
|
|
|
905
928
|
typer.echo(typer.style("No logs found.", dim=True), err=True)
|
|
906
929
|
first_poll = False
|
|
907
930
|
|
|
931
|
+
omit_date = all_entries_today(entries, use_utc=utc)
|
|
908
932
|
for doc in entries:
|
|
909
|
-
timestamp = format_timestamp(doc.get("timestamp") or "", use_utc=utc)
|
|
933
|
+
timestamp = format_timestamp(doc.get("timestamp") or "", use_utc=utc, omit_date=omit_date)
|
|
910
934
|
entry_level = doc.get("level") or ""
|
|
911
935
|
entry_area = doc.get("area") or ""
|
|
912
936
|
entry_operation = doc.get("operation_id") or ""
|
|
@@ -926,9 +950,11 @@ def search(
|
|
|
926
950
|
def last_error(
|
|
927
951
|
q: str = typer.Option("", "--q", help="Search query"),
|
|
928
952
|
area: str = typer.Option(None, "--area"),
|
|
953
|
+
component: str = typer.Option(None, "--component", "-c", help="Filter by component name"),
|
|
929
954
|
operation_id: str = typer.Option(None, "--operation", "-o"),
|
|
930
955
|
since: str = typer.Option(None, "--since"),
|
|
931
956
|
until: str = typer.Option(None, "--until"),
|
|
957
|
+
application: str = typer.Option(None, "--application", "-a", help="Filter by application name"),
|
|
932
958
|
limit: int = typer.Option(1, "--limit"),
|
|
933
959
|
utc: bool = typer.Option(False, "--utc", help="Display timestamps in UTC instead of local time"),
|
|
934
960
|
env: str = ENV_OPTION,
|
|
@@ -939,6 +965,7 @@ def last_error(
|
|
|
939
965
|
|
|
940
966
|
_apply_common_options(env, url)
|
|
941
967
|
client, cfg = require_opensearch()
|
|
968
|
+
effective_application = application or cfg.application
|
|
942
969
|
|
|
943
970
|
try:
|
|
944
971
|
docs = get_last_errors(
|
|
@@ -950,7 +977,8 @@ def last_error(
|
|
|
950
977
|
since=since,
|
|
951
978
|
until=until,
|
|
952
979
|
limit=limit,
|
|
953
|
-
application=
|
|
980
|
+
application=effective_application,
|
|
981
|
+
component=component,
|
|
954
982
|
)
|
|
955
983
|
entries = normalize_log_entries(docs, limit=limit)
|
|
956
984
|
except (ConnectionFailedError, urllib.error.URLError) as e:
|
|
@@ -982,8 +1010,9 @@ def last_error(
|
|
|
982
1010
|
typer.echo(typer.style("No errors found.", dim=True), err=True)
|
|
983
1011
|
return
|
|
984
1012
|
|
|
1013
|
+
omit_date = all_entries_today(entries, use_utc=utc)
|
|
985
1014
|
for doc in entries:
|
|
986
|
-
timestamp = format_timestamp(doc.get("timestamp") or "", use_utc=utc)
|
|
1015
|
+
timestamp = format_timestamp(doc.get("timestamp") or "", use_utc=utc, omit_date=omit_date)
|
|
987
1016
|
entry_level = doc.get("level") or ""
|
|
988
1017
|
entry_area = doc.get("area") or ""
|
|
989
1018
|
entry_operation = doc.get("operation_id") or ""
|
|
@@ -73,6 +73,7 @@ class DevlogsRecord:
|
|
|
73
73
|
message: Optional[str] = None
|
|
74
74
|
level: Optional[str] = None
|
|
75
75
|
area: Optional[str] = None
|
|
76
|
+
operation_id: Optional[str] = None
|
|
76
77
|
environment: Optional[str] = None
|
|
77
78
|
version: Optional[str] = None
|
|
78
79
|
|
|
@@ -122,6 +123,8 @@ class DevlogsRecord:
|
|
|
122
123
|
doc["level"] = self.level
|
|
123
124
|
if self.area is not None:
|
|
124
125
|
doc["area"] = self.area
|
|
126
|
+
if self.operation_id is not None:
|
|
127
|
+
doc["operation_id"] = self.operation_id
|
|
125
128
|
if self.environment is not None:
|
|
126
129
|
doc["environment"] = self.environment
|
|
127
130
|
if self.version is not None:
|
|
@@ -246,6 +249,7 @@ def validate_record(data: Dict[str, Any]) -> DevlogsRecord:
|
|
|
246
249
|
message = validate_string(data.get("message"), "message", required=False)
|
|
247
250
|
level = validate_string(data.get("level"), "level", required=False)
|
|
248
251
|
area = validate_string(data.get("area"), "area", required=False)
|
|
252
|
+
operation_id = validate_string(data.get("operation_id"), "operation_id", required=False)
|
|
249
253
|
environment = validate_string(data.get("environment"), "environment", required=False)
|
|
250
254
|
version = validate_string(data.get("version"), "version", required=False)
|
|
251
255
|
|
|
@@ -259,6 +263,7 @@ def validate_record(data: Dict[str, Any]) -> DevlogsRecord:
|
|
|
259
263
|
message=message,
|
|
260
264
|
level=level,
|
|
261
265
|
area=area,
|
|
266
|
+
operation_id=operation_id,
|
|
262
267
|
environment=environment,
|
|
263
268
|
version=version,
|
|
264
269
|
fields=fields,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Formatting utilities for devlogs output
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Iterable, Dict, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_timestamp(timestamp_str: str | None, use_utc: bool = False, omit_date: bool = False) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Format a timestamp string for display.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
timestamp_str: ISO 8601 timestamp string (typically UTC with Z suffix) or None
|
|
13
|
+
use_utc: If True, display in UTC; if False, display in local time
|
|
14
|
+
omit_date: If True, show only time portion (HH:MM:SS.mmm)
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Formatted timestamp string
|
|
18
|
+
"""
|
|
19
|
+
if not timestamp_str:
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# Parse the ISO timestamp (handles Z suffix and +00:00 format)
|
|
24
|
+
dt = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
25
|
+
|
|
26
|
+
if use_utc:
|
|
27
|
+
dt_display = dt.astimezone(timezone.utc)
|
|
28
|
+
if omit_date:
|
|
29
|
+
return dt_display.strftime("%H:%M:%S.%f")[:-3] + "Z"
|
|
30
|
+
return dt_display.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
31
|
+
else:
|
|
32
|
+
dt_display = dt.astimezone()
|
|
33
|
+
if omit_date:
|
|
34
|
+
return dt_display.strftime("%H:%M:%S.%f")[:-3]
|
|
35
|
+
return dt_display.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
|
|
36
|
+
except (ValueError, AttributeError):
|
|
37
|
+
# If parsing fails, return original string
|
|
38
|
+
return timestamp_str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def all_entries_today(entries: Iterable[Dict[str, Any]], use_utc: bool = False) -> bool:
|
|
42
|
+
"""Check if all entries have timestamps from today.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
entries: Log entries with 'timestamp' keys
|
|
46
|
+
use_utc: If True, compare against UTC date; otherwise local date
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if all entries are from today (or have no timestamp)
|
|
50
|
+
"""
|
|
51
|
+
if use_utc:
|
|
52
|
+
today = datetime.now(timezone.utc).date()
|
|
53
|
+
else:
|
|
54
|
+
today = datetime.now().astimezone().date()
|
|
55
|
+
has_any = False
|
|
56
|
+
for entry in entries:
|
|
57
|
+
ts = entry.get("timestamp")
|
|
58
|
+
if not ts:
|
|
59
|
+
continue
|
|
60
|
+
has_any = True
|
|
61
|
+
try:
|
|
62
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
63
|
+
if use_utc:
|
|
64
|
+
entry_date = dt.astimezone(timezone.utc).date()
|
|
65
|
+
else:
|
|
66
|
+
entry_date = dt.astimezone().date()
|
|
67
|
+
if entry_date != today:
|
|
68
|
+
return False
|
|
69
|
+
except (ValueError, AttributeError):
|
|
70
|
+
return False
|
|
71
|
+
return has_any
|
|
@@ -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)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# Formatting utilities for devlogs output
|
|
2
|
-
|
|
3
|
-
from datetime import datetime, timezone
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def format_timestamp(timestamp_str: str | None, use_utc: bool = False) -> str:
|
|
7
|
-
"""
|
|
8
|
-
Format a timestamp string for display.
|
|
9
|
-
|
|
10
|
-
Args:
|
|
11
|
-
timestamp_str: ISO 8601 timestamp string (typically UTC with Z suffix) or None
|
|
12
|
-
use_utc: If True, display in UTC; if False, display in local time
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
Formatted timestamp string
|
|
16
|
-
"""
|
|
17
|
-
if not timestamp_str:
|
|
18
|
-
return ""
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
# Parse the ISO timestamp (handles Z suffix and +00:00 format)
|
|
22
|
-
dt = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
|
|
23
|
-
|
|
24
|
-
if use_utc:
|
|
25
|
-
# Keep in UTC, format as ISO-like string
|
|
26
|
-
dt_utc = dt.astimezone(timezone.utc)
|
|
27
|
-
return dt_utc.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
28
|
-
else:
|
|
29
|
-
# Convert to local time
|
|
30
|
-
dt_local = dt.astimezone()
|
|
31
|
-
return dt_local.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
|
|
32
|
-
except (ValueError, AttributeError):
|
|
33
|
-
# If parsing fails, return original string
|
|
34
|
-
return timestamp_str
|
|
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
|