наблюдаемость: завершить базовый контур Community
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user