Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod config;
pub mod connector;
pub mod discovery;
pub mod legacy;
pub mod logging;
pub mod messaging;
pub mod protocol;
pub mod registry;
Expand Down
36 changes: 36 additions & 0 deletions backend/src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
pub fn resolve_log_format() -> Result<&'static str, String> {
resolve_log_format_from(std::env::var("TOT_LOG_FORMAT").as_deref().ok())
}

pub fn resolve_log_format_from(value: Option<&str>) -> Result<&'static str, String> {
match value {
None | Some("") | Some("text") => Ok("text"),
Some("json") => Ok("json"),
Some(other) => Err(format!("invalid TOT_LOG_FORMAT value: {}", other)),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn defaults_to_text_when_unset() {
assert_eq!(resolve_log_format_from(None).unwrap(), "text");
}

#[test]
fn accepts_text() {
assert_eq!(resolve_log_format_from(Some("text")).unwrap(), "text");
}

#[test]
fn accepts_json() {
assert_eq!(resolve_log_format_from(Some("json")).unwrap(), "json");
}

#[test]
fn rejects_invalid_value() {
assert!(resolve_log_format_from(Some("xml")).is_err());
}
}
26 changes: 22 additions & 4 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
use clap::Parser;
use tent_backend::discovery::ServiceDiscovery;
use tent_backend::logging::resolve_log_format;
use tent_backend::messaging::MessageBroker;
use tent_backend::registry::ServiceRegistry;
use tracing_subscriber::EnvFilter;
Expand Down Expand Up @@ -28,14 +29,23 @@ struct Cli {
// It's 30 lines of config loading and then it spawns a server.
// Actually it's like 50 lines. Still too fucking many.
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.json()
.init();
let log_format = resolve_log_format().map_err(anyhow::Error::msg)?;

if log_format == "json" {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.json()
.init();
} else {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.init();
}

let cli = Cli::parse();

tracing::info!(
log_format = %log_format,
node_id = %cli.node_id,
consensus = %cli.consensus,
max_connections = %cli.max_connections,
Expand All @@ -54,10 +64,12 @@ async fn main() -> Result<()> {

tracing::info!("all subsystems initialized successfully, entering main loop");

#[cfg(unix)]
let mut signal = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::terminate(),
)?;

#[cfg(unix)]
tokio::select! {
_ = signal.recv() => {
tracing::info!("received SIGTERM, initiating graceful shutdown");
Expand All @@ -67,6 +79,12 @@ async fn main() -> Result<()> {
}
}

#[cfg(not(unix))]
{
let _ = tokio::signal::ctrl_c().await;
tracing::info!("received SIGINT, initiating graceful shutdown");
}

broker.disconnect().await?;
discovery.withdraw(&cli.node_id).await?;
registry.shutdown().await?;
Expand Down
2 changes: 1 addition & 1 deletion backend/src/protocol/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use std::io::{Cursor, Read, Write};
pub const FRAME_MAGIC: u32 = 0x544F5446;

/// Size of the frame header in bytes.
pub const FRAME_HEADER_SIZE: usize = 24;
pub const FRAME_HEADER_SIZE: usize = 20;

/// Maximum frame payload size (16 MB).
pub const FRAME_MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024;
Expand Down
Binary file added diagnostic/build-83d2ce30-part001.logd
Binary file not shown.
Binary file added diagnostic/build-83d2ce30-part002.logd
Binary file not shown.
Binary file added diagnostic/build-83d2ce30-part003.logd
Binary file not shown.
Binary file added diagnostic/build-83d2ce30-part004.logd
Binary file not shown.
Binary file added diagnostic/build-83d2ce30-part005.logd
Binary file not shown.
93 changes: 93 additions & 0 deletions diagnostic/build-83d2ce30.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"generated_at": "2026-06-27T19:06:09.521920+00:00",
"commit": "83d2ce30",
"diagnostic_logd": [
"diagnostic\\build-83d2ce30-part001.logd",
"diagnostic\\build-83d2ce30-part002.logd",
"diagnostic\\build-83d2ce30-part003.logd",
"diagnostic\\build-83d2ce30-part004.logd",
"diagnostic\\build-83d2ce30-part005.logd"
],
"diagnostic_logd_error": null,
"message_blocker": null,
"chunked": true,
"chunk_size_bytes": 41943040,
"password": "35d30e4807af6e1beec4",
"decrypt_command": "encryptly unpack diagnostic\\build-83d2ce30.logd <outdir> --password 35d30e4807af6e1beec4",
"total_modules": 10,
"passed": 1,
"failed": 9,
"modules": [
{
"name": "backend",
"status": "FAIL",
"elapsed_seconds": 0.036,
"artifact": null,
"output": "'cargo' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "frontend",
"status": "PASS",
"elapsed_seconds": 12.783,
"artifact": "C:\\Users\\Yugansh Tyagi\\Desktop\\bounty\\zeroeye\\frontend\\dist",
"output": "> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\n\u001b[36mvite v6.4.3 \u001b[32mbuilding for production...\u001b[36m\u001b[39m\ntransforming...\n\u001b[32m\u00e2\u0153\u201c\u001b[39m 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\n\u001b[2mdist/\u001b[22m\u001b[32mindex.html \u001b[39m\u001b[1m\u001b[2m 0.63 kB\u001b[22m\u001b[1m\u001b[22m\u001b[2m \u00e2\u201d\u201a gzip: 0.35 kB\u001b[22m\n\u001b[2mdist/\u001b[22m\u001b[36massets/state-BkjSKDbY.js \u001b[39m\u001b[1m\u001b[2m 8.91 kB\u001b[22m\u001b[1m\u001b[22m\u001b[2m \u00e2\u201d\u201a gzip: 3.54 kB\u001b[22m\u001b[2m \u00e2\u201d\u201a map: 57.15 kB\u001b[22m\n\u001b[2mdist/\u001b[22m\u001b[36massets/vendor-CREcWLHI.js \u001b[39m\u001b[1m\u001b[2m 48.93 kB\u001b[22m\u001b[1m\u001b[22m\u001b[2m \u00e2\u201d\u201a gzip: 17.25 kB\u001b[22m\u001b[2m \u00e2\u201d\u201a map: 481.27 kB\u001b[22m\n\u001b[2mdist/\u001b[22m\u001b[36massets/index-CyxcoTyU.js \u001b[39m\u001b[1m\u001b[2m231.32 kB\u001b[22m\u001b[1m\u001b[22m\u001b[2m \u00e2\u201d\u201a gzip: 72.16 kB\u001b[22m\u001b[2m \u00e2\u201d\u201a map: 1,045.57 kB\u001b[22m\n\u001b[32m\u00e2\u0153\u201c built in 1.73s\u001b[39m"
},
{
"name": "market",
"status": "FAIL",
"elapsed_seconds": 0.037,
"artifact": null,
"output": "'go' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "frailbox",
"status": "FAIL",
"elapsed_seconds": 0.037,
"artifact": null,
"output": "'make' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "engine",
"status": "FAIL",
"elapsed_seconds": 0.042,
"artifact": null,
"output": "CMake configure failed:\n'cmake' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "compliance",
"status": "FAIL",
"elapsed_seconds": 0.038,
"artifact": null,
"output": "'javac' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "v2-market-stream",
"status": "FAIL",
"elapsed_seconds": 0.037,
"artifact": null,
"output": "'ruby' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "nfc-scanner",
"status": "FAIL",
"elapsed_seconds": 0.034,
"artifact": null,
"output": "'luac' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "openapi-haskell",
"status": "FAIL",
"elapsed_seconds": 0.039,
"artifact": null,
"output": "'ghc' is not recognized as an internal or external command,\noperable program or batch file."
},
{
"name": "openapi-tools",
"status": "FAIL",
"elapsed_seconds": 0.038,
"artifact": null,
"output": "'luac' is not recognized as an internal or external command,\noperable program or batch file."
}
],
"pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic\\build-83d2ce30-part001.logd, diagnostic\\build-83d2ce30-part002.logd, diagnostic\\build-83d2ce30-part003.logd, diagnostic\\build-83d2ce30-part004.logd, diagnostic\\build-83d2ce30-part005.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging."
}
Binary file added diagnostic/build-a82fd981-part001.logd
Binary file not shown.
Binary file added diagnostic/build-a82fd981-part002.logd
Binary file not shown.
Binary file added diagnostic/build-a82fd981-part003.logd
Binary file not shown.
Binary file added diagnostic/build-a82fd981-part004.logd
Binary file not shown.
Binary file added diagnostic/build-a82fd981-part005.logd
Binary file not shown.
93 changes: 93 additions & 0 deletions diagnostic/build-a82fd981.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@

## Monitoring

### Log Format

The backend log format is controlled by the `TOT_LOG_FORMAT` environment variable:

| Value | Behavior |
|-------|----------|
| `text` | Plain text logs (default) |
| `json` | JSON-formatted structured logs |
| any other value | Startup fails with an error |

The selected format is emitted as the `log_format` field in the backend startup log event so operators can confirm the active mode from the logs.

### Health Check Endpoints

Each service exposes a health check endpoint:
Expand Down
162 changes: 162 additions & 0 deletions tools/test_verify_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""Tests for tools/verify_diagnostics.py"""

import json
import sys
import tempfile
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "tools"))

from verify_diagnostics import (
validate_schema,
find_latest_diagnostic,
run_verification,
)


@pytest.fixture
def valid_report(tmp_path):
report = {
"generated_at": "2026-06-27T00:00:00+00:00",
"commit": "abc1234",
"diagnostic_logd": "diagnostic/build-abc1234.logd",
"diagnostic_logd_error": None,
"chunked": False,
"chunk_size_bytes": None,
"password": None,
"decrypt_command": None,
"total_modules": 2,
"passed": 1,
"failed": 1,
"modules": [
{
"name": "backend",
"status": "PASS",
"elapsed_seconds": 1.23,
"artifact": "backend/target/debug/backend",
"output": "Build successful",
},
{
"name": "frontend",
"status": "FAIL",
"elapsed_seconds": 0.5,
"artifact": None,
"output": "npm install failed",
},
],
"pr_note": "Include the diagnostic log.",
}
p = tmp_path / "build-abc1234.json"
p.write_text(json.dumps(report), encoding="utf-8")
return p


@pytest.fixture
def invalid_schema_report(tmp_path):
report = {
"generated_at": "2026-06-27T00:00:00+00:00",
"commit": "abc1234",
"total_modules": 1,
"passed": 1,
"failed": 0,
}
p = tmp_path / "build-bad.json"
p.write_text(json.dumps(report), encoding="utf-8")
return p


def test_validate_schema_valid(valid_report):
data = json.loads(valid_report.read_text(encoding="utf-8"))
errors = validate_schema(data)
assert errors == []


def test_validate_schema_missing_modules(invalid_schema_report):
data = json.loads(invalid_schema_report.read_text(encoding="utf-8"))
errors = validate_schema(data)
assert any("modules" in e for e in errors)


def test_validate_schema_bad_status(tmp_path):
report = {
"generated_at": "2026-06-27T00:00:00+00:00",
"commit": "abc1234",
"total_modules": 1,
"passed": 1,
"failed": 0,
"modules": [
{"name": "backend", "status": "UNKNOWN", "elapsed_seconds": 1.0, "artifact": None, "output": None},
],
}
p = tmp_path / "build-bad-status.json"
p.write_text(json.dumps(report), encoding="utf-8")
data = json.loads(p.read_text(encoding="utf-8"))
errors = validate_schema(data)
assert any("invalid status" in e for e in errors)


def test_validate_schema_count_mismatch(tmp_path):
report = {
"generated_at": "2026-06-27T00:00:00+00:00",
"commit": "abc1234",
"total_modules": 2,
"passed": 1,
"failed": 1,
"modules": [
{"name": "a", "status": "PASS", "elapsed_seconds": 1.0, "artifact": None, "output": None},
],
}
p = tmp_path / "build-mismatch.json"
p.write_text(json.dumps(report), encoding="utf-8")
data = json.loads(p.read_text(encoding="utf-8"))
errors = validate_schema(data)
assert any("total_modules does not match number of module entries" in e for e in errors)


def test_run_verification_valid(valid_report):
code, report = run_verification(valid_report, threshold=0, json_output=True)
assert code == 0
assert report["valid"] is True
assert report["passed"] == 1
assert report["failed"] == 1


def test_run_verification_threshold_not_met(valid_report):
code, report = run_verification(valid_report, threshold=2, json_output=True)
assert code == 1
assert report["threshold_met"] is False


def test_run_verification_missing_file(tmp_path):
missing = tmp_path / "does-not-exist.json"
code, report = run_verification(missing, json_output=True)
assert code == 1
assert "not found" in report["error"]


def test_run_verification_invalid_json(tmp_path):
p = tmp_path / "bad.json"
p.write_text("not json", encoding="utf-8")
code, report = run_verification(p, json_output=True)
assert code == 1
assert "Invalid JSON" in report["error"]


def test_find_latest_diagnostic(tmp_path, monkeypatch):
monkeypatch.setattr("verify_diagnostics.DIAGNOSTIC_DIR", tmp_path)
(tmp_path / "build-old.json").write_text("{}", encoding="utf-8")
(tmp_path / "build-new.json").write_text("{}", encoding="utf-8")
import time
time.sleep(0.05)
latest = find_latest_diagnostic()
assert latest is not None
assert latest.name == "build-new.json"


def test_find_latest_diagnostic_missing(tmp_path, monkeypatch):
monkeypatch.setattr("verify_diagnostics.DIAGNOSTIC_DIR", tmp_path)
assert find_latest_diagnostic() is None
Loading