наблюдаемость: завершить базовый контур Community
CI / Rust Checks (push) Failing after 4m28s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Community Image Smoke (push) Has been skipped
CI / Deploy (push) Has been skipped

Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
2026-07-31 01:01:14 +03:00
parent 99bd05c145
commit 0e8f1ca03a
160 changed files with 13506 additions and 1499 deletions
+86 -76
View File
@@ -8,9 +8,11 @@ use crank_import::rest::{
ImportFinding, ImportFindingSeverity, ImportOperationCandidate, operation_draft_from_candidate,
};
use crank_registry::{
CreateImportJobRequest, FinishImportJobRequest, ImportJobId, ImportJobKind, ImportJobStatus,
ApplyImportJobRequest, CreateImportJobRequest, ImportConflictMode, ImportJobId, ImportJobKind,
ImportJobStatus, ImportOperationDraft,
};
use serde_json::json;
use sha2::{Digest, Sha256};
use time::{Duration, OffsetDateTime, format_description::well_known::Rfc3339};
use tracing::{info, instrument};
@@ -50,7 +52,7 @@ impl AdminService {
kind: ImportJobKind::OpenApi,
source_format: &preview.source.format,
source_version: preview.source.version.as_deref(),
status: ImportJobStatus::Completed,
status: ImportJobStatus::Pending,
preview_payload: &preview_payload,
created_at: &now,
expires_at: &expires_at,
@@ -99,9 +101,13 @@ impl AdminService {
return Err(ApiError::validation("import job kind is not openapi"));
}
let preview: crank_import::rest::ImportPreview =
serde_json::from_value(job.preview_payload.clone())
.map_err(|error| ApiError::internal(error.to_string()))?;
let stored_preview = job
.preview_payload
.get("preview")
.cloned()
.unwrap_or_else(|| job.preview_payload.clone());
let preview: crank_import::rest::ImportPreview = serde_json::from_value(stored_preview)
.map_err(|error| ApiError::internal(error.to_string()))?;
let selected = payload
.selected_operation_keys
.iter()
@@ -120,10 +126,8 @@ impl AdminService {
}
}
let mut created = Vec::new();
let mut skipped = Vec::new();
let mut findings = Vec::new();
let mut created_ids = Vec::new();
let mut operations = Vec::new();
for operation_key in selected {
let Some(candidate) = candidates.get(&operation_key) else {
@@ -137,44 +141,7 @@ impl AdminService {
let mut draft =
operation_draft_from_candidate(candidate, payload.server_url.as_deref());
attach_import_findings(&mut draft, candidate);
if let Some(existing_name) = self
.find_operation_by_name(workspace_id, &draft.name)
.await?
.map(|operation| operation.name)
{
if payload.conflict_mode == "skip" {
skipped.push(OpenApiImportSkippedOperation {
operation_key: candidate.key.clone(),
name: draft.name.clone(),
reason: "operation with this name already exists".to_owned(),
});
findings.push(ImportFinding {
code: "operation_name_conflict".to_owned(),
severity: ImportFindingSeverity::Warning,
message: format!(
"Операция {} уже существует и была пропущена.",
draft.name
),
operation_key: Some(candidate.key.clone()),
});
continue;
}
let renamed = self
.next_available_operation_name(workspace_id, &draft.name)
.await?;
findings.push(ImportFinding {
code: "operation_name_renamed".to_owned(),
severity: ImportFindingSeverity::Info,
message: format!(
"Операция {existing_name} уже существует, новый черновик создан как {renamed}."
),
operation_key: Some(candidate.key.clone()),
});
draft.name = renamed;
}
let payload = OperationPayload {
let operation = self.new_operation_snapshot(OperationPayload {
name: draft.name.clone(),
display_name: draft.display_name.clone(),
category: draft.category,
@@ -197,27 +164,74 @@ impl AdminService {
},
tool_description: draft.tool_description,
wizard_state: draft.wizard_state,
};
let result = self.create_operation(workspace_id, payload).await?;
created_ids.push(result.operation_id.clone());
created.push(OpenApiImportCreatedOperation {
operation_id: result.operation_id,
name: draft.name,
version: result.version,
})?;
operations.push(ImportOperationDraft {
operation_key: candidate.key.clone(),
operation,
});
}
let finished_at = OffsetDateTime::now_utc();
self.registry
.finish_import_job(FinishImportJobRequest {
let application_key = openapi_application_key(&payload)?;
let conflict_mode = if payload.conflict_mode == "skip" {
ImportConflictMode::Skip
} else {
ImportConflictMode::Rename
};
let applied = self
.registry
.apply_import_job(ApplyImportJobRequest {
id: job_id,
status: ImportJobStatus::Completed,
created_operation_ids: &json!(created_ids),
error_text: None,
workspace_id,
application_key: &application_key,
conflict_mode,
operations: &operations,
finished_at: &finished_at,
})
.await?;
let created = applied
.created
.iter()
.map(|operation| OpenApiImportCreatedOperation {
operation_id: operation.operation_id.as_str().to_owned(),
name: operation.name.clone(),
version: operation.version,
})
.collect::<Vec<_>>();
let mut findings = applied
.created
.iter()
.filter_map(|operation| {
operation.renamed_from.as_ref().map(|previous_name| ImportFinding {
code: "operation_name_renamed".to_owned(),
severity: ImportFindingSeverity::Info,
message: format!(
"Операция {previous_name} уже существует, новый черновик создан как {}.",
operation.name
),
operation_key: Some(operation.operation_key.clone()),
})
})
.collect::<Vec<_>>();
for operation in applied.skipped {
skipped.push(OpenApiImportSkippedOperation {
operation_key: operation.operation_key.clone(),
name: operation.name.clone(),
reason: "operation with this name already exists".to_owned(),
});
findings.push(ImportFinding {
code: operation.reason,
severity: ImportFindingSeverity::Warning,
message: format!(
"Операция {} уже существует и была пропущена.",
operation.name
),
operation_key: Some(operation.operation_key),
});
}
info!(
name: "admin.openapi_import.completed",
created = created.len(),
skipped = skipped.len(),
"openapi import created drafts"
@@ -229,25 +243,21 @@ impl AdminService {
findings,
})
}
}
async fn next_available_operation_name(
&self,
workspace_id: &WorkspaceId,
base_name: &str,
) -> Result<String, ApiError> {
for index in 2.. {
let candidate = format!("{base_name}_{index}");
if self
.find_operation_by_name(workspace_id, &candidate)
.await?
.is_none()
{
return Ok(candidate);
}
}
unreachable!()
}
fn openapi_application_key(payload: &OpenApiImportCreatePayload) -> Result<String, ApiError> {
let selected_operation_keys = payload
.selected_operation_keys
.iter()
.cloned()
.collect::<BTreeSet<_>>();
let canonical = serde_json::to_vec(&json!({
"selected_operation_keys": selected_operation_keys,
"server_url": payload.server_url.as_deref(),
"conflict_mode": payload.conflict_mode.as_str(),
}))
.map_err(|error| ApiError::internal(error.to_string()))?;
Ok(format!("{:x}", Sha256::digest(canonical)))
}
fn attach_import_findings(