ae014ac881
Replace uses of the legacy `gpt-image-2-all` model with `gpt-image-2` and standardize image workflows: no-reference generation uses POST /v1/images/generations, any-reference flows use POST /v1/images/edits with multipart `image` parts. Update SKILLs, generation scripts, decision logs, and docs to reflect the contract change and edits-vs-generations guidance. Apply corresponding changes across backend (api-server match3d/puzzle modules, openai image adapter, mappers, telemetry, spacetime client/module), frontend components and services (Match3D, Puzzle, CreativeImageInputPanel, runtime shells), and add new spritesheet/parser files and tests. Also add media/logo.png. These changes align repository code and documentation with the VectorEngine image API contract and update generation/upload handling (green-screen -> alpha processing, spritesheet handling, and related tests).
220 lines
7.1 KiB
Rust
220 lines
7.1 KiB
Rust
use std::io;
|
|
|
|
use opentelemetry::{KeyValue, global, trace::TracerProvider};
|
|
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
|
|
use opentelemetry_otlp::WithExportConfig;
|
|
use opentelemetry_sdk::{
|
|
Resource, logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider,
|
|
};
|
|
use tracing::warn;
|
|
use tracing_subscriber::{
|
|
EnvFilter, Layer, filter::LevelFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct OtelConfig {
|
|
pub enabled: bool,
|
|
}
|
|
|
|
// 统一解析工作区日志过滤器,优先环境变量,其次回落到调用方传入的默认值。
|
|
pub fn resolve_env_filter(default_filter: &str) -> EnvFilter {
|
|
EnvFilter::try_from_default_env()
|
|
.or_else(|_| EnvFilter::try_new(default_filter))
|
|
.unwrap_or_else(|_| EnvFilter::new("info"))
|
|
}
|
|
|
|
// 统一初始化 tracing subscriber,避免各入口重复散落相同配置。
|
|
pub fn init_tracing(default_filter: &str, otel_config: OtelConfig) -> Result<(), io::Error> {
|
|
let env_filter = resolve_env_filter(default_filter);
|
|
let fmt_layer = fmt::layer().with_target(true).with_ansi(false).compact();
|
|
|
|
if !otel_config.enabled {
|
|
return tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer)
|
|
.try_init()
|
|
.map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}")));
|
|
}
|
|
|
|
let Some(otel) = build_otel_pipeline() else {
|
|
return tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer)
|
|
.try_init()
|
|
.map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}")));
|
|
};
|
|
|
|
tracing_subscriber::registry()
|
|
.with(env_filter)
|
|
.with(fmt_layer)
|
|
.with(
|
|
tracing_opentelemetry::layer()
|
|
.with_tracer(otel.tracer_provider.tracer("genarrative-api")),
|
|
)
|
|
.with(OpenTelemetryTracingBridge::new(&otel.logger_provider).with_filter(LevelFilter::INFO))
|
|
.try_init()
|
|
.map_err(|error| io::Error::other(format!("初始化 tracing subscriber 失败:{error}")))
|
|
}
|
|
|
|
struct OtelPipeline {
|
|
tracer_provider: SdkTracerProvider,
|
|
_meter_provider: SdkMeterProvider,
|
|
logger_provider: SdkLoggerProvider,
|
|
}
|
|
|
|
fn build_otel_pipeline() -> Option<OtelPipeline> {
|
|
let resource = Resource::builder()
|
|
.with_service_name(read_env_or_default("OTEL_SERVICE_NAME", "genarrative-api"))
|
|
.with_attribute(KeyValue::new("service.namespace", "genarrative"))
|
|
.build();
|
|
|
|
let span_exporter = match opentelemetry_otlp::SpanExporter::builder()
|
|
.with_http()
|
|
.with_endpoint(resolve_otlp_http_signal_endpoint(
|
|
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
"/v1/traces",
|
|
))
|
|
.build()
|
|
{
|
|
Ok(exporter) => exporter,
|
|
Err(error) => {
|
|
warn!(%error, "OpenTelemetry span exporter 初始化失败,已回退为本地日志");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let metric_exporter = match opentelemetry_otlp::MetricExporter::builder()
|
|
.with_http()
|
|
.with_endpoint(resolve_otlp_http_signal_endpoint(
|
|
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
|
|
"/v1/metrics",
|
|
))
|
|
.build()
|
|
{
|
|
Ok(exporter) => exporter,
|
|
Err(error) => {
|
|
warn!(%error, "OpenTelemetry metric exporter 初始化失败,已回退为本地日志");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let log_exporter = match opentelemetry_otlp::LogExporter::builder()
|
|
.with_http()
|
|
.with_endpoint(resolve_otlp_http_signal_endpoint(
|
|
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
"/v1/logs",
|
|
))
|
|
.build()
|
|
{
|
|
Ok(exporter) => exporter,
|
|
Err(error) => {
|
|
warn!(%error, "OpenTelemetry log exporter 初始化失败,已回退为本地日志");
|
|
return None;
|
|
}
|
|
};
|
|
|
|
let tracer_provider = SdkTracerProvider::builder()
|
|
.with_resource(resource.clone())
|
|
.with_batch_exporter(span_exporter)
|
|
.build();
|
|
let meter_provider = SdkMeterProvider::builder()
|
|
.with_resource(resource)
|
|
.with_periodic_exporter(metric_exporter)
|
|
.build();
|
|
let logger_provider = SdkLoggerProvider::builder()
|
|
.with_resource(
|
|
Resource::builder()
|
|
.with_service_name(read_env_or_default("OTEL_SERVICE_NAME", "genarrative-api"))
|
|
.with_attribute(KeyValue::new("service.namespace", "genarrative"))
|
|
.build(),
|
|
)
|
|
.with_batch_exporter(log_exporter)
|
|
.build();
|
|
|
|
global::set_tracer_provider(tracer_provider.clone());
|
|
global::set_meter_provider(meter_provider.clone());
|
|
|
|
Some(OtelPipeline {
|
|
tracer_provider,
|
|
_meter_provider: meter_provider,
|
|
logger_provider,
|
|
})
|
|
}
|
|
|
|
fn read_env_or_default(key: &str, default_value: &str) -> String {
|
|
std::env::var(key)
|
|
.ok()
|
|
.filter(|value| !value.trim().is_empty())
|
|
.unwrap_or_else(|| default_value.to_string())
|
|
}
|
|
|
|
fn resolve_otlp_http_signal_endpoint(signal_key: &str, signal_path: &str) -> String {
|
|
if let Ok(value) = std::env::var(signal_key)
|
|
&& !value.trim().is_empty()
|
|
{
|
|
return value;
|
|
}
|
|
|
|
append_otlp_signal_path(
|
|
&read_env_or_default("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:4318"),
|
|
signal_path,
|
|
)
|
|
}
|
|
|
|
fn append_otlp_signal_path(base_endpoint: &str, signal_path: &str) -> String {
|
|
let base_endpoint = base_endpoint.trim_end_matches('/');
|
|
let signal_path = signal_path.trim_start_matches('/');
|
|
format!("{base_endpoint}/{signal_path}")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
use super::resolve_otlp_http_signal_endpoint;
|
|
|
|
const OTEL_ENDPOINT_ENV_KEYS: [&str; 4] = [
|
|
"OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
|
|
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
|
|
];
|
|
|
|
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
|
|
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
LOCK.get_or_init(|| Mutex::new(())).lock().unwrap()
|
|
}
|
|
|
|
fn clear_otel_endpoint_env() {
|
|
unsafe {
|
|
for key in OTEL_ENDPOINT_ENV_KEYS {
|
|
std::env::remove_var(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn generic_otlp_http_endpoint_expands_to_signal_paths() {
|
|
let _guard = env_lock();
|
|
clear_otel_endpoint_env();
|
|
unsafe {
|
|
std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "http://127.0.0.1:4318");
|
|
}
|
|
|
|
assert_eq!(
|
|
resolve_otlp_http_signal_endpoint("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "/v1/traces"),
|
|
"http://127.0.0.1:4318/v1/traces"
|
|
);
|
|
assert_eq!(
|
|
resolve_otlp_http_signal_endpoint("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "/v1/metrics"),
|
|
"http://127.0.0.1:4318/v1/metrics"
|
|
);
|
|
assert_eq!(
|
|
resolve_otlp_http_signal_endpoint("OTEL_EXPORTER_OTLP_LOGS_ENDPOINT", "/v1/logs"),
|
|
"http://127.0.0.1:4318/v1/logs"
|
|
);
|
|
|
|
clear_otel_endpoint_env();
|
|
}
|
|
}
|