Track app/read request duration (#35048)

## What changed

- Record `codex.apps.read.duration_ms` when `app/read` returns a response.
- Tag the duration by the request's `include_tools` value.
- Move the `app/read` handler into its own request-processor module.

GitOrigin-RevId: e1563cd41c72ad03e7f5d5d33cd8a3b35e27984b
This commit is contained in:
stevenlee-oai
2026-07-23 23:55:05 +00:00
committed by copyberry
parent b115de97d7
commit 4123bf6189
2 changed files with 94 additions and 77 deletions
@@ -1,8 +1,10 @@
use super::*;
use crate::app_info::app_info_to_api;
use crate::app_info::connector_metadata_to_api;
mod installed;
mod read;
pub(super) use read::APP_READ_MAX_IDS;
pub(crate) struct AppsRequestProcessor {
auth_manager: Arc<AuthManager>,
@@ -35,80 +37,6 @@ impl AppsRequestProcessor {
}
}
pub(crate) async fn apps_read(
&self,
params: AppsReadParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
let AppsReadParams {
app_ids,
include_tools,
} = params;
if app_ids.len() > APP_READ_MAX_IDS {
return Err(invalid_params(format!(
"app/read accepts at most {APP_READ_MAX_IDS} appIds"
)));
}
let mut seen_app_ids = HashSet::new();
let app_ids = app_ids
.into_iter()
.filter(|app_id| seen_app_ids.insert(app_id.clone()))
.collect::<Vec<_>>();
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let auth = self.auth_manager.auth().await;
if !config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|| !self
.workspace_codex_plugins_enabled(&config, auth.as_ref())
.await
{
return Ok(Some(
AppsReadResponse {
apps: Vec::new(),
missing_app_ids: app_ids,
}
.into(),
));
}
let auth = auth
.as_ref()
.ok_or_else(|| internal_error("app/read requires ChatGPT auth".to_string()))?;
let connectors::ConnectorMetadataReadResult {
apps,
missing_app_ids,
} = connectors::read_connector_metadata(&config, auth, &app_ids, include_tools)
.await
.map_err(|err| internal_error(format!("failed to read app metadata: {err}")))?;
let loaded_plugins = self
.thread_manager
.plugins_manager()
.plugins_for_config(&config.plugins_config_input())
.await;
let connector_snapshot =
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
);
let apps = apps
.into_iter()
.map(|metadata| {
let mut app = connector_metadata_to_api(metadata);
app.plugin_display_names = connector_snapshot
.plugin_display_names_for_connector_id(app.id.as_str())
.to_vec();
app
})
.collect();
Ok(Some(
AppsReadResponse {
apps,
missing_app_ids,
}
.into(),
))
}
pub(crate) async fn apps_list(
&self,
request_id: &ConnectionRequestId,
@@ -477,8 +405,6 @@ fn record_legacy_apps_installed_duration(started_at: Instant, reload: bool) {
);
}
}
pub(super) const APP_READ_MAX_IDS: usize = 100;
enum AppListLoadResult {
Accessible(Result<AccessibleConnectorsStatus, String>),
Directory(Result<Vec<AppInfo>, String>),
@@ -0,0 +1,91 @@
use super::*;
use crate::app_info::connector_metadata_to_api;
pub(in crate::request_processors) const APP_READ_MAX_IDS: usize = 100;
const APPS_READ_DURATION_METRIC: &str = "codex.apps.read.duration_ms";
impl AppsRequestProcessor {
pub(crate) async fn apps_read(
&self,
params: AppsReadParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
let started_at = Instant::now();
let AppsReadParams {
app_ids,
include_tools,
} = params;
if app_ids.len() > APP_READ_MAX_IDS {
return Err(invalid_params(format!(
"app/read accepts at most {APP_READ_MAX_IDS} appIds"
)));
}
let mut seen_app_ids = HashSet::new();
let app_ids = app_ids
.into_iter()
.filter(|app_id| seen_app_ids.insert(app_id.clone()))
.collect::<Vec<_>>();
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let auth = self.auth_manager.auth().await;
if !config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
|| !self
.workspace_codex_plugins_enabled(&config, auth.as_ref())
.await
{
let response = AppsReadResponse {
apps: Vec::new(),
missing_app_ids: app_ids,
};
record_apps_read_duration(started_at, include_tools);
return Ok(Some(response.into()));
}
let auth = auth
.as_ref()
.ok_or_else(|| internal_error("app/read requires ChatGPT auth".to_string()))?;
let connectors::ConnectorMetadataReadResult {
apps,
missing_app_ids,
} = connectors::read_connector_metadata(&config, auth, &app_ids, include_tools)
.await
.map_err(|err| internal_error(format!("failed to read app metadata: {err}")))?;
let loaded_plugins = self
.thread_manager
.plugins_manager()
.plugins_for_config(&config.plugins_config_input())
.await;
let connector_snapshot =
codex_connectors::ConnectorSnapshot::from_plugin_capability_summaries(
loaded_plugins.capability_summaries(),
);
let apps = apps
.into_iter()
.map(|metadata| {
let mut app = connector_metadata_to_api(metadata);
app.plugin_display_names = connector_snapshot
.plugin_display_names_for_connector_id(app.id.as_str())
.to_vec();
app
})
.collect();
let response = AppsReadResponse {
apps,
missing_app_ids,
};
record_apps_read_duration(started_at, include_tools);
Ok(Some(response.into()))
}
}
fn record_apps_read_duration(started_at: Instant, include_tools: bool) {
let include_tools = if include_tools { "true" } else { "false" };
if let Some(metrics) = codex_otel::global() {
let _ = metrics.record_duration(
APPS_READ_DURATION_METRIC,
started_at.elapsed(),
&[("include_tools", include_tools)],
);
}
}