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.
- {devlogs-2.0.3/src/devlogs.egg-info → devlogs-2.2.0}/PKG-INFO +1 -1
- {devlogs-2.0.3 → devlogs-2.2.0}/pyproject.toml +1 -1
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/cli.py +38 -161
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/cli.py +2 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/forwarder.py +2 -4
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/server.py +25 -12
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/config.py +241 -15
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/demo.py +67 -20
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/devlogs_client.py +22 -7
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/handler.py +58 -6
- devlogs-2.2.0/src/devlogs/jenkins/cli.py +102 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/mcp/server.py +64 -55
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/queries.py +31 -12
- {devlogs-2.0.3 → devlogs-2.2.0/src/devlogs.egg-info}/PKG-INFO +1 -1
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/SOURCES.txt +1 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_cli.py +16 -26
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_server.py +24 -42
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_config.py +17 -33
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_devlogs_client.py +5 -5
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_handler.py +79 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_mcp_server.py +43 -25
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_opensearch_queries.py +111 -0
- devlogs-2.2.0/tests/test_url_parsing.py +276 -0
- devlogs-2.0.3/src/devlogs/jenkins/cli.py +0 -257
- {devlogs-2.0.3 → devlogs-2.2.0}/LICENSE +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/MANIFEST.in +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/README.md +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/setup.cfg +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/__init__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/__main__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/build_info.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/__init__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/auth.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/errors.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/ingestor.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/collector/schema.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/context.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/formatting.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/jenkins/__init__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/jenkins/core.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/levels.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/mcp/__init__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/__init__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/client.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/indexing.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/opensearch/mappings.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/retention.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/scrub.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/time_utils.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/version.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/__init__.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/server.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/static/devlogs.css +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/static/devlogs.js +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/web/static/index.html +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs/wrapper.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/dependency_links.txt +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/entry_points.txt +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/requires.txt +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/src/devlogs.egg-info/top_level.txt +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_build_info.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_auth.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_config.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_collector_schema.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_context.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_formatting.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_indexing.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_levels.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_mappings.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_opensearch_client.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_retention.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_scrub.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_time_utils.py +0 -0
- {devlogs-2.0.3 → devlogs-2.2.0}/tests/test_web.py +0 -0
|
@@ -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="
|
|
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(
|
|
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
|
-
|
|
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"{
|
|
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
|
|
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
|
-
|
|
1414
|
-
if
|
|
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"
|
|
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
|
|
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
|
-
|
|
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("/
|
|
143
|
+
@app.post("/")
|
|
131
144
|
async def ingest_logs(request: Request):
|
|
132
145
|
"""Ingest log records.
|
|
133
146
|
|