Add shared OTLP telemetry and propagate command traces across agents
This commit is contained in:
29 files changed
+1227
-213
No files matched your search
@@ -4,7 +4,7 @@ use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
use tracing::{info, info_span, warn};
|
||||
use tracing::{info, warn};
|
||||
use uuid::Uuid;
|
||||
use wakey_agent::protocol::{
|
||||
AgentCapability, AgentCapabilityOptions, AgentCommand, ErrorPayload, RequestId, ServerMessage,
|
||||
@@ -85,6 +85,7 @@ pub async fn run_command(
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "relay_command", skip_all, fields(agent_id = %agent_id, request_id = tracing::field::Empty, command = tracing::field::Empty))]
|
||||
pub async fn relay_agent_command(
|
||||
state: &AppState,
|
||||
agent_id: &str,
|
||||
@@ -93,14 +94,11 @@ pub async fn relay_agent_command(
|
||||
) -> Result<RelayCommandResponse, ApiError> {
|
||||
let request_id_string = format!("req-{}", Uuid::new_v4());
|
||||
let command_kind = command_kind(&command);
|
||||
tracing::Span::current()
|
||||
.record("request_id", &request_id_string)
|
||||
.record("command", command_kind);
|
||||
let started = Instant::now();
|
||||
let span = info_span!(
|
||||
"relay_command",
|
||||
agent_id = %agent_id,
|
||||
request_id = %request_id_string,
|
||||
command = %command_kind,
|
||||
);
|
||||
let _span_guard = span.enter();
|
||||
let mut operation = wakey_core::telemetry::Operation::new(command_kind);
|
||||
|
||||
let request_id = RequestId::try_from(request_id_string.clone()).map_err(|err| {
|
||||
ApiError::new(
|
||||
@@ -149,6 +147,7 @@ pub async fn relay_agent_command(
|
||||
if let Err(err) = tx.send(SessionEvent::Message(ServerMessage::Command {
|
||||
request_id,
|
||||
command,
|
||||
trace_context: wakey_core::telemetry::current_trace_context(),
|
||||
})) {
|
||||
state.pending.lock().await.remove(&request_id_string);
|
||||
warn!(error = %err, "failed sending command to agent session");
|
||||
@@ -249,6 +248,7 @@ pub async fn relay_agent_command(
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
operation.outcome("timeout");
|
||||
state.pending.lock().await.remove(&request_id_string);
|
||||
warn!(
|
||||
timeout_ms = timeout.as_millis() as u64,
|
||||
@@ -276,6 +276,11 @@ pub async fn relay_agent_command(
|
||||
}
|
||||
};
|
||||
|
||||
operation.outcome(if response.status == "ok" {
|
||||
"ok"
|
||||
} else {
|
||||
"error"
|
||||
});
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ pub struct TerminalSessionResponse {
|
||||
pub attachment_token: Option<String>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "terminal_create", skip_all)]
|
||||
pub async fn create_terminal(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<CreateTerminalRequest>,
|
||||
@@ -183,6 +184,7 @@ pub async fn list_terminals(State(state): State<AppState>) -> Json<Vec<TerminalS
|
||||
)
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "terminal_attach", skip_all)]
|
||||
pub async fn attach_terminal(
|
||||
State(state): State<AppState>,
|
||||
Path(terminal_id): Path<String>,
|
||||
@@ -210,6 +212,7 @@ pub async fn attach_terminal(
|
||||
}))
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "terminal_close", skip_all)]
|
||||
pub async fn close_terminal(
|
||||
State(state): State<AppState>,
|
||||
Path(terminal_id): Path<String>,
|
||||
|
||||
@@ -113,6 +113,9 @@ pub fn write_init_config(args: &InitConfigArgs) -> Result<Option<PathBuf>> {
|
||||
ui_dist_dir,
|
||||
bootstrap_enroll_tokens,
|
||||
telemetry: WritableTelemetry {
|
||||
authorization_env: base_telemetry
|
||||
.authorization_env
|
||||
.or_else(|| defaults.telemetry.authorization_env.clone()),
|
||||
otlp_endpoint: args
|
||||
.telemetry_otlp_endpoint
|
||||
.clone()
|
||||
@@ -223,6 +226,7 @@ bootstrap_enroll_tokens = ["old-token"]
|
||||
|
||||
[telemetry]
|
||||
otlp_endpoint = "http://otel.example"
|
||||
authorization_env = "WAKEY_OTLP_AUTHORIZATION"
|
||||
service_name = "old-service"
|
||||
json_logs = true
|
||||
"#,
|
||||
@@ -261,6 +265,10 @@ json_logs = true
|
||||
Some("/tmp/wakey-base/old.sqlite3")
|
||||
);
|
||||
assert_eq!(parsed["telemetry"]["json_logs"].as_bool(), Some(true));
|
||||
assert_eq!(
|
||||
parsed["telemetry"]["authorization_env"].as_str(),
|
||||
Some("WAKEY_OTLP_AUTHORIZATION")
|
||||
);
|
||||
assert_eq!(
|
||||
parsed["telemetry"]["service_name"].as_str(),
|
||||
Some("old-service")
|
||||
|
||||
@@ -231,6 +231,7 @@ fn resolve_admin_public_url(
|
||||
fn resolve_telemetry(file: Option<FileTelemetryConfig>) -> TelemetryConfig {
|
||||
let mut out = TelemetryConfig::default();
|
||||
if let Some(file) = file {
|
||||
out.authorization_env = file.authorization_env;
|
||||
if let Some(endpoint) = file.otlp_endpoint {
|
||||
let trimmed = endpoint.trim().to_string();
|
||||
if !trimmed.is_empty() {
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct DaemonConfig {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TelemetryConfig {
|
||||
pub otlp_endpoint: Option<String>,
|
||||
pub authorization_env: Option<String>,
|
||||
pub service_name: String,
|
||||
pub json_logs: bool,
|
||||
}
|
||||
@@ -32,6 +33,7 @@ impl Default for TelemetryConfig {
|
||||
let defaults = &DEFAULT_CONFIG.telemetry;
|
||||
Self {
|
||||
otlp_endpoint: defaults.otlp_endpoint.clone(),
|
||||
authorization_env: defaults.authorization_env.clone(),
|
||||
service_name: defaults.service_name.clone(),
|
||||
json_logs: defaults.json_logs,
|
||||
}
|
||||
@@ -57,6 +59,7 @@ pub(crate) struct FileConfig {
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub(crate) struct FileTelemetryConfig {
|
||||
pub(crate) otlp_endpoint: Option<String>,
|
||||
pub(crate) authorization_env: Option<String>,
|
||||
pub(crate) service_name: Option<String>,
|
||||
pub(crate) json_logs: Option<bool>,
|
||||
}
|
||||
@@ -81,6 +84,8 @@ pub(crate) struct WritableConfig {
|
||||
pub(crate) struct WritableTelemetry {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) otlp_endpoint: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) authorization_env: Option<String>,
|
||||
pub(crate) service_name: String,
|
||||
pub(crate) json_logs: bool,
|
||||
}
|
||||
@@ -98,6 +103,7 @@ pub(crate) static DEFAULT_CONFIG: LazyLock<WritableConfig> = LazyLock::new(|| Wr
|
||||
bootstrap_enroll_tokens: Vec::new(),
|
||||
telemetry: WritableTelemetry {
|
||||
otlp_endpoint: None,
|
||||
authorization_env: None,
|
||||
service_name: "wakey-control-plane".to_string(),
|
||||
json_logs: false,
|
||||
},
|
||||
|
||||
@@ -28,8 +28,10 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
let daemon = config::DaemonConfig::from_serve_args(&args)?;
|
||||
tracing::init(cli.verbose, &daemon.telemetry)?;
|
||||
runtime::serve(daemon).await
|
||||
let telemetry = tracing::init(cli.verbose, &daemon.telemetry)?;
|
||||
let result = runtime::serve(daemon).await;
|
||||
tokio::task::spawn_blocking(move || telemetry.shutdown()).await?;
|
||||
result
|
||||
}
|
||||
Command::InitConfig(args) => {
|
||||
if let Some(path) = config::write_init_config(&args)? {
|
||||
|
||||
@@ -213,6 +213,8 @@ pub async fn serve(daemon: config::DaemonConfig) -> Result<()> {
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
let mut server = server;
|
||||
let mut hup = signal(SignalKind::hangup()).context("failed to install SIGHUP handler")?;
|
||||
let mut terminate =
|
||||
signal(SignalKind::terminate()).context("failed to install SIGTERM handler")?;
|
||||
let mut gc_tick = tokio::time::interval(Duration::from_secs(300));
|
||||
gc_tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
|
||||
|
||||
@@ -224,6 +226,11 @@ pub async fn serve(daemon: config::DaemonConfig) -> Result<()> {
|
||||
server.abort();
|
||||
break;
|
||||
}
|
||||
_ = terminate.recv() => {
|
||||
info!("SIGTERM received; shutting down control-plane");
|
||||
server.abort();
|
||||
break;
|
||||
}
|
||||
_ = hup.recv() => {
|
||||
match app_state.store.reload_from_disk().await {
|
||||
Ok(()) => info!("reloaded state from disk"),
|
||||
|
||||
@@ -1,99 +1,22 @@
|
||||
use anyhow::{Context, Result};
|
||||
use opentelemetry::global;
|
||||
use opentelemetry::trace::TracerProvider as _;
|
||||
use opentelemetry_otlp::WithExportConfig;
|
||||
use opentelemetry_sdk::Resource;
|
||||
use opentelemetry_sdk::trace::{SdkTracerProvider, Tracer};
|
||||
use time::UtcOffset;
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use tracing_subscriber::fmt::time::OffsetTime;
|
||||
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
use crate::config::TelemetryConfig;
|
||||
use anyhow::Result;
|
||||
pub use wakey_core::telemetry::TelemetryGuard;
|
||||
|
||||
pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<()> {
|
||||
let filter = EnvFilter::try_from_default_env()
|
||||
.or_else(|_| EnvFilter::try_new(default_filter(verbose)))
|
||||
.expect("static tracing filter should parse");
|
||||
|
||||
let otel = build_otel_layer(telemetry)?;
|
||||
|
||||
if telemetry.json_logs {
|
||||
if let Some(otel_layer) = otel {
|
||||
tracing_subscriber::registry()
|
||||
.with(otel_layer)
|
||||
.with(filter)
|
||||
.with(fmt::layer().json().with_timer(local_offset_timer()))
|
||||
.init();
|
||||
tracing::info!(endpoint = ?telemetry.otlp_endpoint, json_logs = telemetry.json_logs, "tracing initialized with otlp exporter");
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt::layer().json().with_timer(local_offset_timer()))
|
||||
.init();
|
||||
tracing::info!(
|
||||
json_logs = telemetry.json_logs,
|
||||
"tracing initialized without otlp exporter"
|
||||
);
|
||||
}
|
||||
} else if let Some(otel_layer) = otel {
|
||||
tracing_subscriber::registry()
|
||||
.with(otel_layer)
|
||||
.with(filter)
|
||||
.with(fmt::layer().with_timer(local_offset_timer()))
|
||||
.init();
|
||||
tracing::info!(endpoint = ?telemetry.otlp_endpoint, json_logs = telemetry.json_logs, "tracing initialized with otlp exporter");
|
||||
} else {
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(fmt::layer().with_timer(local_offset_timer()))
|
||||
.init();
|
||||
tracing::info!(
|
||||
json_logs = telemetry.json_logs,
|
||||
"tracing initialized without otlp exporter"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_otel_layer(
|
||||
telemetry: &TelemetryConfig,
|
||||
) -> Result<Option<tracing_opentelemetry::OpenTelemetryLayer<tracing_subscriber::Registry, Tracer>>>
|
||||
{
|
||||
let Some(endpoint) = telemetry.otlp_endpoint.as_deref() else {
|
||||
return Ok(None);
|
||||
pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<TelemetryGuard> {
|
||||
let config = wakey_core::telemetry::TelemetryConfig {
|
||||
otlp_endpoint: telemetry.otlp_endpoint.clone(),
|
||||
authorization_env: telemetry.authorization_env.clone(),
|
||||
service_name: telemetry.service_name.clone(),
|
||||
json_logs: telemetry.json_logs,
|
||||
};
|
||||
|
||||
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||
.with_tonic()
|
||||
.with_endpoint(endpoint)
|
||||
.build()
|
||||
.context("failed to build OTLP span exporter")?;
|
||||
|
||||
let provider = SdkTracerProvider::builder()
|
||||
.with_batch_exporter(exporter)
|
||||
.with_resource(
|
||||
Resource::builder_empty()
|
||||
.with_service_name(telemetry.service_name.clone())
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
let tracer = provider.tracer(telemetry.service_name.clone());
|
||||
global::set_tracer_provider(provider);
|
||||
Ok(Some(tracing_opentelemetry::layer().with_tracer(tracer)))
|
||||
}
|
||||
|
||||
fn default_filter(verbose: u8) -> &'static str {
|
||||
match verbose {
|
||||
0 => "wakey_control_plane=info",
|
||||
1 => "wakey_control_plane=debug",
|
||||
_ => "wakey_control_plane=trace",
|
||||
}
|
||||
}
|
||||
|
||||
fn local_offset_timer() -> OffsetTime<Rfc3339> {
|
||||
let offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
|
||||
OffsetTime::new(offset, Rfc3339)
|
||||
wakey_core::telemetry::init(
|
||||
&config,
|
||||
"wakey-control-plane",
|
||||
match verbose {
|
||||
0 => "wakey_control_plane=info,wakey_core=info,opentelemetry=warn",
|
||||
1 => "wakey_control_plane=debug,wakey_core=info,opentelemetry=warn",
|
||||
_ => "wakey_control_plane=trace,wakey_core=info,opentelemetry=warn",
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
@@ -309,12 +309,18 @@ async fn process_agent_text(
|
||||
anyhow::bail!("device_snapshot for unauthenticated or mismatched agent");
|
||||
}
|
||||
ensure_current_session(state, &agent_id, connection_id).await?;
|
||||
match state
|
||||
.store
|
||||
.replace_agent_device_snapshot(&agent_id, &devices)
|
||||
.await
|
||||
let mut operation = wakey_core::telemetry::Operation::new("snapshot_store");
|
||||
let span = tracing::info_span!("store_device_snapshot", agent_id = %agent_id, devices = devices.len());
|
||||
match tracing::Instrument::instrument(
|
||||
state
|
||||
.store
|
||||
.replace_agent_device_snapshot(&agent_id, &devices),
|
||||
span,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(accepted) => {
|
||||
operation.outcome("ok");
|
||||
debug!(agent_id = %agent_id, accepted, "agent websocket device snapshot accepted");
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
Reference in new issue
Block a user