anwesen/src/main.rs

95 lines
3.6 KiB
Rust
Raw Normal View History

//! Anwesen: read-only HTTP daemon over a markdown vault.
//!
//! This module wires the CLI to the `serve`, `doctor`, `merge`, and
//! `version` subcommands.
mod cli;
use std::sync::Arc;
use anwesen::app::Anwesen;
use anwesen::doctor;
use anwesen::merge;
ANW-42 Telemetry: take export config from OTEL_ environment variables only Delete the uptrace DSN parser and the {endpoint}/v1/* concatenation that produced the dead export, and build the OTLP exporters with no endpoint, headers, or protocol so the SDK reads OTEL_EXPORTER_OTLP_* itself. The SDK concatenates as naively as we did: measured against a local sink, a ?query base sends POST /?query/v1/metrics and a #fragment base sends POST /. So one check stays -- a query or fragment on OTEL_EXPORTER_OTLP_ENDPOINT fails at startup. Per-signal variables are used verbatim and need none. A protocol variable is the second startup check. The exporter picks its transport at build time and this binary ships OTLP/HTTP alone, so OTEL_EXPORTER_OTLP_PROTOCOL=grpc kept exporting over HTTP with nothing in the log -- the same silence this change removes. service.name stays a default rather than policy: an attribute set on the resource builder wins over the SDK detectors that read OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES, so anwesen supplies its own only for keys the environment leaves alone. Telemetry-off is our own check on the endpoint variables: with none set the exporter would still build and aim at the SDK default localhost:4318. --uptrace-dsn, --otlp-endpoint, and --otlp-header stay parsed but hidden, so a deployment upgrading with them set fails naming the OTEL_ replacement instead of going quiet. Assumed hidden clap stubs are the right migration shape; clap rejecting the flags as unknown would say nothing about the replacement. Flag if a plain unknown-argument error is wanted instead. Assumed rejecting http/json alongside grpc is right: neither has a transport in this build. Flag if a build with both features is wanted instead.
2026-07-26 23:44:51 +03:00
use anwesen::telemetry::{self, OtelEnv, RawTelemetryArgs, TelemetryConfig};
use anyhow::Result;
use clap::Parser;
use hydra::Application;
use crate::cli::{Cli, Command};
// Result is retained at the binary boundary per [[ADR-001 Language and
// Foundation Libraries]] (anyhow at main); telemetry config resolution and
// exporter setup surface startup errors through it.
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Serve(args) => {
init_logging(args.log_level);
ANW-42 Telemetry: take export config from OTEL_ environment variables only Delete the uptrace DSN parser and the {endpoint}/v1/* concatenation that produced the dead export, and build the OTLP exporters with no endpoint, headers, or protocol so the SDK reads OTEL_EXPORTER_OTLP_* itself. The SDK concatenates as naively as we did: measured against a local sink, a ?query base sends POST /?query/v1/metrics and a #fragment base sends POST /. So one check stays -- a query or fragment on OTEL_EXPORTER_OTLP_ENDPOINT fails at startup. Per-signal variables are used verbatim and need none. A protocol variable is the second startup check. The exporter picks its transport at build time and this binary ships OTLP/HTTP alone, so OTEL_EXPORTER_OTLP_PROTOCOL=grpc kept exporting over HTTP with nothing in the log -- the same silence this change removes. service.name stays a default rather than policy: an attribute set on the resource builder wins over the SDK detectors that read OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES, so anwesen supplies its own only for keys the environment leaves alone. Telemetry-off is our own check on the endpoint variables: with none set the exporter would still build and aim at the SDK default localhost:4318. --uptrace-dsn, --otlp-endpoint, and --otlp-header stay parsed but hidden, so a deployment upgrading with them set fails naming the OTEL_ replacement instead of going quiet. Assumed hidden clap stubs are the right migration shape; clap rejecting the flags as unknown would say nothing about the replacement. Flag if a plain unknown-argument error is wanted instead. Assumed rejecting http/json alongside grpc is right: neither has a transport in this build. Flag if a build with both features is wanted instead.
2026-07-26 23:44:51 +03:00
// Resolve telemetry config before the supervisor starts; no
// OTEL_EXPORTER_OTLP_* endpoint leaves it `None` (export off,
// no middleware). A removed flag is a startup error (ANW-42).
let telemetry = match TelemetryConfig::resolve(
&RawTelemetryArgs {
uptrace_dsn: args.uptrace_dsn,
otlp_endpoint: args.otlp_endpoint,
otlp_headers: args.otlp_headers,
slow_request_ms: args.otlp_slow_request_ms,
},
OtelEnv::from_env(),
)? {
Some(cfg) => Some(Arc::new(telemetry::init(cfg)?)),
None => None,
};
tracing::info!(
vault = %args.vault.display(),
bind = %args.bind,
telemetry = telemetry.is_some(),
"anwesen serve: starting supervisor tree"
);
// Blocks until the supervisor exits (SIGTERM / SIGINT / crash).
Anwesen::new(args.vault, args.bind, telemetry.clone()).run();
// Flush and shut down exporters after the server loop returns.
if let Some(telemetry) = telemetry {
telemetry.shutdown();
}
}
Command::Doctor(args) => {
init_logging(args.log_level);
let (rendered, exit) = doctor::run_and_render(&args.vault);
// Render to stdout so the report is pipe-friendly; logs go to
// stderr via the tracing subscriber.
print!("{rendered}");
std::process::exit(exit);
}
Command::Merge(args) => {
init_logging(args.log_level);
match merge::run(&args.vault, &args.query) {
// `print!`, not `println!`: the merged document is byte-stable
// and byte-identical to the HTTP merge body, which carries no
// trailing newline. An empty match set prints nothing, exit 0.
Ok(doc) => print!("{doc}"),
Err(e) => {
eprint!("{}", e.render());
std::process::exit(1);
}
}
}
Command::Version => {
println!("{}", env!("CARGO_PKG_VERSION"));
}
}
Ok(())
}
fn init_logging(level: cli::LogLevel) {
// The User Manual lists --log-level / ANWESEN_LOG_LEVEL as the only knobs
// for verbosity, and pins "CLI flags win over environment variables".
// Resolution happens in clap; this function only builds the filter from
// the already-resolved level.
let filter = tracing_subscriber::EnvFilter::new(level.as_filter_directive());
let _ = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.try_init();
}