#[cfg(feature = "agent")]
mod cmd_agent;
mod cmd_alias;
mod cmd_audit_cmd;
#[cfg(feature = "cloud-pull-aws")]
mod cmd_aws_pull;
#[cfg(feature = "cloud-pull-aws")]
mod cmd_aws_push;
#[cfg(feature = "biometric")]
mod cmd_biometric;
#[cfg(feature = "cloud-pull-bitwarden")]
mod cmd_bitwarden_pull;
#[cfg(feature = "nativehost")]
mod cmd_browser_native_host;
#[cfg(feature = "browser")]
mod cmd_browser_profile;
#[cfg(feature = "collab")]
mod cmd_collab;
mod cmd_config_cmd;
#[cfg(feature = "git-helpers")]
mod cmd_credential_helper;
mod cmd_diff;
mod cmd_doctor;
mod cmd_exec;
#[cfg(feature = "cloud-pull-gcp")]
mod cmd_gcp_pull;
#[cfg(feature = "cloud-pull-gcp")]
mod cmd_gcp_push;
mod cmd_gen;
#[cfg(feature = "git-helpers")]
mod cmd_git;
mod cmd_import;
#[cfg(feature = "cloud-pull-keepass")]
mod cmd_keepass_pull;
#[cfg(feature = "akv-pull")]
mod cmd_kv_pull;
#[cfg(feature = "akv-pull")]
mod cmd_kv_push;
mod cmd_ns;
#[cfg(feature = "plugins")]
mod cmd_plugin;
mod cmd_policy;
mod cmd_profile_cmd;
#[cfg(feature = "multi-pull")]
mod cmd_pull;
#[cfg(feature = "akv-pull")]
mod cmd_push;
mod cmd_rotate;
#[cfg(feature = "ots-sharing")]
mod cmd_share;
mod cmd_snapshot_cmd;
#[cfg(feature = "ssh")]
mod cmd_ssh;
#[cfg(feature = "cloud-pull-aws")]
mod cmd_ssm_pull;
#[cfg(feature = "cloud-pull-aws")]
mod cmd_ssm_push;
#[cfg(feature = "git-helpers")]
mod cmd_sync;
#[cfg(feature = "team-core")]
mod cmd_team;
mod cmd_template;
mod cmd_totp;
mod cmd_validate;
mod cmd_vault;
#[cfg(any(feature = "cloud-pull-vault", feature = "cloud-pull-1password"))]
mod cmd_vault_pull;
mod helpers;
#[cfg(all(windows, feature = "biometric"))]
mod windows_hello;
#[cfg(feature = "agent")]
use cmd_agent::cmd_agent;
use cmd_alias::{cmd_alias, cmd_history, cmd_mv};
use cmd_audit_cmd::{cmd_audit, cmd_audit_verify};
#[cfg(feature = "cloud-pull-aws")]
use cmd_aws_pull::cmd_aws_pull;
#[cfg(feature = "cloud-pull-aws")]
use cmd_aws_push::cmd_aws_push;
#[cfg(feature = "biometric")]
use cmd_biometric::cmd_biometric;
#[cfg(feature = "cloud-pull-bitwarden")]
use cmd_bitwarden_pull::cmd_bitwarden_pull;
#[cfg(feature = "nativehost")]
use cmd_browser_native_host::cmd_browser_native_host;
#[cfg(feature = "browser")]
use cmd_browser_profile::cmd_browser_profile;
#[cfg(feature = "collab")]
use cmd_collab::cmd_collab;
use cmd_config_cmd::cmd_config;
#[cfg(feature = "git-helpers")]
use cmd_credential_helper::cmd_credential_helper;
#[cfg(feature = "git-helpers")]
use cmd_diff::cmd_hook_install;
use cmd_diff::{cmd_audit_export, cmd_compare, cmd_diff};
use cmd_doctor::cmd_doctor;
use cmd_exec::cmd_exec;
#[cfg(feature = "cloud-pull-gcp")]
use cmd_gcp_pull::cmd_gcp_pull;
#[cfg(feature = "cloud-pull-gcp")]
use cmd_gcp_push::cmd_gcp_push;
use cmd_gen::{cmd_completions, cmd_completions_data, cmd_gen};
#[cfg(feature = "git-helpers")]
use cmd_git::cmd_git;
use cmd_import::cmd_import;
#[cfg(feature = "cloud-pull-keepass")]
use cmd_keepass_pull::cmd_keepass_pull;
#[cfg(feature = "akv-pull")]
use cmd_kv_pull::cmd_kv_pull;
#[cfg(feature = "akv-pull")]
use cmd_kv_push::cmd_kv_push;
use cmd_ns::cmd_ns;
#[cfg(feature = "plugins")]
use cmd_plugin::cmd_plugin;
use cmd_policy::{cmd_policy, cmd_rotate_due};
use cmd_profile_cmd::{cmd_profile, cmd_rotate, cmd_unlock};
#[cfg(feature = "multi-pull")]
use cmd_pull::cmd_pull;
#[cfg(feature = "akv-pull")]
use cmd_push::cmd_push;
use cmd_rotate::cmd_rotate_key;
#[cfg(feature = "ots-sharing")]
use cmd_share::{cmd_receive_once, cmd_share_once};
use cmd_snapshot_cmd::cmd_snapshot;
#[cfg(feature = "ssh")]
use cmd_ssh::{cmd_ssh, cmd_ssh_add, cmd_ssh_import};
#[cfg(feature = "cloud-pull-aws")]
use cmd_ssm_pull::cmd_ssm_pull;
#[cfg(feature = "cloud-pull-aws")]
use cmd_ssm_push::cmd_ssm_push;
#[cfg(feature = "git-helpers")]
use cmd_sync::cmd_sync;
#[cfg(feature = "team-core")]
use cmd_team::cmd_team;
use cmd_template::{cmd_redact, cmd_template};
use cmd_totp::{cmd_pin, cmd_qr, cmd_totp, cmd_unpin};
use cmd_validate::cmd_validate;
use cmd_vault::{cmd_delete, cmd_export, cmd_get, cmd_init, cmd_list, cmd_set};
#[cfg(feature = "cloud-pull-1password")]
use cmd_vault_pull::cmd_op_pull;
#[cfg(feature = "cloud-pull-vault")]
use cmd_vault_pull::cmd_vault_pull;
use anyhow::Result;
use clap::Parser;
use colored::Colorize;
use tsafe_cli::cli::{Cli, Commands, ExecPresetSetting};
use tsafe_core::profile;
fn main() {
#[cfg(feature = "otel")]
let _otel_provider = init_tracing();
#[cfg(not(feature = "otel"))]
init_tracing();
let cli = Cli::parse();
if let Err(e) = run(cli) {
eprintln!("{} {e:#}", "error:".red().bold());
std::process::exit(1);
}
}
fn run(cli: Cli) -> Result<()> {
let profile_explicit = cli.profile.is_some();
let profile = cli.profile.unwrap_or_else(profile::get_default_profile);
if command_requires_valid_profile(&cli.command) {
profile::validate_profile_name(&profile)?;
}
match cli.command {
Commands::Init => cmd_init(&profile, profile_explicit),
Commands::Config { action } => cmd_config(action),
Commands::Set {
key,
value,
tags,
overwrite,
} => cmd_set(&profile, &key, value, tags, overwrite),
Commands::Get { key, copy, version } => cmd_get(&profile, &key, copy, version),
Commands::Delete { key } => cmd_delete(&profile, &key),
Commands::List { tags, ns } => cmd_list(&profile, &tags, ns.as_deref()),
Commands::Export {
format,
keys,
tags,
ns,
} => cmd_export(&profile, format, keys, tags, ns.as_deref()),
Commands::Exec {
cmd,
contract,
ns,
keys,
mode,
timeout: _,
preset,
dry_run,
plan,
no_inherit,
minimal,
only,
require,
env_mappings,
deny_dangerous_env,
allow_dangerous_env,
redact_output,
no_redact_output,
} => {
let effective_minimal = minimal || matches!(preset, Some(ExecPresetSetting::Minimal));
cmd_exec(
&profile,
profile_explicit,
contract.as_deref(),
cmd,
ns.as_deref(),
keys,
mode,
dry_run,
plan,
no_inherit,
effective_minimal,
only,
require,
env_mappings,
deny_dangerous_env,
allow_dangerous_env,
redact_output,
no_redact_output,
)
}
Commands::Import {
from,
file,
overwrite,
skip_duplicates,
ns,
dry_run,
} => cmd_import(
&profile,
&from,
file.as_deref(),
overwrite,
skip_duplicates,
ns.as_deref(),
dry_run,
),
Commands::Ns { action } => cmd_ns(&profile, action),
Commands::Rotate => cmd_rotate(&profile),
Commands::RotateKey {
profile: profile_override,
} => {
let effective = profile_override.as_deref().unwrap_or(&profile);
cmd_rotate_key(effective)
}
Commands::Profile { action } => cmd_profile(&profile, action),
Commands::Audit {
limit,
hibp,
explain,
json,
cell_id,
} => cmd_audit(&profile, limit, hibp, explain, json, cell_id.as_deref()),
Commands::Validate {
cellos_policy,
policy_file,
json,
} => {
let path = cellos_policy.or(policy_file).ok_or_else(|| {
anyhow::anyhow!("one of --cellos-policy or --policy-file is required")
})?;
cmd_validate(&path, json)
}
Commands::Snapshot { action } => cmd_snapshot(&profile, action),
#[cfg(feature = "akv-pull")]
Commands::KvPull {
prefix,
overwrite,
on_error,
} => cmd_kv_pull(&profile, prefix.as_deref(), overwrite, on_error),
#[cfg(feature = "akv-pull")]
Commands::KvPush {
prefix,
ns,
dry_run,
yes,
delete_missing,
} => cmd_kv_push(
&profile,
prefix.as_deref(),
ns.as_deref(),
dry_run,
yes,
delete_missing,
),
#[cfg(feature = "ots-sharing")]
Commands::ShareOnce { key, ttl } => cmd_share_once(&profile, &key, &ttl),
Commands::Gen {
key,
length,
charset,
words,
tags,
print,
exclude_ambiguous,
} => cmd_gen(
&profile,
&key,
length,
&charset,
words,
tags,
print,
exclude_ambiguous,
),
Commands::Diff => cmd_diff(&profile),
Commands::Compare { profile_b } => cmd_compare(&profile, &profile_b),
#[cfg(feature = "git-helpers")]
Commands::HookInstall { dir } => cmd_hook_install(dir.as_deref()),
Commands::AuditExport { format, output } => {
cmd_audit_export(&profile, format, output.as_deref())
}
Commands::AuditVerify { json } => cmd_audit_verify(&profile, json),
#[cfg(feature = "cloud-pull-vault")]
Commands::VaultPull {
addr,
token,
mount,
prefix,
overwrite,
} => cmd_vault_pull(
&profile,
addr.as_deref(),
token.as_deref(),
mount.as_deref(),
prefix.as_deref(),
overwrite,
),
#[cfg(feature = "cloud-pull-1password")]
Commands::OpPull {
item,
op_vault,
overwrite,
} => cmd_op_pull(&profile, &item, op_vault.as_deref(), overwrite),
#[cfg(feature = "cloud-pull-bitwarden")]
Commands::BwPull {
bw_client_id,
bw_client_secret,
bw_api_url,
bw_identity_url,
bw_folder,
bw_password_env,
overwrite,
on_error,
dry_run,
} => {
if dry_run {
println!("Dry run — Bitwarden pull would contact the bw CLI.");
println!(
" client_id: {}",
bw_client_id
.as_deref()
.unwrap_or("(from TSAFE_BW_CLIENT_ID)")
);
println!(
" api_url: {}",
bw_api_url.as_deref().unwrap_or("https://api.bitwarden.com")
);
println!(
" identity_url: {}",
bw_identity_url
.as_deref()
.unwrap_or("https://identity.bitwarden.com")
);
println!(
" folder: {}",
bw_folder.as_deref().unwrap_or("(all items)")
);
println!(
" password_env: {}",
bw_password_env.as_deref().unwrap_or("TSAFE_BW_PASSWORD")
);
println!(" overwrite: {overwrite}");
return Ok(());
}
cmd_bitwarden_pull(
&profile,
bw_api_url.as_deref(),
bw_identity_url.as_deref(),
bw_client_id.as_deref(),
bw_client_secret.as_deref(),
bw_folder.as_deref(),
bw_password_env.as_deref(),
overwrite,
on_error,
)
}
#[cfg(feature = "cloud-pull-keepass")]
Commands::KpPull {
kp_path,
kp_password_env,
kp_keyfile,
kp_group,
kp_recursive,
overwrite,
on_error,
} => {
use tsafe_core::pullconfig::PullSource;
let src = PullSource::Keepass {
name: None,
ns: None,
path: kp_path,
password_env: Some(kp_password_env),
keyfile_path: kp_keyfile,
group: kp_group,
recursive: Some(kp_recursive),
overwrite,
};
cmd_keepass_pull(&profile, &src, overwrite, on_error)
}
#[cfg(feature = "cloud-pull-aws")]
Commands::AwsPull {
region,
prefix,
overwrite,
on_error,
} => cmd_aws_pull(
&profile,
region.as_deref(),
prefix.as_deref(),
overwrite,
on_error,
),
#[cfg(feature = "cloud-pull-gcp")]
Commands::GcpPull {
project,
prefix,
overwrite,
on_error,
} => cmd_gcp_pull(
&profile,
project.as_deref(),
prefix.as_deref(),
overwrite,
on_error,
),
#[cfg(feature = "cloud-pull-gcp")]
Commands::GcpPush {
project,
prefix,
ns,
dry_run,
yes,
delete_missing,
} => cmd_gcp_push(
&profile,
project.as_deref(),
prefix.as_deref(),
ns.as_deref(),
dry_run,
yes,
delete_missing,
),
#[cfg(feature = "cloud-pull-aws")]
Commands::AwsPush {
region,
prefix,
dry_run,
yes,
delete_missing,
} => cmd_aws_push(
&profile,
region.as_deref(),
prefix.as_deref(),
dry_run,
yes,
delete_missing,
),
#[cfg(feature = "cloud-pull-aws")]
Commands::SsmPull {
region,
path,
overwrite,
on_error,
} => cmd_ssm_pull(
&profile,
region.as_deref(),
path.as_deref(),
overwrite,
on_error,
),
#[cfg(feature = "cloud-pull-aws")]
Commands::SsmPush {
region,
path,
dry_run,
yes,
delete_missing,
} => cmd_ssm_push(
&profile,
region.as_deref(),
path.as_deref(),
dry_run,
yes,
delete_missing,
),
Commands::Completions { shell } => cmd_completions(shell),
Commands::CompletionsData { data_type } => cmd_completions_data(&data_type),
Commands::Doctor { json } => cmd_doctor(&profile, json),
Commands::Explain { topic } => {
tsafe_cli::explain::run(topic);
Ok(())
}
Commands::Unlock => cmd_unlock(&profile),
#[cfg(feature = "tui")]
Commands::Ui => {
std::env::set_var("TSAFE_CLI_VERSION", env!("CARGO_PKG_VERSION"));
tsafe_tui::run().map_err(|e| anyhow::anyhow!(e))
}
Commands::Qr { key } => cmd_qr(&profile, &key),
Commands::Totp { action } => cmd_totp(&profile, action),
Commands::Pin { key } => cmd_pin(&profile, &key),
Commands::Unpin { key } => cmd_unpin(&profile, &key),
Commands::Alias {
target_key,
alias_name,
list,
} => cmd_alias(&profile, target_key.as_deref(), alias_name.as_deref(), list),
#[cfg(feature = "browser")]
Commands::BrowserProfile { action } => cmd_browser_profile(&profile, action),
#[cfg(feature = "nativehost")]
Commands::BrowserNativeHost { action } => cmd_browser_native_host(action),
#[cfg(feature = "ots-sharing")]
Commands::ReceiveOnce { url, store } => cmd_receive_once(&profile, &url, store.as_deref()),
#[cfg(feature = "agent")]
Commands::Agent { action } => cmd_agent(&profile, action),
#[cfg(feature = "git-helpers")]
Commands::Git { args } => cmd_git(&profile, args),
Commands::History { key } => cmd_history(&profile, &key),
Commands::Mv {
source,
dest,
to_profile,
force,
} => cmd_mv(
&profile,
&source,
dest.as_deref(),
to_profile.as_deref(),
force,
),
Commands::Policy { action } => cmd_policy(&profile, action),
Commands::RotateDue { json, fail } => cmd_rotate_due(&profile, json, fail),
#[cfg(feature = "ssh")]
Commands::SshAdd { key } => cmd_ssh_add(&profile, &key),
#[cfg(feature = "ssh")]
Commands::SshImport { path, name, tags } => {
cmd_ssh_import(&profile, &path, name.as_deref(), tags)
}
#[cfg(feature = "ssh")]
Commands::Ssh { action } => cmd_ssh(&profile, action),
#[cfg(feature = "multi-pull")]
Commands::Pull {
config,
overwrite,
on_error,
dry_run,
sources,
} => cmd_pull(
&profile,
config.as_deref(),
overwrite,
on_error,
dry_run,
&sources,
),
#[cfg(feature = "akv-pull")]
Commands::Push {
config,
source,
dry_run,
yes,
delete_missing,
on_error,
} => cmd_push(
&profile,
config.as_deref(),
&source,
dry_run,
yes,
delete_missing,
on_error,
),
#[cfg(feature = "biometric")]
Commands::Biometric { action } => cmd_biometric(&profile, action),
#[cfg(feature = "team-core")]
Commands::Team { action } => cmd_team(&profile, action),
#[cfg(feature = "git-helpers")]
Commands::Sync {
remote,
branch,
file,
dry_run,
} => cmd_sync(&profile, &remote, &branch, file.as_deref(), dry_run),
Commands::Template {
file,
output,
ignore_missing,
} => cmd_template(&profile, &file, output.as_deref(), ignore_missing),
Commands::Redact => cmd_redact(&profile),
Commands::BuildInfo { json } => cmd_build_info(json),
#[cfg(feature = "plugins")]
Commands::Plugin { tool, args } => cmd_plugin(&profile, tool.as_deref(), &args),
#[cfg(feature = "git-helpers")]
Commands::CredentialHelper { action, global } => {
cmd_credential_helper(&profile, action, global)
}
#[cfg(feature = "collab")]
Commands::Collab { action } => cmd_collab(&profile, action),
}
}
fn command_requires_valid_profile(command: &Commands) -> bool {
let skips_profile_validation = matches!(
command,
Commands::BuildInfo { .. }
| Commands::Completions { .. }
| Commands::CompletionsData { .. }
| Commands::Config { .. }
| Commands::Explain { .. }
| Commands::Validate { .. }
);
#[cfg(feature = "tui")]
let skips_profile_validation = skips_profile_validation || matches!(command, Commands::Ui);
#[cfg(feature = "nativehost")]
let skips_profile_validation =
skips_profile_validation || matches!(command, Commands::BrowserNativeHost { .. });
!skips_profile_validation
}
fn compile_time_feature_flags() -> Vec<&'static str> {
let mut feature_flags = Vec::new();
if cfg!(feature = "tui") {
feature_flags.push("tui");
}
if cfg!(feature = "akv-pull") {
feature_flags.push("akv-pull");
}
if cfg!(feature = "biometric") {
feature_flags.push("biometric");
}
if cfg!(feature = "agent") {
feature_flags.push("agent");
}
if cfg!(feature = "team-core") {
feature_flags.push("team-core");
}
if cfg!(feature = "cloud-pull-aws") {
feature_flags.push("cloud-pull-aws");
}
if cfg!(feature = "cloud-pull-gcp") {
feature_flags.push("cloud-pull-gcp");
}
if cfg!(feature = "cloud-pull-vault") {
feature_flags.push("cloud-pull-vault");
}
if cfg!(feature = "cloud-pull-1password") {
feature_flags.push("cloud-pull-1password");
}
if cfg!(feature = "cloud-pull-keepass") {
feature_flags.push("cloud-pull-keepass");
}
if cfg!(feature = "cloud-pull-bitwarden") {
feature_flags.push("cloud-pull-bitwarden");
}
if cfg!(feature = "multi-pull") {
feature_flags.push("multi-pull");
}
if cfg!(feature = "pm-import-extended") {
feature_flags.push("pm-import-extended");
}
if cfg!(feature = "ots-sharing") {
feature_flags.push("ots-sharing");
}
if cfg!(feature = "git-helpers") {
feature_flags.push("git-helpers");
}
if cfg!(feature = "browser") {
feature_flags.push("browser");
}
if cfg!(feature = "nativehost") {
feature_flags.push("nativehost");
}
if cfg!(feature = "ssh") {
feature_flags.push("ssh");
}
if cfg!(feature = "plugins") {
feature_flags.push("plugins");
}
if cfg!(feature = "otel") {
feature_flags.push("otel");
}
feature_flags.sort_unstable();
feature_flags
}
const DEFAULT_CORE_BUILD_PROFILE: &[&str] =
&["agent", "akv-pull", "biometric", "ssh", "team-core", "tui"];
fn build_profile_label(capabilities: &[&'static str]) -> &'static str {
if capabilities.is_empty() {
"enterprise-minimal"
} else if capabilities == DEFAULT_CORE_BUILD_PROFILE {
"default-core"
} else {
"custom"
}
}
fn cmd_build_info(json: bool) -> Result<()> {
let capabilities = compile_time_feature_flags();
let profile = build_profile_label(&capabilities);
if json {
let payload = serde_json::json!({
"build_profile": profile,
"capabilities": capabilities,
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
println!("build_profile: {profile}");
if capabilities.is_empty() {
println!("capabilities: none");
} else {
println!("capabilities: {}", capabilities.join(","));
}
}
Ok(())
}
fn tracing_log_enabled() -> bool {
std::env::var("TSAFE_LOG")
.ok()
.filter(|v| !v.is_empty())
.is_some()
}
fn tracing_json_enabled() -> bool {
std::env::var("TSAFE_LOG_FORMAT")
.map(|v| v.eq_ignore_ascii_case("json"))
.unwrap_or(false)
}
#[cfg(not(feature = "otel"))]
fn init_tracing() {
if !tracing_log_enabled() {
return;
}
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::Layer as _;
use tracing_subscriber::{fmt, EnvFilter};
let filter = EnvFilter::from_env("TSAFE_LOG");
let fmt_layer = if tracing_json_enabled() {
fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.json()
.with_span_events(FmtSpan::CLOSE)
.boxed()
} else {
fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.compact()
.boxed()
};
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
}
#[cfg(feature = "otel")]
fn otel_stdout_enabled() -> bool {
std::env::var("TSAFE_OTEL_STDOUT")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
#[cfg(feature = "otel")]
fn otel_trace_endpoint() -> Option<String> {
std::env::var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
.ok()
.filter(|value| !value.trim().is_empty())
.or_else(|| {
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
.ok()
.filter(|value| !value.trim().is_empty())
})
}
#[cfg(feature = "otel")]
fn build_otel_provider() -> Option<opentelemetry_sdk::trace::SdkTracerProvider> {
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::{Protocol, WithExportConfig};
use opentelemetry_sdk::trace::SdkTracerProvider;
let stdout_enabled = otel_stdout_enabled();
let otlp_endpoint = otel_trace_endpoint();
if !stdout_enabled && otlp_endpoint.is_none() {
return None;
}
let mut builder = SdkTracerProvider::builder();
let mut has_exporter = false;
if stdout_enabled {
builder = builder.with_simple_exporter(opentelemetry_stdout::SpanExporter::default());
has_exporter = true;
}
if let Some(endpoint) = otlp_endpoint {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(endpoint)
.with_protocol(Protocol::HttpBinary)
.build();
match exporter {
Ok(exporter) => {
builder = builder.with_batch_exporter(exporter);
has_exporter = true;
}
Err(err) => {
eprintln!(
"{} could not initialize OTLP HTTP exporter: {err}",
"warn:".yellow()
);
}
}
}
if !has_exporter {
return None;
}
let provider = builder.build();
opentelemetry::global::set_tracer_provider(provider.clone());
let _ = provider.tracer("tsafe");
Some(provider)
}
#[cfg(feature = "otel")]
fn init_tracing() -> Option<opentelemetry_sdk::trace::SdkTracerProvider> {
use opentelemetry::trace::TracerProvider as _;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt as _;
use tracing_subscriber::util::SubscriberInitExt as _;
use tracing_subscriber::Layer as _;
use tracing_subscriber::{fmt, EnvFilter};
let otel_provider = build_otel_provider();
let log_enabled = tracing_log_enabled();
if log_enabled {
let filter = EnvFilter::from_env("TSAFE_LOG");
let otel_layer = otel_provider.as_ref().map(|provider| {
tracing_opentelemetry::layer()
.with_tracer(provider.tracer("tsafe"))
.boxed()
});
let fmt_layer = if tracing_json_enabled() {
fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.json()
.with_span_events(FmtSpan::CLOSE)
.boxed()
} else {
fmt::layer()
.with_writer(std::io::stderr)
.with_target(false)
.compact()
.boxed()
};
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(otel_layer)
.init();
} else if let Some(provider) = otel_provider.as_ref() {
let tracer = provider.tracer("tsafe");
tracing_subscriber::registry()
.with(tracing_opentelemetry::layer().with_tracer(tracer))
.init();
}
otel_provider
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_profile_label_marks_empty_build_as_enterprise_minimal() {
assert_eq!(build_profile_label(&[]), "enterprise-minimal");
}
#[test]
fn build_profile_label_marks_core_bundle_as_default_core() {
assert_eq!(
build_profile_label(DEFAULT_CORE_BUILD_PROFILE),
"default-core"
);
}
#[test]
fn build_profile_label_marks_extra_opt_in_capabilities_as_custom() {
let capabilities = [
"agent",
"akv-pull",
"biometric",
"nativehost",
"team-core",
"tui",
];
assert_eq!(build_profile_label(&capabilities), "custom");
}
}
#[cfg(all(test, feature = "otel"))]
mod otel_tests {
use super::*;
#[test]
fn otel_trace_endpoint_prefers_traces_specific_env() {
temp_env::with_vars(
[
(
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
Some(std::ffi::OsStr::new("http://localhost:4318/v1/traces")),
),
(
"OTEL_EXPORTER_OTLP_ENDPOINT",
Some(std::ffi::OsStr::new("http://localhost:4318")),
),
],
|| {
assert_eq!(
otel_trace_endpoint().as_deref(),
Some("http://localhost:4318/v1/traces")
);
},
);
}
#[test]
fn otel_trace_endpoint_falls_back_to_base_endpoint() {
temp_env::with_vars(
[
("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", None),
(
"OTEL_EXPORTER_OTLP_ENDPOINT",
Some(std::ffi::OsStr::new("http://localhost:4318")),
),
],
|| {
assert_eq!(
otel_trace_endpoint().as_deref(),
Some("http://localhost:4318")
);
},
);
}
#[test]
fn otel_stdout_enabled_accepts_boolean_forms() {
temp_env::with_var("TSAFE_OTEL_STDOUT", Some("true"), || {
assert!(otel_stdout_enabled());
});
temp_env::with_var("TSAFE_OTEL_STDOUT", Some("1"), || {
assert!(otel_stdout_enabled());
});
temp_env::with_var("TSAFE_OTEL_STDOUT", Some("0"), || {
assert!(!otel_stdout_enabled());
});
}
#[test]
fn otel_span_fields_do_not_include_secret_bearing_names() {
let safe_span_fields = [
"m_cost",
"t_cost",
"p_cost",
"plaintext_len",
"ciphertext_len",
"secrets",
"secret_count",
];
let secret_bearing_names = [
"password",
"new_password",
"salt",
"key",
"value",
"plaintext",
"ciphertext",
"secret",
];
for safe_field in &safe_span_fields {
for secret_name in &secret_bearing_names {
assert_ne!(
safe_field, secret_name,
"span field '{safe_field}' must not be a secret-bearing parameter name"
);
}
}
}
}