mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(app-server, core): add more spans (#14479)
## Description This PR expands tracing coverage across app-server thread startup, core session initialization, and the Responses transport layer. It also gives core dispatch spans stable operation-specific names so traces are easier to follow than the old generic `submission_dispatch` spans. Also use `fmt::Display` for types that we serialize in traces so we send strings instead of rust types
This commit is contained in:
@@ -80,6 +80,7 @@ use tokio::sync::oneshot;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio_tungstenite::tungstenite::Error;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tracing::instrument;
|
||||
use tracing::trace;
|
||||
use tracing::warn;
|
||||
|
||||
@@ -732,6 +733,18 @@ impl ModelClientSession {
|
||||
Ok(())
|
||||
}
|
||||
/// Returns a websocket connection for this turn.
|
||||
#[instrument(
|
||||
name = "model_client.websocket_connection",
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
provider = %self.client.state.provider.name,
|
||||
wire_api = %self.client.state.provider.wire_api,
|
||||
transport = "responses_websocket",
|
||||
api.path = "responses",
|
||||
turn.has_metadata_header = turn_metadata_header.is_some()
|
||||
)
|
||||
)]
|
||||
async fn websocket_connection(
|
||||
&mut self,
|
||||
session_telemetry: &SessionTelemetry,
|
||||
@@ -789,6 +802,19 @@ impl ModelClientSession {
|
||||
/// Handles SSE fixtures, reasoning summaries, verbosity, and the
|
||||
/// `text` controls used for output schemas.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(
|
||||
name = "model_client.stream_responses_api",
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
model = %model_info.slug,
|
||||
wire_api = %self.client.state.provider.wire_api,
|
||||
transport = "responses_http",
|
||||
http.method = "POST",
|
||||
api.path = "responses",
|
||||
turn.has_metadata_header = turn_metadata_header.is_some()
|
||||
)
|
||||
)]
|
||||
async fn stream_responses_api(
|
||||
&self,
|
||||
prompt: &Prompt,
|
||||
@@ -856,6 +882,19 @@ impl ModelClientSession {
|
||||
|
||||
/// Streams a turn via the Responses API over WebSocket transport.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(
|
||||
name = "model_client.stream_responses_websocket",
|
||||
level = "info",
|
||||
skip_all,
|
||||
fields(
|
||||
model = %model_info.slug,
|
||||
wire_api = %self.client.state.provider.wire_api,
|
||||
transport = "responses_websocket",
|
||||
api.path = "responses",
|
||||
turn.has_metadata_header = turn_metadata_header.is_some(),
|
||||
websocket.warmup = warmup
|
||||
)
|
||||
)]
|
||||
async fn stream_responses_websocket(
|
||||
&mut self,
|
||||
prompt: &Prompt,
|
||||
|
||||
+62
-12
@@ -584,7 +584,6 @@ impl Codex {
|
||||
let session_source_clone = session_configuration.session_source.clone();
|
||||
let (agent_status_tx, agent_status_rx) = watch::channel(AgentStatus::PendingInit);
|
||||
|
||||
let session_init_span = info_span!("session_init");
|
||||
let session = Session::new(
|
||||
session_configuration,
|
||||
config.clone(),
|
||||
@@ -601,7 +600,6 @@ impl Codex {
|
||||
file_watcher,
|
||||
agent_control,
|
||||
)
|
||||
.instrument(session_init_span)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Failed to create session: {e:#}");
|
||||
@@ -1340,6 +1338,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(name = "session_init", level = "info", skip_all)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn new(
|
||||
mut session_configuration: SessionConfiguration,
|
||||
@@ -1431,18 +1430,29 @@ impl Session {
|
||||
.await?;
|
||||
Ok((Some(rollout_recorder), state_db_ctx))
|
||||
}
|
||||
};
|
||||
}
|
||||
.instrument(info_span!(
|
||||
"session_init.rollout",
|
||||
otel.name = "session_init.rollout",
|
||||
session_init.ephemeral = config.ephemeral,
|
||||
));
|
||||
|
||||
let is_subagent = matches!(
|
||||
session_configuration.session_source,
|
||||
SessionSource::SubAgent(_)
|
||||
);
|
||||
let history_meta_fut = async {
|
||||
if matches!(
|
||||
session_configuration.session_source,
|
||||
SessionSource::SubAgent(_)
|
||||
) {
|
||||
if is_subagent {
|
||||
(0, 0)
|
||||
} else {
|
||||
crate::message_history::history_metadata(&config).await
|
||||
}
|
||||
};
|
||||
}
|
||||
.instrument(info_span!(
|
||||
"session_init.history_metadata",
|
||||
otel.name = "session_init.history_metadata",
|
||||
session_init.is_subagent = is_subagent,
|
||||
));
|
||||
let auth_manager_clone = Arc::clone(&auth_manager);
|
||||
let config_for_mcp = Arc::clone(&config);
|
||||
let mcp_manager_for_mcp = Arc::clone(&mcp_manager);
|
||||
@@ -1455,7 +1465,11 @@ impl Session {
|
||||
)
|
||||
.await;
|
||||
(auth, mcp_servers, auth_statuses)
|
||||
};
|
||||
}
|
||||
.instrument(info_span!(
|
||||
"session_init.auth_mcp",
|
||||
otel.name = "session_init.auth_mcp",
|
||||
));
|
||||
|
||||
// Join all independent futures.
|
||||
let (
|
||||
@@ -1613,7 +1627,12 @@ impl Session {
|
||||
tx
|
||||
};
|
||||
let thread_name =
|
||||
match session_index::find_thread_name_by_id(&config.codex_home, &conversation_id).await
|
||||
match session_index::find_thread_name_by_id(&config.codex_home, &conversation_id)
|
||||
.instrument(info_span!(
|
||||
"session_init.thread_name_lookup",
|
||||
otel.name = "session_init.thread_name_lookup",
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(name) => name,
|
||||
Err(err) => {
|
||||
@@ -1663,6 +1682,12 @@ impl Session {
|
||||
managed_network_requirements_enabled,
|
||||
network_proxy_audit_metadata,
|
||||
)
|
||||
.instrument(info_span!(
|
||||
"session_init.network_proxy",
|
||||
otel.name = "session_init.network_proxy",
|
||||
session_init.managed_network_requirements_enabled =
|
||||
managed_network_requirements_enabled,
|
||||
))
|
||||
.await?;
|
||||
(Some(network_proxy), Some(session_network_proxy))
|
||||
} else {
|
||||
@@ -1812,6 +1837,8 @@ impl Session {
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
required_mcp_servers.sort();
|
||||
let enabled_mcp_server_count = mcp_servers.values().filter(|server| server.enabled).count();
|
||||
let required_mcp_server_count = required_mcp_servers.len();
|
||||
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config.as_ref());
|
||||
{
|
||||
let mut cancel_guard = sess.services.mcp_startup_cancellation_token.lock().await;
|
||||
@@ -1829,6 +1856,12 @@ impl Session {
|
||||
codex_apps_tools_cache_key(auth),
|
||||
tool_plugin_provenance,
|
||||
)
|
||||
.instrument(info_span!(
|
||||
"session_init.mcp_manager_init",
|
||||
otel.name = "session_init.mcp_manager_init",
|
||||
session_init.enabled_mcp_server_count = enabled_mcp_server_count,
|
||||
session_init.required_mcp_server_count = required_mcp_server_count,
|
||||
))
|
||||
.await;
|
||||
{
|
||||
let mut manager_guard = sess.services.mcp_connection_manager.write().await;
|
||||
@@ -1848,6 +1881,11 @@ impl Session {
|
||||
.read()
|
||||
.await
|
||||
.required_startup_failures(&required_mcp_servers)
|
||||
.instrument(info_span!(
|
||||
"session_init.required_mcp_wait",
|
||||
otel.name = "session_init.required_mcp_wait",
|
||||
session_init.required_mcp_server_count = required_mcp_server_count,
|
||||
))
|
||||
.await;
|
||||
if !failures.is_empty() {
|
||||
let details = failures
|
||||
@@ -4269,11 +4307,23 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
}
|
||||
|
||||
fn submission_dispatch_span(sub: &Submission) -> tracing::Span {
|
||||
let op_name = sub.op.kind();
|
||||
let span_name = format!("op.dispatch.{op_name}");
|
||||
let dispatch_span = match &sub.op {
|
||||
Op::RealtimeConversationAudio(_) => {
|
||||
debug_span!("submission_dispatch", submission.id = sub.id.as_str())
|
||||
debug_span!(
|
||||
"submission_dispatch",
|
||||
otel.name = span_name.as_str(),
|
||||
submission.id = sub.id.as_str(),
|
||||
codex.op = op_name
|
||||
)
|
||||
}
|
||||
_ => info_span!("submission_dispatch", submission.id = sub.id.as_str()),
|
||||
_ => info_span!(
|
||||
"submission_dispatch",
|
||||
otel.name = span_name.as_str(),
|
||||
submission.id = sub.id.as_str(),
|
||||
codex.op = op_name
|
||||
),
|
||||
};
|
||||
if let Some(trace) = sub.trace.as_ref()
|
||||
&& !set_parent_from_w3c_trace_context(&dispatch_span, trace)
|
||||
|
||||
@@ -2491,6 +2491,34 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn op_kind_distinguishes_turn_ops() {
|
||||
assert_eq!(
|
||||
Op::OverrideTurnContext {
|
||||
cwd: None,
|
||||
approval_policy: None,
|
||||
sandbox_policy: None,
|
||||
windows_sandbox_level: None,
|
||||
model: None,
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
}
|
||||
.kind(),
|
||||
"override_turn_context"
|
||||
);
|
||||
assert_eq!(
|
||||
Op::UserInput {
|
||||
items: vec![],
|
||||
final_output_json_schema: None,
|
||||
}
|
||||
.kind(),
|
||||
"user_input"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn spawn_task_turn_span_inherits_dispatch_trace_context() {
|
||||
struct TraceCaptureTask {
|
||||
|
||||
@@ -28,6 +28,7 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use thiserror::Error;
|
||||
use tokio::fs;
|
||||
use tokio::task::spawn_blocking;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::bash::parse_shell_lc_plain_commands;
|
||||
use crate::bash::parse_shell_lc_single_command_prefix;
|
||||
@@ -187,6 +188,7 @@ impl ExecPolicyManager {
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) async fn load(config_stack: &ConfigLayerStack) -> Result<Self, ExecPolicyError> {
|
||||
let (policy, warning) = load_exec_policy_with_warning(config_stack).await?;
|
||||
if let Some(err) = warning.as_ref() {
|
||||
|
||||
@@ -16,6 +16,7 @@ use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::time::Duration;
|
||||
|
||||
const DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 300_000;
|
||||
@@ -40,6 +41,15 @@ pub enum WireApi {
|
||||
Responses,
|
||||
}
|
||||
|
||||
impl fmt::Display for WireApi {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value = match self {
|
||||
Self::Responses => "responses",
|
||||
};
|
||||
f.write_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WireApi {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
|
||||
@@ -18,6 +18,7 @@ use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use http::HeaderMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -26,6 +27,7 @@ use tokio::sync::TryLockError;
|
||||
use tokio::time::timeout;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
use tracing::instrument;
|
||||
|
||||
const MODEL_CACHE_FILE: &str = "models_cache.json";
|
||||
const DEFAULT_MODEL_CACHE_TTL: Duration = Duration::from_secs(300);
|
||||
@@ -42,6 +44,22 @@ pub enum RefreshStrategy {
|
||||
OnlineIfUncached,
|
||||
}
|
||||
|
||||
impl RefreshStrategy {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Online => "online",
|
||||
Self::Offline => "offline",
|
||||
Self::OnlineIfUncached => "online_if_uncached",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RefreshStrategy {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// How the manager's base catalog is sourced for the lifetime of the process.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CatalogMode {
|
||||
@@ -102,6 +120,11 @@ impl ModelsManager {
|
||||
/// List all available models, refreshing according to the specified strategy.
|
||||
///
|
||||
/// Returns model presets sorted by priority and filtered by auth mode and visibility.
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip(self),
|
||||
fields(refresh_strategy = %refresh_strategy)
|
||||
)]
|
||||
pub async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec<ModelPreset> {
|
||||
if let Err(err) = self.refresh_available_models(refresh_strategy).await {
|
||||
error!("failed to refresh available models: {err}");
|
||||
@@ -137,6 +160,14 @@ impl ModelsManager {
|
||||
///
|
||||
/// If `model` is provided, returns it directly. Otherwise selects the default based on
|
||||
/// auth mode and available models.
|
||||
#[instrument(
|
||||
level = "info",
|
||||
skip(self, model),
|
||||
fields(
|
||||
model.provided = model.is_some(),
|
||||
refresh_strategy = %refresh_strategy
|
||||
)
|
||||
)]
|
||||
pub async fn get_default_model(
|
||||
&self,
|
||||
model: &Option<String>,
|
||||
@@ -160,6 +191,7 @@ impl ModelsManager {
|
||||
|
||||
// todo(aibrahim): look if we can tighten it to pub(crate)
|
||||
/// Look up model metadata, applying remote overrides and config adjustments.
|
||||
#[instrument(level = "info", skip(self, config), fields(model = model))]
|
||||
pub async fn get_model_info(&self, model: &str, config: &Config) -> ModelInfo {
|
||||
let remote_models = self.get_remote_models().await;
|
||||
Self::construct_model_info_from_candidates(model, &remote_models, config)
|
||||
|
||||
@@ -31,6 +31,7 @@ use std::path::PathBuf;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use toml::Value as TomlValue;
|
||||
use tracing::error;
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) const HIERARCHICAL_AGENTS_MESSAGE: &str =
|
||||
include_str!("../hierarchical_agents_message.md");
|
||||
@@ -80,6 +81,7 @@ fn render_js_repl_instructions(config: &Config) -> Option<String> {
|
||||
|
||||
/// Combines `Config::instructions` and `AGENTS.md` (if present) into a single
|
||||
/// string of instructions.
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub(crate) async fn get_user_instructions(
|
||||
config: &Config,
|
||||
skills: Option<&[SkillMetadata]>,
|
||||
|
||||
Reference in New Issue
Block a user