devlogs 2.0.3__tar.gz → 2.2.0__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 (74) hide show
  1. {devlogs-2.0.3/src/devlogs.egg-info → devlogs-2.2.0}/PKG-INFO +1 -1
  2. {devlogs-2.0.3 → devlogs-2.2.0}/pyproject.toml +1 -1
  3. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/cli.py +38 -161
  4. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/cli.py +2 -0
  5. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/forwarder.py +2 -4
  6. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/server.py +25 -12
  7. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/config.py +241 -15
  8. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/demo.py +67 -20
  9. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/devlogs_client.py +22 -7
  10. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/handler.py +58 -6
  11. devlogs-2.2.0/src/devlogs/jenkins/cli.py +102 -0
  12. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/mcp/server.py +64 -55
  13. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/queries.py +31 -12
  14. {devlogs-2.0.3 → devlogs-2.2.0/src/devlogs.egg-info}/PKG-INFO +1 -1
  15. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/SOURCES.txt +1 -0
  16. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_cli.py +16 -26
  17. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_server.py +24 -42
  18. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_config.py +17 -33
  19. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_devlogs_client.py +5 -5
  20. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_handler.py +79 -0
  21. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_mcp_server.py +43 -25
  22. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_opensearch_queries.py +111 -0
  23. devlogs-2.2.0/tests/test_url_parsing.py +276 -0
  24. devlogs-2.0.3/src/devlogs/jenkins/cli.py +0 -257
  25. {devlogs-2.0.3 → devlogs-2.2.0}/LICENSE +0 -0
  26. {devlogs-2.0.3 → devlogs-2.2.0}/MANIFEST.in +0 -0
  27. {devlogs-2.0.3 → devlogs-2.2.0}/README.md +0 -0
  28. {devlogs-2.0.3 → devlogs-2.2.0}/setup.cfg +0 -0
  29. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/__init__.py +0 -0
  30. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/__main__.py +0 -0
  31. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/build_info.py +0 -0
  32. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/__init__.py +0 -0
  33. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/auth.py +0 -0
  34. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/errors.py +0 -0
  35. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/ingestor.py +0 -0
  36. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/schema.py +0 -0
  37. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/context.py +0 -0
  38. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/formatting.py +0 -0
  39. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/jenkins/__init__.py +0 -0
  40. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/jenkins/core.py +0 -0
  41. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/levels.py +0 -0
  42. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/mcp/__init__.py +0 -0
  43. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/__init__.py +0 -0
  44. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/client.py +0 -0
  45. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/indexing.py +0 -0
  46. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/mappings.py +0 -0
  47. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/retention.py +0 -0
  48. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/scrub.py +0 -0
  49. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/time_utils.py +0 -0
  50. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/version.py +0 -0
  51. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/__init__.py +0 -0
  52. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/server.py +0 -0
  53. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/static/devlogs.css +0 -0
  54. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/static/devlogs.js +0 -0
  55. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/static/index.html +0 -0
  56. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/wrapper.py +0 -0
  57. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/dependency_links.txt +0 -0
  58. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/entry_points.txt +0 -0
  59. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/requires.txt +0 -0
  60. {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/top_level.txt +0 -0
  61. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_build_info.py +0 -0
  62. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_auth.py +0 -0
  63. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_config.py +0 -0
  64. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_schema.py +0 -0
  65. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_context.py +0 -0
  66. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_formatting.py +0 -0
  67. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_indexing.py +0 -0
  68. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_levels.py +0 -0
  69. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_mappings.py +0 -0
  70. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_opensearch_client.py +0 -0
  71. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_retention.py +0 -0
  72. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_scrub.py +0 -0
  73. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_time_utils.py +0 -0
  74. {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devlogs
3
- Version: 2.0.3
3
+ Version: 2.2.0
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.0.3"
7
+ version = "2.2.0"
8
8
  description = "Developer-focused logging library for Python with OpenSearch integration."
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -10,7 +10,7 @@ import click
10
10
  import typer
11
11
  from pathlib import Path
12
12
 
13
- from .config import load_config, set_dotenv_path, set_url, URLParseError, _parse_opensearch_url
13
+ from .config import load_config, set_dotenv_path, set_url, URLParseError, _parse_opensearch_url, parse_url, CollectorURLConfig
14
14
  from .formatting import format_timestamp
15
15
  from .opensearch.client import (
16
16
  get_opensearch_client,
@@ -39,7 +39,7 @@ OLD_TEMPLATE_NAMES = ("devlogs-template", "devlogs-logs-template")
39
39
 
40
40
  # Common options for commands - these can be placed anywhere in the command line
41
41
  ENV_OPTION = typer.Option(None, "--env", help="Path to .env file to load")
42
- URL_OPTION = typer.Option(None, "--url", help="OpenSearch URL (e.g., https://user:pass@host:port/index)")
42
+ URL_OPTION = typer.Option(None, "--url", help="URL (e.g., opensearchs://user:pass@host:port/index or https://TOKEN@host:port/path)")
43
43
 
44
44
 
45
45
  def _apply_common_options(env: str = None, url: str = None):
@@ -61,11 +61,17 @@ def main_callback(
61
61
  _apply_common_options(env, url)
62
62
 
63
63
 
64
+ _HIDDEN_FIELDS = {"run_id", "build_number", "build_url", "job", "node_id", "seq"}
65
+
66
+
64
67
  def _format_features(features):
65
68
  if not features:
66
69
  return ""
67
70
  if isinstance(features, dict):
68
- items = sorted(features.items(), key=lambda item: str(item[0]))
71
+ items = sorted(
72
+ ((k, v) for k, v in features.items() if k not in _HIDDEN_FIELDS),
73
+ key=lambda item: str(item[0]),
74
+ )
69
75
  parts = []
70
76
  for key, value in items:
71
77
  key_text = str(key)
@@ -722,6 +728,7 @@ def tail(
722
728
  since=since,
723
729
  limit=limit,
724
730
  search_after=search_after,
731
+ application=cfg.application,
725
732
  )
726
733
  _verbose_echo(f"Received {len(docs)} docs, next cursor={search_after}")
727
734
  if verbose and docs:
@@ -845,6 +852,7 @@ def search(
845
852
  since=since,
846
853
  limit=limit,
847
854
  search_after=search_after,
855
+ application=cfg.application,
848
856
  )
849
857
  else:
850
858
  docs = search_logs(
@@ -856,6 +864,7 @@ def search(
856
864
  level=level,
857
865
  since=since,
858
866
  limit=limit,
867
+ application=cfg.application,
859
868
  )
860
869
  entries = normalize_log_entries(docs, limit=limit)
861
870
  consecutive_errors = 0
@@ -941,6 +950,7 @@ def last_error(
941
950
  since=since,
942
951
  until=until,
943
952
  limit=limit,
953
+ application=cfg.application,
944
954
  )
945
955
  entries = normalize_log_entries(docs, limit=limit)
946
956
  except (ConnectionFailedError, urllib.error.URLError) as e:
@@ -1166,7 +1176,13 @@ def demo(
1166
1176
  """Generate demo logs to illustrate devlogs capabilities."""
1167
1177
  _apply_common_options(env, url)
1168
1178
  from .demo import run_demo
1169
- run_demo(duration, count, require_opensearch)
1179
+
1180
+ cfg = load_config()
1181
+ if cfg.url_mode == "collector":
1182
+ from .devlogs_client import DevlogsClient
1183
+ run_demo(duration, count, require_opensearch=None, collector_url=cfg.collector_url)
1184
+ else:
1185
+ run_demo(duration, count, require_opensearch=require_opensearch)
1170
1186
 
1171
1187
 
1172
1188
  @app.command()
@@ -1181,8 +1197,20 @@ def serve(
1181
1197
 
1182
1198
 
1183
1199
  def _build_opensearch_url(scheme: str, host: str, port: int, user: str, password: str, index: str) -> str:
1184
- """Build an OpenSearch URL from components, URL-encoding credentials."""
1200
+ """Build an OpenSearch URL from components, URL-encoding credentials.
1201
+
1202
+ Uses the opensearchs:// scheme (TLS) or opensearch:// (non-TLS).
1203
+ The scheme parameter accepts 'https'/'http' (mapped to opensearch schemes)
1204
+ or 'opensearchs'/'opensearch' directly.
1205
+ """
1185
1206
  from urllib.parse import quote
1207
+ # Map legacy schemes to opensearch:// variants
1208
+ if scheme == "https":
1209
+ url_scheme = "opensearchs"
1210
+ elif scheme == "http":
1211
+ url_scheme = "opensearch"
1212
+ else:
1213
+ url_scheme = scheme
1186
1214
  # URL-encode username and password to handle special characters
1187
1215
  encoded_user = quote(user, safe="") if user else ""
1188
1216
  encoded_pass = quote(password, safe="") if password else ""
@@ -1193,7 +1221,7 @@ def _build_opensearch_url(scheme: str, host: str, port: int, user: str, password
1193
1221
  else:
1194
1222
  auth = ""
1195
1223
  path = f"/{index}" if index else ""
1196
- return f"{scheme}://{auth}{host}:{port}{path}"
1224
+ return f"{url_scheme}://{auth}{host}:{port}{path}"
1197
1225
 
1198
1226
 
1199
1227
  def _format_env_output(scheme: str, host: str, port: int, user: str, password: str, index: str) -> str:
@@ -1211,155 +1239,6 @@ def _format_env_output(scheme: str, host: str, port: int, user: str, password: s
1211
1239
  return "\n".join(lines)
1212
1240
 
1213
1241
 
1214
- @app.command()
1215
- def initjenkins(
1216
- jenkinsfile: str = typer.Argument("Jenkinsfile", help="Path to Jenkinsfile to modify"),
1217
- credential_id: str = typer.Option("devlogs-opensearch-url", "--credential-id", "-c", help="Jenkins credential ID to use"),
1218
- env: str = ENV_OPTION,
1219
- url: str = URL_OPTION,
1220
- ):
1221
- """Add devlogs configuration to an existing Jenkinsfile.
1222
-
1223
- This command modifies an existing Jenkinsfile to add an options block
1224
- with the devlogs pipeline step configured to use a Jenkins credential.
1225
-
1226
- After running this command, you need to create a Jenkins credential
1227
- of type "Secret text" with the OpenSearch URL.
1228
-
1229
- Examples:
1230
- devlogs initjenkins # Modify ./Jenkinsfile
1231
- devlogs initjenkins path/to/Jenkinsfile # Modify specific file
1232
- devlogs initjenkins --credential-id my-cred # Use custom credential ID
1233
- """
1234
- import re
1235
-
1236
- _apply_common_options(env, url)
1237
-
1238
- jenkinsfile_path = Path(jenkinsfile)
1239
- if not jenkinsfile_path.is_file():
1240
- typer.echo(typer.style(f"Error: Jenkinsfile not found: {jenkinsfile_path}", fg=typer.colors.RED), err=True)
1241
- raise typer.Exit(1)
1242
-
1243
- content = jenkinsfile_path.read_text(encoding="utf-8")
1244
-
1245
- # Check if this looks like a declarative pipeline
1246
- if "pipeline" not in content:
1247
- typer.echo(typer.style("Error: File does not appear to be a declarative Jenkins pipeline.", fg=typer.colors.RED), err=True)
1248
- typer.echo("This command only supports declarative pipelines with a 'pipeline { }' block.", err=True)
1249
- raise typer.Exit(1)
1250
-
1251
- # Check if devlogs is already configured
1252
- if "devlogs(" in content:
1253
- typer.echo(typer.style("Warning: Jenkinsfile already appears to have devlogs configuration.", fg=typer.colors.YELLOW))
1254
- typer.echo("Review the file manually to ensure correct configuration.")
1255
- raise typer.Exit(0)
1256
-
1257
- options_line = f" devlogs(credentialsId: '{credential_id}')"
1258
-
1259
- modified = False
1260
- lines = content.split("\n")
1261
- result_lines = []
1262
- i = 0
1263
-
1264
- # Track brace depth and whether we're inside pipeline block
1265
- in_pipeline = False
1266
- pipeline_brace_depth = 0
1267
- added_options = False
1268
-
1269
- while i < len(lines):
1270
- line = lines[i]
1271
- result_lines.append(line)
1272
-
1273
- # Detect entering pipeline block
1274
- if not in_pipeline and re.match(r'^\s*pipeline\s*\{', line):
1275
- in_pipeline = True
1276
- pipeline_brace_depth = 1
1277
- i += 1
1278
- continue
1279
-
1280
- if in_pipeline:
1281
- # Count braces to track depth
1282
- pipeline_brace_depth += line.count('{') - line.count('}')
1283
-
1284
- # Check for existing options block and add our line
1285
- if not added_options and re.match(r'^\s*options\s*\{', line):
1286
- result_lines.append(options_line)
1287
- added_options = True
1288
- modified = True
1289
-
1290
- # If we hit stages and haven't added options, add it before stages
1291
- if re.match(r'^\s*stages\s*\{', line) and not added_options:
1292
- # Insert before the stages line
1293
- result_lines.pop() # Remove the stages line we just added
1294
-
1295
- result_lines.append("")
1296
- result_lines.append(" options {")
1297
- result_lines.append(options_line)
1298
- result_lines.append(" }")
1299
- added_options = True
1300
- modified = True
1301
-
1302
- result_lines.append("")
1303
- result_lines.append(line) # Re-add the stages line
1304
-
1305
- # Exit pipeline tracking when we close the pipeline block
1306
- if pipeline_brace_depth == 0:
1307
- in_pipeline = False
1308
-
1309
- i += 1
1310
-
1311
- if not modified:
1312
- typer.echo(typer.style("Error: Could not find a suitable location to add devlogs configuration.", fg=typer.colors.RED), err=True)
1313
- typer.echo("Ensure the Jenkinsfile has a 'pipeline { stages { } }' structure.", err=True)
1314
- raise typer.Exit(1)
1315
-
1316
- # Write the modified file
1317
- new_content = "\n".join(result_lines)
1318
- jenkinsfile_path.write_text(new_content, encoding="utf-8")
1319
-
1320
- typer.echo(typer.style(f"Modified {jenkinsfile_path}", fg=typer.colors.GREEN))
1321
- typer.echo()
1322
- typer.echo("Added:")
1323
- typer.echo(f" - Options: devlogs(credentialsId: '{credential_id}')")
1324
-
1325
- # Print setup instructions
1326
- typer.echo()
1327
- typer.echo(typer.style("Next steps - Create Jenkins credential:", fg=typer.colors.CYAN, bold=True))
1328
- typer.echo("=" * 60)
1329
- typer.echo()
1330
- typer.echo("1. Go to Jenkins > Manage Jenkins > Credentials")
1331
- typer.echo("2. Select the appropriate domain (e.g., Global)")
1332
- typer.echo("3. Click 'Add Credentials'")
1333
- typer.echo("4. Configure:")
1334
- typer.echo(f" - Kind: Secret text")
1335
- typer.echo(f" - ID: {credential_id}")
1336
- typer.echo(" - Secret: <your OpenSearch URL>")
1337
- typer.echo()
1338
-
1339
- # Try to load config and show the URL value
1340
- try:
1341
- cfg = load_config()
1342
- if cfg.enabled:
1343
- # Build the URL from config
1344
- credential_url = _build_opensearch_url(
1345
- cfg.opensearch_scheme,
1346
- cfg.opensearch_host,
1347
- cfg.opensearch_port,
1348
- cfg.opensearch_user,
1349
- cfg.opensearch_pass,
1350
- cfg.index,
1351
- )
1352
- typer.echo(typer.style("Credential value (from your .env/environment):", fg=typer.colors.GREEN, bold=True))
1353
- typer.echo("-" * 60)
1354
- typer.echo(credential_url)
1355
- typer.echo("-" * 60)
1356
- else:
1357
- typer.echo("Tip: Set up a .env file with DEVLOGS_OPENSEARCH_URL to see the exact value here.")
1358
- typer.echo(" Run 'devlogs mkurl' to interactively build the URL.")
1359
- except Exception:
1360
- typer.echo("Tip: Run 'devlogs mkurl' to interactively build the OpenSearch URL.")
1361
-
1362
-
1363
1242
  @app.command()
1364
1243
  def mkurl():
1365
1244
  """Interactively create an OpenSearch URL and show .env formats.
@@ -1369,7 +1248,7 @@ def mkurl():
1369
1248
  the components (host, port, credentials, index) one by one.
1370
1249
 
1371
1250
  The output shows three equivalent formats:
1372
- - A bare URL (for --url flag or DEVLOGS_OPENSEARCH_URL)
1251
+ - A bare URL (for --url flag or DEVLOGS_URL)
1373
1252
  - The URL as a single .env variable
1374
1253
  - Individual .env variables for each component
1375
1254
  """
@@ -1410,10 +1289,8 @@ def mkurl():
1410
1289
  else:
1411
1290
  # Prompt for each component
1412
1291
  typer.echo()
1413
- scheme = typer.prompt("Scheme (http/https)", default="https")
1414
- if scheme not in ("http", "https"):
1415
- typer.echo(typer.style("Error: Scheme must be 'http' or 'https'.", fg=typer.colors.RED), err=True)
1416
- raise typer.Exit(1)
1292
+ use_tls = typer.prompt("Use TLS? (yes/no)", default="yes")
1293
+ scheme = "https" if use_tls.lower() in ("yes", "y") else "http"
1417
1294
  host = typer.prompt("Host", default="localhost")
1418
1295
  default_port = 443 if scheme == "https" else 9200
1419
1296
  port = int(typer.prompt("Port", default=str(default_port)))
@@ -1441,7 +1318,7 @@ def mkurl():
1441
1318
  typer.echo()
1442
1319
  typer.echo(typer.style("2. Single .env variable:", fg=typer.colors.CYAN, bold=True))
1443
1320
  typer.echo("-" * 50)
1444
- typer.echo(f"DEVLOGS_OPENSEARCH_URL={url}")
1321
+ typer.echo(f"DEVLOGS_URL={url}")
1445
1322
 
1446
1323
  # Format 3: Individual .env variables
1447
1324
  typer.echo()
@@ -75,6 +75,8 @@ def serve(
75
75
  typer.echo(f" OpenSearch: {cfg.opensearch_host}:{cfg.opensearch_port}")
76
76
  typer.echo(f" Index: {cfg.index}")
77
77
 
78
+ from ..version import __version__
79
+ typer.echo(f" Version: {__version__}")
78
80
  typer.echo(f" Listening on: {host}:{port}")
79
81
  typer.echo()
80
82
 
@@ -24,7 +24,7 @@ def forward_request(
24
24
  Forwards the request body as-is, preserving relevant headers.
25
25
 
26
26
  Args:
27
- forward_url: The upstream URL to forward to (should end with /v1/logs)
27
+ forward_url: The upstream URL to forward to
28
28
  body: The raw request body bytes
29
29
  content_type: The Content-Type header value
30
30
  auth_header: Optional Authorization header to forward
@@ -37,9 +37,7 @@ def forward_request(
37
37
  Raises:
38
38
  ForwardError: If the forward request fails
39
39
  """
40
- # Ensure URL ends with /v1/logs
41
- if not forward_url.endswith("/v1/logs"):
42
- forward_url = forward_url.rstrip("/") + "/v1/logs"
40
+ forward_url = forward_url.rstrip("/")
43
41
 
44
42
  headers = {
45
43
  "Content-Type": content_type,
@@ -72,6 +72,30 @@ async def lifespan(app: FastAPI):
72
72
 
73
73
  yield
74
74
 
75
+ # Emit shutdown trace
76
+ if mode == "ingest":
77
+ try:
78
+ client = get_opensearch_client()
79
+ doc = DevlogsRecord(
80
+ application="devlogs-collector",
81
+ component="lifecycle",
82
+ timestamp=get_current_timestamp(),
83
+ message="Collector stopped",
84
+ level="info",
85
+ area="shutdown",
86
+ version=__version__,
87
+ fields={
88
+ "mode": mode,
89
+ "host": platform.node(),
90
+ },
91
+ )
92
+ doc.collected_ts = get_current_timestamp()
93
+ doc.client_ip = "127.0.0.1"
94
+ doc._identity = {"mode": "internal"}
95
+ client.index(index=cfg.index, body=doc.to_dict())
96
+ except Exception:
97
+ pass
98
+
75
99
 
76
100
  # Create FastAPI app for collector
77
101
  app = FastAPI(
@@ -115,19 +139,8 @@ async def collector_error_handler(request: Request, exc: CollectorError):
115
139
  )
116
140
 
117
141
 
118
- @app.get("/health")
119
- async def health():
120
- """Health check endpoint."""
121
- cfg = load_config()
122
- mode = cfg.get_collector_mode()
123
- return {
124
- "status": "healthy",
125
- "mode": mode,
126
- "version": __version__,
127
- }
128
-
129
142
 
130
- @app.post("/v1/logs")
143
+ @app.post("/")
131
144
  async def ingest_logs(request: Request):
132
145
  """Ingest log records.
133
146