diff --git a/src/api/http/src/handler/http/router/mod.rs b/src/api/http/src/handler/http/router/mod.rs index 21ac3023a7..f7511e284c 100644 --- a/src/api/http/src/handler/http/router/mod.rs +++ b/src/api/http/src/handler/http/router/mod.rs @@ -33,8 +33,8 @@ use openobserve_api_management::request::profiling; use openobserve_api_management::request::{ alerts, announcements, authz, dashboards, db_monitoring, downtimes, folders, gen_ai_health, ingest_rejections, kv, metrics_usage, model_pricing, organization, public_dashboards, - query_history, rum_analytics, service_accounts, short_url, slack, slos, sourcemaps, status, - status_pages, stream, synthetics, users, + query_history, rum_analytics, service_accounts, service_streams, short_url, slack, slos, + sourcemaps, status, status_pages, stream, synthetics, users, }; #[cfg(feature = "cloud")] use openobserve_api_management::request::{cloud, organization::billing_group}; @@ -65,8 +65,7 @@ use { openobserve_api_management::request::{ ai, alert_calibration, annotation_queues, annotations, anomaly_detection, datasets, discovery, domain_management, eval_jobs, experiments, fleet, gen_ai, keys, license, oncall, - playground, prompts, providers, quality, remote_tasks, score_configs, scorers, - service_streams, workflows, + playground, prompts, providers, quality, remote_tasks, score_configs, scorers, workflows, }, openobserve_api_pipelines::request::re_pattern, openobserve_api_search::search::patterns, @@ -1215,9 +1214,13 @@ pub fn service_routes() -> Router { #[cfg(not(feature = "enterprise"))] let server = cfg.common.instance_name_short.to_string(); - // Downtimes and Slack live in their own tables to keep this function under the line limit. - let mut router = - public_dashboard_admin_routes(Router::new().merge(downtime_routes()).merge(slack_routes())); + // Downtimes, Slack and service streams live in their own tables to keep this function under the line limit. + let mut router = public_dashboard_admin_routes( + Router::new() + .merge(downtime_routes()) + .merge(slack_routes()) + .merge(service_streams_routes()), + ); // Full UI configuration — authenticated counterpart of the unauthenticated // `/config` bootstrap in config_routes() router = router.route("/{org_id}/config", get(status::zo_config)); @@ -1909,10 +1912,7 @@ pub fn service_routes() -> Router { // Service streams .route("/{org_id}/service_streams", get(service_streams::list_services)) - .route("/{org_id}/service_streams/_analytics", get(service_streams::get_dimension_analytics)) - .route("/{org_id}/service_streams/_correlate", post(service_streams::correlate_streams)) .route("/{org_id}/service_streams/config/identity", get(service_streams::get_identity_config).put(service_streams::save_identity_config)) - .route("/{org_id}/service_streams/_reset", delete(service_streams::reset_services)) .route("/{org_id}/storage",get(organization::storage::get).post(organization::storage::save).put(organization::storage::update)); router = ai_chat_routes(router); @@ -2287,6 +2287,15 @@ pub fn service_routes() -> Router { })) } +/// Registered in both builds so OSS answers the handlers' 403 enterprise-only stub, not a 404. +#[rustfmt::skip] +fn service_streams_routes() -> Router { + Router::new() + .route("/{org_id}/service_streams/_analytics", get(service_streams::get_dimension_analytics)) + .route("/{org_id}/service_streams/_correlate", post(service_streams::correlate_streams)) + .route("/{org_id}/service_streams/_reset", delete(service_streams::reset_services)) +} + /// Downtimes routes; the literal segments stay before the {downtime_id} catch-all. #[rustfmt::skip] pub fn downtime_routes() -> Router { @@ -3138,6 +3147,42 @@ mod tests { } } + #[cfg(not(feature = "enterprise"))] + #[tokio::test] + async fn service_streams_routes_answer_403_not_404_in_the_oss_build() { + let routes = [ + ( + Method::GET, + "/default/service_streams/_analytics", + Body::empty(), + ), + ( + Method::POST, + "/default/service_streams/_correlate", + Body::from( + r#"{"source_stream":"s","source_type":"logs","available_dimensions":{}}"#, + ), + ), + ( + Method::DELETE, + "/default/service_streams/_reset", + Body::empty(), + ), + ]; + for (method, uri, body) in routes { + let what = format!("{method} {uri}"); + let req = Request::builder() + .method(method) + .uri(uri) + .header("user_id", "root@example.com") + .header("content-type", "application/json") + .body(body) + .unwrap(); + let response = service_streams_routes().oneshot(req).await.unwrap(); + assert_eq!(response.status(), StatusCode::FORBIDDEN, "{what}"); + } + } + #[cfg(feature = "enterprise")] #[tokio::test] async fn a_bad_body_or_query_is_404_while_slack_is_off() { @@ -4950,6 +4995,14 @@ mod tests { .replace(",)", ")") } + #[test] + fn service_streams_routes_are_merged_unconditionally_in_service_routes() { + assert!( + service_routes_registrations().contains(".merge(service_streams_routes())"), + "service_routes() must merge service_streams_routes() outside the enterprise block" + ); + } + #[test] fn metric_usage_route_is_registered_in_service_routes() { assert!( diff --git a/src/api/http/src/handler/http/router/openapi.rs b/src/api/http/src/handler/http/router/openapi.rs index 8ee31b9c02..c82e84183c 100644 --- a/src/api/http/src/handler/http/router/openapi.rs +++ b/src/api/http/src/handler/http/router/openapi.rs @@ -311,6 +311,7 @@ use crate::{ service_streams::correlate_streams, service_streams::get_identity_config, service_streams::save_identity_config, + service_streams::reset_services, gen_ai::clear_agent_registry, gen_ai::get_agent_mapping, gen_ai::list_scored_agents, @@ -1366,7 +1367,6 @@ pub struct ApiDoc; openobserve_api_management::request::scorers::preview_llm_judge_output_schema, openobserve_api_management::request::scorers::test_scorer, openobserve_api_management::request::scorers::update_scorer, - openobserve_api_management::request::service_streams::reset_services, openobserve_api_management::request::synthetics::agent_setup, openobserve_api_management::request::users::service_accounts::exchange_token, openobserve_api_pipelines::request::re_pattern::delete, diff --git a/src/api/http/src/handler/http/router/openapi_coverage/baseline.txt b/src/api/http/src/handler/http/router/openapi_coverage/baseline.txt index db900afea0..8a58647a67 100644 --- a/src/api/http/src/handler/http/router/openapi_coverage/baseline.txt +++ b/src/api/http/src/handler/http/router/openapi_coverage/baseline.txt @@ -510,8 +510,6 @@ oss route POST /api/{org_id}/search_jobs/{job_id}/cancel oss route GET /api/{org_id}/search_jobs/{job_id}/result oss route POST /api/{org_id}/search_jobs/{job_id}/retry oss route GET /api/{org_id}/service_streams -oss route GET /api/{org_id}/service_streams/_analytics -oss route POST /api/{org_id}/service_streams/_correlate oss route GET /api/{org_id}/service_streams/config/identity oss route PUT /api/{org_id}/service_streams/config/identity oss route GET /api/{org_id}/settings/gen_ai/agent_mapping diff --git a/src/api/http/src/handler/http/router/openapi_coverage/mod.rs b/src/api/http/src/handler/http/router/openapi_coverage/mod.rs index 6c3f728d85..f8e0220166 100644 --- a/src/api/http/src/handler/http/router/openapi_coverage/mod.rs +++ b/src/api/http/src/handler/http/router/openapi_coverage/mod.rs @@ -64,6 +64,7 @@ const ROUTER_PREFIXES: &[(&str, &str)] = &[ ("oncall_routes", "/api"), ("fleet_router", "/api"), ("slack_routes", "/api"), + ("service_streams_routes", "/api"), ("es_compat_routes", "/api"), ("workflow_hook_routes", ""), ];