mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 00:09:35 +00:00
feat: co-relation phase 2 (#9513)
Design at : streams-corelation --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
github-actions[bot]
greptile-apps[bot]
parent
3f4e98fd05
commit
79bbe72ee3
@@ -26,6 +26,7 @@ use config::{
|
||||
promql::ClusterLeader,
|
||||
ratelimit::CachedUserRoles,
|
||||
stream::StreamParams,
|
||||
system_settings::SystemSetting,
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
@@ -101,6 +102,12 @@ pub static SHORT_URLS: Lazy<RwHashMap<String, ShortUrlRecord>> = Lazy::new(DashM
|
||||
pub static USER_ROLES_CACHE: Lazy<RwAHashMap<String, CachedUserRoles>> =
|
||||
Lazy::new(Default::default);
|
||||
|
||||
/// System settings cache
|
||||
/// Key format: "{scope}:{org_id}:{user_id}:{setting_key}" where org_id/user_id can be "_" if not
|
||||
/// applicable
|
||||
pub static SYSTEM_SETTINGS: Lazy<Arc<RwAHashMap<String, SystemSetting>>> =
|
||||
Lazy::new(|| Arc::new(tokio::sync::RwLock::new(HashMap::new())));
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
||||
@@ -307,10 +307,19 @@ impl FromRequest for AuthExtractor {
|
||||
path_columns[0].to_string()
|
||||
}
|
||||
} else if url_len == 2 || (url_len > 2 && path_columns[1].eq("settings")) {
|
||||
// for settings, the post/delete require PUT permissions, GET needs LIST permissions
|
||||
// also the special settings exception is for 3-part urls for logo /text
|
||||
// which are of path /org/settings/logo , which need permission of operating
|
||||
// on permission in general
|
||||
// for settings (including settings/v2), the post/delete require PUT permissions,
|
||||
// GET needs LIST permissions. This handles:
|
||||
// - /org/settings (v1 settings API)
|
||||
// - /org/settings/logo, /org/settings/text (logo/text endpoints)
|
||||
// - /org/settings/v2/... (v2 multi-level settings API)
|
||||
//
|
||||
// Settings v2 API paths:
|
||||
// - GET /{org_id}/settings/v2/{key} - get setting
|
||||
// - GET /{org_id}/settings/v2 - list settings
|
||||
// - POST /{org_id}/settings/v2/org - set org setting
|
||||
// - POST /{org_id}/settings/v2/user/{user_id} - set user setting
|
||||
// - DELETE /{org_id}/settings/v2/org/{key} - delete org setting
|
||||
// - DELETE /{org_id}/settings/v2/user/{user_id}/{key} - delete user setting
|
||||
if path_columns[1].eq("settings") {
|
||||
if method.eq("POST") || method.eq("DELETE") {
|
||||
method = "PUT".to_string();
|
||||
|
||||
@@ -53,7 +53,7 @@ pub type RwAHashSet<K> = tokio::sync::RwLock<HashSet<K>>;
|
||||
pub type RwBTreeMap<K, V> = tokio::sync::RwLock<BTreeMap<K, V>>;
|
||||
|
||||
// for DDL commands and migrations
|
||||
pub const DB_SCHEMA_VERSION: u64 = 15;
|
||||
pub const DB_SCHEMA_VERSION: u64 = 16;
|
||||
pub const DB_SCHEMA_KEY: &str = "/db_schema_version/";
|
||||
|
||||
// global version variables
|
||||
|
||||
@@ -174,159 +174,7 @@ impl SemanticFieldGroup {
|
||||
/// instead.
|
||||
#[allow(dead_code)]
|
||||
pub fn default_presets() -> Vec<Self> {
|
||||
vec![
|
||||
Self::new(
|
||||
"host",
|
||||
"Host",
|
||||
&["host", "hostname", "node", "node_name"],
|
||||
true,
|
||||
),
|
||||
// IP addresses removed - too high cardinality for service dimensions
|
||||
// Users with stable IPs can use "host" dimension instead
|
||||
Self::new(
|
||||
"service",
|
||||
"Service",
|
||||
&[
|
||||
// Generic
|
||||
"service",
|
||||
"service_name",
|
||||
"svc",
|
||||
"app",
|
||||
"application",
|
||||
"app_name",
|
||||
// OpenTelemetry (traces and metrics)
|
||||
"service.name", // OTLP attribute format
|
||||
"attributes_service.name", // Flattened OTLP attributes
|
||||
"resource_service_name",
|
||||
"resource_attributes_service_name",
|
||||
"resource_attributes_service.name",
|
||||
// Prometheus/Metrics
|
||||
"job", // Prometheus job label (common service identifier)
|
||||
"service_instance",
|
||||
"__name__", // Metric name can indicate service
|
||||
// Kubernetes
|
||||
"kubernetes_labels_app",
|
||||
"kubernetes_labels_app_kubernetes_io_name",
|
||||
"k8s_labels_app",
|
||||
// AWS ECS/Fargate
|
||||
"ecs_task_family",
|
||||
"ecs_service_name",
|
||||
"ecs_container_name",
|
||||
// AWS Lambda
|
||||
"lambda_function_name",
|
||||
"aws_lambda_function_name",
|
||||
// GCP Cloud Run/Functions
|
||||
"resource_labels_service_name",
|
||||
"cloud_run_service_name",
|
||||
"resource_labels_function_name",
|
||||
// Azure
|
||||
"ServiceName",
|
||||
"ContainerName",
|
||||
"azure_service_name",
|
||||
// Systemd (VMs)
|
||||
"_SYSTEMD_UNIT",
|
||||
],
|
||||
true,
|
||||
),
|
||||
// Kubernetes groups
|
||||
Self::new(
|
||||
"k8s-cluster",
|
||||
"K8s Cluster",
|
||||
&["k8s_cluster", "cluster", "cluster_name", "cluster_id"],
|
||||
false,
|
||||
),
|
||||
Self::new(
|
||||
"k8s-namespace",
|
||||
"K8s Namespace",
|
||||
&["k8s_namespace", "namespace", "k8s_ns", "ns"],
|
||||
false,
|
||||
),
|
||||
Self::new(
|
||||
"k8s-pod",
|
||||
"K8s Pod",
|
||||
&["k8s_pod", "pod", "pod_name", "pod_id"],
|
||||
false,
|
||||
),
|
||||
Self::new(
|
||||
"k8s-node",
|
||||
"K8s Node",
|
||||
&["k8s_node", "node", "node_name", "kubernetes_node"],
|
||||
false,
|
||||
),
|
||||
Self::new(
|
||||
"k8s-container",
|
||||
"K8s Container",
|
||||
&[
|
||||
"container",
|
||||
"container_name",
|
||||
"container_id",
|
||||
"k8s_container",
|
||||
],
|
||||
false,
|
||||
),
|
||||
// Environment dimension (common across all platforms)
|
||||
Self::new(
|
||||
"environment",
|
||||
"Environment",
|
||||
&[
|
||||
"environment",
|
||||
"env",
|
||||
"stage",
|
||||
"tier",
|
||||
"deployment_environment",
|
||||
"service_deployment_environment", /* OTLP traces: service_ + attribute key
|
||||
* (dots replaced with underscores) */
|
||||
"attributes_deployment_environment", // Flattened OTLP attributes
|
||||
"resource_deployment_environment",
|
||||
"resource_attributes_deployment_environment",
|
||||
"kubernetes_labels_environment",
|
||||
"kubernetes_labels_env",
|
||||
"k8s_labels_environment",
|
||||
"ecs_task_definition_tags_environment",
|
||||
"labels_environment",
|
||||
],
|
||||
true,
|
||||
),
|
||||
// Version dimension
|
||||
Self::new(
|
||||
"version",
|
||||
"Version",
|
||||
&[
|
||||
"version",
|
||||
"app_version",
|
||||
"service_version",
|
||||
"release",
|
||||
// OTLP traces - service_ prefix with dots replaced by underscores
|
||||
"service_service_version", /* OTLP traces: service_ + service.version (dots
|
||||
* → underscores) */
|
||||
"attributes_service_version", // Flattened OTLP attributes
|
||||
"resource_service_version",
|
||||
"resource_attributes_service_version",
|
||||
// Kubernetes
|
||||
"kubernetes_labels_version",
|
||||
"k8s_labels_version",
|
||||
"aws_lambda_version",
|
||||
],
|
||||
true,
|
||||
),
|
||||
// Region dimension (cloud + datacenter)
|
||||
Self::new(
|
||||
"region",
|
||||
"Region",
|
||||
&[
|
||||
"region",
|
||||
"aws_region",
|
||||
"gcp_region",
|
||||
"azure_region",
|
||||
"resource_labels_location",
|
||||
"kubernetes_node_labels_topology_kubernetes_io_region",
|
||||
"kubernetes_node_labels_region",
|
||||
"datacenter",
|
||||
"location",
|
||||
],
|
||||
false,
|
||||
),
|
||||
]
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Load default semantic groups
|
||||
@@ -399,6 +247,22 @@ pub struct GlobalDeduplicationConfig {
|
||||
/// Can be overridden per-alert.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub time_window_minutes: Option<i64>,
|
||||
|
||||
/// FQN (Fully Qualified Name) priority dimensions for service correlation
|
||||
///
|
||||
/// Defines which semantic dimensions are used to derive the service-fqn,
|
||||
/// in priority order. The first dimension with a value wins.
|
||||
///
|
||||
/// Default priority (if empty):
|
||||
/// 1. k8s-deployment, k8s-statefulset, k8s-daemonset, k8s-job (K8s workloads)
|
||||
/// 2. aws-ecs-task, faas-name, gcp-cloud-run, azure-cloud-role (Cloud workloads)
|
||||
/// 3. process-name (Bare metal)
|
||||
/// 4. service (Fallback)
|
||||
///
|
||||
/// Example custom priority: ["k8s-deployment", "aws-ecs-task", "service"]
|
||||
/// This would skip statefulset/daemonset and use deployment directly.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub fqn_priority_dimensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Per-alert deduplication configuration (from main branch)
|
||||
@@ -600,9 +464,25 @@ impl GlobalDeduplicationConfig {
|
||||
alert_dedup_enabled: false,
|
||||
alert_fingerprint_groups: vec![],
|
||||
time_window_minutes: None,
|
||||
fqn_priority_dimensions: Self::default_fqn_priority(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get default FQN priority dimensions
|
||||
///
|
||||
/// For OSS builds, returns empty (must be configured).
|
||||
/// For enterprise builds, ServiceStreamsConfig provides the full default list.
|
||||
///
|
||||
/// Default priority order for deriving service-fqn (first match wins):
|
||||
/// 1. K8s workload: deployment, statefulset, daemonset, job
|
||||
/// 2. Cloud workload: ECS task family, Lambda function, Cloud Run service
|
||||
/// 3. Bare metal: process name
|
||||
/// 4. Fallback: service dimension
|
||||
pub fn default_fqn_priority() -> Vec<String> {
|
||||
// OSS builds return empty - enterprise provides defaults via ServiceStreamsConfig
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Map a field name to its semantic group ID
|
||||
///
|
||||
/// Returns the first matching semantic group ID, or None if no match.
|
||||
@@ -743,6 +623,7 @@ mod tests {
|
||||
],
|
||||
alert_fingerprint_groups: vec![],
|
||||
time_window_minutes: Some(10),
|
||||
fqn_priority_dimensions: vec![],
|
||||
};
|
||||
|
||||
assert!(config.validate().is_ok());
|
||||
@@ -814,6 +695,7 @@ mod tests {
|
||||
],
|
||||
alert_fingerprint_groups: vec![],
|
||||
time_window_minutes: Some(10),
|
||||
fqn_priority_dimensions: vec![],
|
||||
};
|
||||
|
||||
// Should succeed - overlaps are allowed, just logged as warnings
|
||||
@@ -831,6 +713,7 @@ mod tests {
|
||||
],
|
||||
alert_fingerprint_groups: vec![],
|
||||
time_window_minutes: Some(10),
|
||||
fqn_priority_dimensions: vec![],
|
||||
};
|
||||
|
||||
let json = serde_json::to_string_pretty(&config).unwrap();
|
||||
@@ -896,19 +779,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_default_presets() {
|
||||
// OSS builds return empty defaults - semantic groups are loaded from
|
||||
// enterprise JSON or configured via API
|
||||
let presets = SemanticFieldGroup::default_presets();
|
||||
assert!(!presets.is_empty());
|
||||
|
||||
// Check that we have common groups
|
||||
assert!(presets.iter().any(|g| g.id == "host"));
|
||||
assert!(presets.iter().any(|g| g.id == "service"));
|
||||
assert!(presets.iter().any(|g| g.id == "k8s-cluster"));
|
||||
|
||||
// Validate all preset IDs
|
||||
for preset in &presets {
|
||||
assert!(SemanticFieldGroup::validate_id(&preset.id));
|
||||
assert!(!preset.display.is_empty());
|
||||
assert!(!preset.fields.is_empty());
|
||||
}
|
||||
assert!(presets.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod service_streams;
|
||||
pub mod short_url;
|
||||
pub mod sql;
|
||||
pub mod stream;
|
||||
pub mod system_settings;
|
||||
pub mod timed_annotations;
|
||||
pub mod triggers;
|
||||
pub mod user;
|
||||
|
||||
@@ -297,6 +297,81 @@ pub enum CardinalityClass {
|
||||
VeryHigh,
|
||||
}
|
||||
|
||||
/// Response for grouped services API
|
||||
/// Groups services by their FQN (Fully Qualified Name) for correlation visualization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct GroupedServicesResponse {
|
||||
/// Services grouped by FQN
|
||||
pub groups: Vec<ServiceFqnGroup>,
|
||||
|
||||
/// Total number of unique FQNs
|
||||
pub total_fqns: usize,
|
||||
|
||||
/// Total number of services (across all FQNs)
|
||||
pub total_services: usize,
|
||||
}
|
||||
|
||||
/// A group of services sharing the same FQN
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ServiceFqnGroup {
|
||||
/// The Fully Qualified Name (e.g., "o2-openobserve-querier")
|
||||
pub fqn: String,
|
||||
|
||||
/// Services that share this FQN
|
||||
pub services: Vec<ServiceInGroup>,
|
||||
|
||||
/// Summary of streams in this group
|
||||
pub stream_summary: StreamSummary,
|
||||
}
|
||||
|
||||
/// A service within an FQN group
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ServiceInGroup {
|
||||
/// Service name (e.g., "openobserve", "querier")
|
||||
pub service_name: String,
|
||||
|
||||
/// How the FQN was derived (e.g., "k8s-statefulset", "k8s-deployment", "service")
|
||||
pub derived_from: String,
|
||||
|
||||
/// Streams by type
|
||||
pub streams: ServiceStreams,
|
||||
|
||||
/// Key dimensions for this service
|
||||
pub dimensions: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Streams organized by type
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
|
||||
pub struct ServiceStreams {
|
||||
/// Log stream names
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub logs: Vec<String>,
|
||||
|
||||
/// Trace stream names
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub traces: Vec<String>,
|
||||
|
||||
/// Metric stream names
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub metrics: Vec<String>,
|
||||
}
|
||||
|
||||
/// Summary of streams in an FQN group
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, Default)]
|
||||
pub struct StreamSummary {
|
||||
/// Number of log streams
|
||||
pub logs_count: usize,
|
||||
|
||||
/// Number of trace streams
|
||||
pub traces_count: usize,
|
||||
|
||||
/// Number of metric streams
|
||||
pub metrics_count: usize,
|
||||
|
||||
/// Whether this group has all three telemetry types
|
||||
pub has_full_correlation: bool,
|
||||
}
|
||||
|
||||
impl CardinalityClass {
|
||||
/// Classify cardinality based on unique value count
|
||||
pub fn from_cardinality(cardinality: usize) -> Self {
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! System Settings Types
|
||||
//!
|
||||
//! This module defines the types for the multi-level settings system.
|
||||
//! Settings can be stored at three levels:
|
||||
//! - System: Global defaults for all organizations
|
||||
//! - Org: Organization-specific overrides
|
||||
//! - User: User-specific preferences within an organization
|
||||
//!
|
||||
//! Resolution order (most specific wins):
|
||||
//! 1. User level (if user_id provided)
|
||||
//! 2. Org level (if org_id provided)
|
||||
//! 3. System level (global defaults)
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Setting scope levels
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SettingScope {
|
||||
/// Global system-level settings (defaults for all orgs)
|
||||
System,
|
||||
/// Organization-level settings (overrides system defaults)
|
||||
Org,
|
||||
/// User-level settings within an organization
|
||||
User,
|
||||
}
|
||||
|
||||
impl SettingScope {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SettingScope::System => "system",
|
||||
SettingScope::Org => "org",
|
||||
SettingScope::User => "user",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SettingScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SettingScope {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"system" => Ok(SettingScope::System),
|
||||
"org" => Ok(SettingScope::Org),
|
||||
"user" => Ok(SettingScope::User),
|
||||
_ => Err(format!("Invalid setting scope: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Common setting categories for organizing settings
|
||||
///
|
||||
/// NOTE: This enum provides a set of common categories but is not exhaustive.
|
||||
/// The `setting_category` field in `SystemSetting` is an `Option<String>` to allow
|
||||
/// for custom categories beyond these predefined ones. Use these constants for
|
||||
/// consistency when applicable, but feel free to use custom category strings
|
||||
/// for domain-specific settings.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SettingCategory {
|
||||
/// Correlation and service identity settings
|
||||
Correlation,
|
||||
/// UI/UX preferences
|
||||
Ui,
|
||||
/// Alert and notification settings
|
||||
Alerts,
|
||||
/// Data retention settings
|
||||
Retention,
|
||||
/// Search settings
|
||||
Search,
|
||||
/// Ingestion settings
|
||||
Ingestion,
|
||||
/// General/miscellaneous settings
|
||||
General,
|
||||
}
|
||||
|
||||
impl SettingCategory {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SettingCategory::Correlation => "correlation",
|
||||
SettingCategory::Ui => "ui",
|
||||
SettingCategory::Alerts => "alerts",
|
||||
SettingCategory::Retention => "retention",
|
||||
SettingCategory::Search => "search",
|
||||
SettingCategory::Ingestion => "ingestion",
|
||||
SettingCategory::General => "general",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SettingCategory {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SettingCategory {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"correlation" => Ok(SettingCategory::Correlation),
|
||||
"ui" => Ok(SettingCategory::Ui),
|
||||
"alerts" => Ok(SettingCategory::Alerts),
|
||||
"retention" => Ok(SettingCategory::Retention),
|
||||
"search" => Ok(SettingCategory::Search),
|
||||
"ingestion" => Ok(SettingCategory::Ingestion),
|
||||
"general" => Ok(SettingCategory::General),
|
||||
_ => Err(format!("Invalid setting category: {}", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Well-known setting keys
|
||||
pub mod keys {
|
||||
/// FQN priority dimensions for service correlation
|
||||
pub const FQN_PRIORITY_DIMENSIONS: &str = "fqn_priority_dimensions";
|
||||
/// Semantic field groups for dimension extraction
|
||||
pub const SEMANTIC_FIELD_GROUPS: &str = "semantic_field_groups";
|
||||
/// UI theme preference
|
||||
pub const THEME: &str = "theme";
|
||||
/// Scrape interval for metrics
|
||||
pub const SCRAPE_INTERVAL: &str = "scrape_interval";
|
||||
/// Trace ID field name
|
||||
pub const TRACE_ID_FIELD_NAME: &str = "trace_id_field_name";
|
||||
/// Span ID field name
|
||||
pub const SPAN_ID_FIELD_NAME: &str = "span_id_field_name";
|
||||
/// Toggle ingestion logs
|
||||
pub const TOGGLE_INGESTION_LOGS: &str = "toggle_ingestion_logs";
|
||||
/// Streaming aggregation enabled
|
||||
pub const STREAMING_AGGREGATION_ENABLED: &str = "streaming_aggregation_enabled";
|
||||
/// Enable streaming search
|
||||
pub const ENABLE_STREAMING_SEARCH: &str = "enable_streaming_search";
|
||||
/// Minimum auto refresh interval
|
||||
pub const MIN_AUTO_REFRESH_INTERVAL: &str = "min_auto_refresh_interval";
|
||||
/// Light mode theme color
|
||||
pub const LIGHT_MODE_THEME_COLOR: &str = "light_mode_theme_color";
|
||||
/// Dark mode theme color
|
||||
pub const DARK_MODE_THEME_COLOR: &str = "dark_mode_theme_color";
|
||||
}
|
||||
|
||||
/// A system setting record
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SystemSetting {
|
||||
/// Unique identifier
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<i64>,
|
||||
/// Setting scope (system, org, user)
|
||||
pub scope: SettingScope,
|
||||
/// Organization ID (required for org/user scope)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub org_id: Option<String>,
|
||||
/// User ID (required for user scope)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
/// Setting key identifier
|
||||
pub setting_key: String,
|
||||
/// Optional category for grouping
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub setting_category: Option<String>,
|
||||
/// The setting value (JSON)
|
||||
pub setting_value: serde_json::Value,
|
||||
/// Optional description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
/// Created timestamp (microseconds)
|
||||
#[serde(default)]
|
||||
pub created_at: i64,
|
||||
/// Updated timestamp (microseconds)
|
||||
#[serde(default)]
|
||||
pub updated_at: i64,
|
||||
/// User who created this setting
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_by: Option<String>,
|
||||
/// User who last updated this setting
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_by: Option<String>,
|
||||
}
|
||||
|
||||
impl SystemSetting {
|
||||
/// Create a new system-level setting
|
||||
pub fn new_system(key: impl Into<String>, value: serde_json::Value) -> Self {
|
||||
let now = chrono::Utc::now().timestamp_micros();
|
||||
Self {
|
||||
id: None,
|
||||
scope: SettingScope::System,
|
||||
org_id: None,
|
||||
user_id: None,
|
||||
setting_key: key.into(),
|
||||
setting_category: None,
|
||||
setting_value: value,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new org-level setting
|
||||
pub fn new_org(
|
||||
org_id: impl Into<String>,
|
||||
key: impl Into<String>,
|
||||
value: serde_json::Value,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now().timestamp_micros();
|
||||
Self {
|
||||
id: None,
|
||||
scope: SettingScope::Org,
|
||||
org_id: Some(org_id.into()),
|
||||
user_id: None,
|
||||
setting_key: key.into(),
|
||||
setting_category: None,
|
||||
setting_value: value,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new user-level setting
|
||||
pub fn new_user(
|
||||
org_id: impl Into<String>,
|
||||
user_id: impl Into<String>,
|
||||
key: impl Into<String>,
|
||||
value: serde_json::Value,
|
||||
) -> Self {
|
||||
let now = chrono::Utc::now().timestamp_micros();
|
||||
Self {
|
||||
id: None,
|
||||
scope: SettingScope::User,
|
||||
org_id: Some(org_id.into()),
|
||||
user_id: Some(user_id.into()),
|
||||
setting_key: key.into(),
|
||||
setting_category: None,
|
||||
setting_value: value,
|
||||
description: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the category for this setting
|
||||
pub fn with_category(mut self, category: SettingCategory) -> Self {
|
||||
self.setting_category = Some(category.as_str().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the description for this setting
|
||||
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Validate the setting based on scope rules
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
match self.scope {
|
||||
SettingScope::System => {
|
||||
if self.org_id.is_some() || self.user_id.is_some() {
|
||||
return Err("System-level settings cannot have org_id or user_id".to_string());
|
||||
}
|
||||
}
|
||||
SettingScope::Org => {
|
||||
if self.org_id.is_none() {
|
||||
return Err("Org-level settings require org_id".to_string());
|
||||
}
|
||||
if self.user_id.is_some() {
|
||||
return Err("Org-level settings cannot have user_id".to_string());
|
||||
}
|
||||
}
|
||||
SettingScope::User => {
|
||||
if self.org_id.is_none() {
|
||||
return Err("User-level settings require org_id".to_string());
|
||||
}
|
||||
if self.user_id.is_none() {
|
||||
return Err("User-level settings require user_id".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to create or update a setting
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct SystemSettingPayload {
|
||||
/// Setting key
|
||||
pub setting_key: String,
|
||||
/// Setting value (JSON)
|
||||
pub setting_value: serde_json::Value,
|
||||
/// Optional category
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub setting_category: Option<String>,
|
||||
/// Optional description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Query parameters for fetching settings
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SystemSettingQuery {
|
||||
/// Filter by specific key
|
||||
pub key: Option<String>,
|
||||
/// Filter by category
|
||||
pub category: Option<String>,
|
||||
/// User ID for user-level resolution
|
||||
pub user_id: Option<String>,
|
||||
/// Include system-level defaults in response
|
||||
#[serde(default = "default_include_defaults")]
|
||||
pub include_defaults: bool,
|
||||
}
|
||||
|
||||
fn default_include_defaults() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Response containing resolved settings
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ResolvedSettings {
|
||||
/// The resolved settings (most specific value for each key)
|
||||
pub settings: std::collections::HashMap<String, serde_json::Value>,
|
||||
/// Source information for each setting (which level it came from)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sources: Option<std::collections::HashMap<String, SettingScope>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_setting_scope_from_str() {
|
||||
assert_eq!(
|
||||
"system".parse::<SettingScope>().unwrap(),
|
||||
SettingScope::System
|
||||
);
|
||||
assert_eq!("org".parse::<SettingScope>().unwrap(), SettingScope::Org);
|
||||
assert_eq!("user".parse::<SettingScope>().unwrap(), SettingScope::User);
|
||||
assert!("invalid".parse::<SettingScope>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_setting_validation() {
|
||||
// Valid system setting
|
||||
let system_setting = SystemSetting::new_system("test_key", serde_json::json!("value"));
|
||||
assert!(system_setting.validate().is_ok());
|
||||
|
||||
// Valid org setting
|
||||
let org_setting = SystemSetting::new_org("default", "test_key", serde_json::json!("value"));
|
||||
assert!(org_setting.validate().is_ok());
|
||||
|
||||
// Valid user setting
|
||||
let user_setting = SystemSetting::new_user(
|
||||
"default",
|
||||
"user@example.com",
|
||||
"test_key",
|
||||
serde_json::json!("value"),
|
||||
);
|
||||
assert!(user_setting.validate().is_ok());
|
||||
|
||||
// Invalid: system with org_id
|
||||
let mut invalid = SystemSetting::new_system("test_key", serde_json::json!("value"));
|
||||
invalid.org_id = Some("default".to_string());
|
||||
assert!(invalid.validate().is_err());
|
||||
|
||||
// Invalid: org without org_id
|
||||
let mut invalid = SystemSetting::new_org("default", "test_key", serde_json::json!("value"));
|
||||
invalid.org_id = None;
|
||||
assert!(invalid.validate().is_err());
|
||||
|
||||
// Invalid: user without user_id
|
||||
let mut invalid = SystemSetting::new_user(
|
||||
"default",
|
||||
"user@example.com",
|
||||
"test_key",
|
||||
serde_json::json!("value"),
|
||||
);
|
||||
invalid.user_id = None;
|
||||
assert!(invalid.validate().is_err());
|
||||
}
|
||||
}
|
||||
@@ -391,8 +391,10 @@ pub async fn preview_semantic_groups_diff(
|
||||
|
||||
/// Save semantic field groups for an organization
|
||||
///
|
||||
/// Replaces all semantic groups with the provided list.
|
||||
/// Usually called after user reviews the diff from preview-diff endpoint.
|
||||
/// Merges provided groups with existing ones:
|
||||
/// - Groups with matching IDs are updated (replaced)
|
||||
/// - New groups (no matching ID) are added
|
||||
/// - Existing groups not in the request are preserved
|
||||
#[cfg(feature = "enterprise")]
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
@@ -402,7 +404,7 @@ pub async fn preview_semantic_groups_diff(
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization ID"),
|
||||
),
|
||||
request_body(content = Vec<SemanticFieldGroup>, description = "Semantic groups to save", content_type = "application/json"),
|
||||
request_body(content = Vec<SemanticFieldGroup>, description = "Semantic groups to save (merged with existing)", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Success"),
|
||||
(status = 400, description = "Bad request - Invalid semantic groups"),
|
||||
@@ -415,10 +417,10 @@ pub async fn save_semantic_groups(
|
||||
groups: web::Json<Vec<SemanticFieldGroup>>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let org_id = org_id.into_inner();
|
||||
let new_groups = groups.into_inner();
|
||||
let incoming_groups = groups.into_inner();
|
||||
|
||||
// Validate all group IDs
|
||||
for group in &new_groups {
|
||||
for group in &incoming_groups {
|
||||
if !SemanticFieldGroup::validate_id(&group.id) {
|
||||
return Ok(HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": format!("Invalid semantic group ID: '{}'. IDs must be lowercase, alphanumeric, dash-separated", group.id)
|
||||
@@ -430,7 +432,7 @@ pub async fn save_semantic_groups(
|
||||
let mut config =
|
||||
match crate::service::alerts::org_config::get_deduplication_config(&org_id).await {
|
||||
Ok(Some(cfg)) => cfg,
|
||||
Ok(None) => GlobalDeduplicationConfig::default(),
|
||||
Ok(None) => GlobalDeduplicationConfig::default_with_presets(),
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Error getting deduplication config for org {}: {}",
|
||||
@@ -443,14 +445,43 @@ pub async fn save_semantic_groups(
|
||||
}
|
||||
};
|
||||
|
||||
// Replace semantic groups
|
||||
config.semantic_field_groups = new_groups;
|
||||
// Merge incoming groups with existing ones
|
||||
// Build a map of incoming groups by ID for quick lookup
|
||||
let incoming_map: std::collections::HashMap<String, SemanticFieldGroup> = incoming_groups
|
||||
.into_iter()
|
||||
.map(|g| (g.id.clone(), g))
|
||||
.collect();
|
||||
|
||||
// Update existing groups if they're in incoming, keep others unchanged
|
||||
let mut merged_groups: Vec<SemanticFieldGroup> = config
|
||||
.semantic_field_groups
|
||||
.into_iter()
|
||||
.map(|existing| {
|
||||
if let Some(updated) = incoming_map.get(&existing.id) {
|
||||
updated.clone()
|
||||
} else {
|
||||
existing
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Add new groups that don't exist in current config
|
||||
let existing_ids: std::collections::HashSet<String> =
|
||||
merged_groups.iter().map(|g| g.id.clone()).collect();
|
||||
for (id, group) in incoming_map {
|
||||
if !existing_ids.contains(&id) {
|
||||
merged_groups.push(group);
|
||||
}
|
||||
}
|
||||
|
||||
let total_groups = merged_groups.len();
|
||||
config.semantic_field_groups = merged_groups;
|
||||
|
||||
// Save config
|
||||
match crate::service::alerts::org_config::set_deduplication_config(&org_id, &config).await {
|
||||
Ok(()) => Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
"message": "Semantic groups saved successfully",
|
||||
"total_groups": config.semantic_field_groups.len()
|
||||
"total_groups": total_groups
|
||||
}))),
|
||||
Err(e) => {
|
||||
log::error!("Error saving semantic groups for org {}: {}", org_id, e);
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
pub mod es;
|
||||
pub mod org;
|
||||
pub mod settings;
|
||||
pub mod system_settings;
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! System Settings HTTP Handler
|
||||
//!
|
||||
//! Provides REST API endpoints for multi-level system settings.
|
||||
//! Settings resolution order (most specific wins): User -> Org -> System
|
||||
|
||||
use std::io::Error as StdErr;
|
||||
|
||||
use actix_web::{HttpResponse, delete, get, post, web};
|
||||
use config::meta::system_settings::{
|
||||
SettingScope, SystemSetting, SystemSettingPayload, SystemSettingQuery,
|
||||
};
|
||||
|
||||
use crate::{common::meta::http::HttpResponse as MetaHttpResponse, service::db::system_settings};
|
||||
|
||||
/// Get a specific system setting with resolution (user -> org -> system)
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
tag = "Organizations",
|
||||
operation_id = "SystemSettingGetResolved",
|
||||
summary = "Get resolved system setting",
|
||||
description = "Retrieves a setting value with multi-level resolution. Checks user-level first, \
|
||||
then org-level, then system-level defaults. Returns the most specific setting found.",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
("key" = String, Path, description = "Setting key"),
|
||||
("user_id" = Option<String>, Query, description = "User ID for user-level resolution"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = SystemSetting),
|
||||
(status = 404, description = "Not Found", content_type = "application/json", body = ()),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = ()),
|
||||
),
|
||||
extensions(
|
||||
("x-o2-ratelimit" = json!({"module": "Settings", "operation": "get"}))
|
||||
)
|
||||
)]
|
||||
#[get("/{org_id}/settings/v2/{key}")]
|
||||
async fn get_setting(
|
||||
path: web::Path<(String, String)>,
|
||||
query: web::Query<SystemSettingQuery>,
|
||||
) -> Result<HttpResponse, StdErr> {
|
||||
let (org_id, key) = path.into_inner();
|
||||
let user_id = query.user_id.as_deref();
|
||||
|
||||
match system_settings::get_resolved(Some(&org_id), user_id, &key).await {
|
||||
Ok(Some(setting)) => Ok(HttpResponse::Ok().json(setting)),
|
||||
Ok(None) => Ok(MetaHttpResponse::not_found("Setting not found")),
|
||||
Err(e) => Ok(MetaHttpResponse::bad_request(e.to_string().as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// List all resolved settings for an organization/user
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
tag = "Organizations",
|
||||
operation_id = "SystemSettingListResolved",
|
||||
summary = "List all resolved system settings",
|
||||
description = "Lists all settings with multi-level resolution applied. Merges system, org, and user \
|
||||
levels, returning the most specific value for each key.",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
("user_id" = Option<String>, Query, description = "User ID for user-level resolution"),
|
||||
("category" = Option<String>, Query, description = "Filter by setting category"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = Object),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = ()),
|
||||
),
|
||||
extensions(
|
||||
("x-o2-ratelimit" = json!({"module": "Settings", "operation": "list"}))
|
||||
)
|
||||
)]
|
||||
#[get("/{org_id}/settings/v2")]
|
||||
async fn list_settings(
|
||||
path: web::Path<String>,
|
||||
query: web::Query<SystemSettingQuery>,
|
||||
) -> Result<HttpResponse, StdErr> {
|
||||
let org_id = path.into_inner();
|
||||
let user_id = query.user_id.as_deref();
|
||||
let category = query.category.as_deref();
|
||||
|
||||
match system_settings::list_resolved(Some(&org_id), user_id, category).await {
|
||||
Ok(settings) => Ok(HttpResponse::Ok().json(settings)),
|
||||
Err(e) => Ok(MetaHttpResponse::bad_request(e.to_string().as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create or update a setting at org level
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
tag = "Organizations",
|
||||
operation_id = "SystemSettingSetOrg",
|
||||
summary = "Set organization-level setting",
|
||||
description = "Creates or updates an organization-level setting. This setting applies to all users \
|
||||
in the organization unless overridden at the user level.",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
),
|
||||
request_body(content = SystemSettingPayload, description = "Setting to create/update", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = SystemSetting),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = ()),
|
||||
),
|
||||
extensions(
|
||||
("x-o2-ratelimit" = json!({"module": "Settings", "operation": "create"}))
|
||||
)
|
||||
)]
|
||||
#[post("/{org_id}/settings/v2")]
|
||||
async fn set_org_setting(
|
||||
path: web::Path<String>,
|
||||
payload: web::Json<SystemSettingPayload>,
|
||||
) -> Result<HttpResponse, StdErr> {
|
||||
let org_id = path.into_inner();
|
||||
let payload = payload.into_inner();
|
||||
|
||||
let mut setting = SystemSetting::new_org(&org_id, &payload.setting_key, payload.setting_value);
|
||||
if let Some(cat) = payload.setting_category.as_deref() {
|
||||
setting.setting_category = Some(cat.to_string());
|
||||
}
|
||||
if let Some(desc) = payload.description.as_deref() {
|
||||
setting.description = Some(desc.to_string());
|
||||
}
|
||||
|
||||
match system_settings::set(&setting).await {
|
||||
Ok(result) => Ok(HttpResponse::Ok().json(result)),
|
||||
Err(e) => Ok(MetaHttpResponse::bad_request(e.to_string().as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create or update a setting at user level
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
tag = "Organizations",
|
||||
operation_id = "SystemSettingSetUser",
|
||||
summary = "Set user-level setting",
|
||||
description = "Creates or updates a user-level setting. This setting applies only to the specific \
|
||||
user and overrides org-level and system-level settings.",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
("user_id" = String, Path, description = "User ID"),
|
||||
),
|
||||
request_body(content = SystemSettingPayload, description = "Setting to create/update", content_type = "application/json"),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = SystemSetting),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = ()),
|
||||
),
|
||||
extensions(
|
||||
("x-o2-ratelimit" = json!({"module": "Settings", "operation": "create"}))
|
||||
)
|
||||
)]
|
||||
#[post("/{org_id}/settings/v2/user/{user_id}")]
|
||||
async fn set_user_setting(
|
||||
path: web::Path<(String, String)>,
|
||||
payload: web::Json<SystemSettingPayload>,
|
||||
) -> Result<HttpResponse, StdErr> {
|
||||
let (org_id, user_id) = path.into_inner();
|
||||
let payload = payload.into_inner();
|
||||
|
||||
let mut setting = SystemSetting::new_user(
|
||||
&org_id,
|
||||
&user_id,
|
||||
&payload.setting_key,
|
||||
payload.setting_value,
|
||||
);
|
||||
if let Some(cat) = payload.setting_category.as_deref() {
|
||||
setting.setting_category = Some(cat.to_string());
|
||||
}
|
||||
if let Some(desc) = payload.description.as_deref() {
|
||||
setting.description = Some(desc.to_string());
|
||||
}
|
||||
|
||||
match system_settings::set(&setting).await {
|
||||
Ok(result) => Ok(HttpResponse::Ok().json(result)),
|
||||
Err(e) => Ok(MetaHttpResponse::bad_request(e.to_string().as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete an organization-level setting
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
tag = "Organizations",
|
||||
operation_id = "SystemSettingDeleteOrg",
|
||||
summary = "Delete organization-level setting",
|
||||
description = "Deletes an organization-level setting. After deletion, queries for this key will \
|
||||
fall back to system-level defaults.",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
("key" = String, Path, description = "Setting key"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = Object),
|
||||
(status = 404, description = "Not Found", content_type = "application/json", body = ()),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = ()),
|
||||
),
|
||||
extensions(
|
||||
("x-o2-ratelimit" = json!({"module": "Settings", "operation": "delete"}))
|
||||
)
|
||||
)]
|
||||
#[delete("/{org_id}/settings/v2/{key}")]
|
||||
async fn delete_org_setting(path: web::Path<(String, String)>) -> Result<HttpResponse, StdErr> {
|
||||
let (org_id, key) = path.into_inner();
|
||||
|
||||
match system_settings::delete(&SettingScope::Org, Some(&org_id), None, &key).await {
|
||||
Ok(true) => Ok(HttpResponse::Ok().json(serde_json::json!({"deleted": true}))),
|
||||
Ok(false) => Ok(MetaHttpResponse::not_found("Setting not found")),
|
||||
Err(e) => Ok(MetaHttpResponse::bad_request(e.to_string().as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a user-level setting
|
||||
#[utoipa::path(
|
||||
context_path = "/api",
|
||||
tag = "Organizations",
|
||||
operation_id = "SystemSettingDeleteUser",
|
||||
summary = "Delete user-level setting",
|
||||
description = "Deletes a user-level setting. After deletion, queries for this key will fall back \
|
||||
to org-level or system-level settings.",
|
||||
security(
|
||||
("Authorization"= [])
|
||||
),
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization name"),
|
||||
("user_id" = String, Path, description = "User ID"),
|
||||
("key" = String, Path, description = "Setting key"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Success", content_type = "application/json", body = Object),
|
||||
(status = 404, description = "Not Found", content_type = "application/json", body = ()),
|
||||
(status = 400, description = "Failure", content_type = "application/json", body = ()),
|
||||
),
|
||||
extensions(
|
||||
("x-o2-ratelimit" = json!({"module": "Settings", "operation": "delete"}))
|
||||
)
|
||||
)]
|
||||
#[delete("/{org_id}/settings/v2/user/{user_id}/{key}")]
|
||||
async fn delete_user_setting(
|
||||
path: web::Path<(String, String, String)>,
|
||||
) -> Result<HttpResponse, StdErr> {
|
||||
let (org_id, user_id, key) = path.into_inner();
|
||||
|
||||
match system_settings::delete(&SettingScope::User, Some(&org_id), Some(&user_id), &key).await {
|
||||
Ok(true) => Ok(HttpResponse::Ok().json(serde_json::json!({"deleted": true}))),
|
||||
Ok(false) => Ok(MetaHttpResponse::not_found("Setting not found")),
|
||||
Err(e) => Ok(MetaHttpResponse::bad_request(e.to_string().as_str())),
|
||||
}
|
||||
}
|
||||
@@ -183,9 +183,70 @@ pub struct CorrelationRequest {
|
||||
pub available_dimensions: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// GET /api/{org_id}/service_streams/_grouped
|
||||
///
|
||||
/// Get services grouped by their Fully Qualified Name (FQN)
|
||||
///
|
||||
/// This endpoint is used by the Correlation Settings UI to display
|
||||
/// which services are correlated together via their shared FQN.
|
||||
///
|
||||
/// Response includes:
|
||||
/// - Services grouped by FQN
|
||||
/// - Each group shows which services share the FQN
|
||||
/// - Stream counts per group (logs/traces/metrics)
|
||||
/// - Whether each group has full telemetry coverage
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/{org_id}/service_streams/_grouped",
|
||||
tag = "Service Streams",
|
||||
params(
|
||||
("org_id" = String, Path, description = "Organization ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Services grouped by FQN", body = GroupedServicesResponse),
|
||||
(status = 401, description = "Unauthorized - Authentication required"),
|
||||
(status = 403, description = "Forbidden - Enterprise feature"),
|
||||
(status = 500, description = "Internal server error")
|
||||
),
|
||||
security(
|
||||
("Authorization" = [])
|
||||
)
|
||||
)]
|
||||
#[get("/{org_id}/service_streams/_grouped")]
|
||||
pub async fn get_services_grouped(
|
||||
org_id: web::Path<String>,
|
||||
Headers(_user_email): Headers<UserEmail>, // Require authentication
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let org_id = org_id.into_inner();
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
match o2_enterprise::enterprise::service_streams::storage::ServiceStorage::list_grouped_by_fqn(&org_id)
|
||||
.await
|
||||
{
|
||||
Ok(response) => Ok(MetaHttpResponse::json(response)),
|
||||
Err(e) => Ok(
|
||||
HttpResponse::InternalServerError().json(MetaHttpResponse::error(
|
||||
500u16,
|
||||
format!("Failed to get grouped services: {}", e),
|
||||
)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
Ok(HttpResponse::Forbidden().json(MetaHttpResponse::error(
|
||||
403u16,
|
||||
"Service Discovery is an enterprise-only feature".to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export shared types from config for API documentation (utoipa)
|
||||
// These types are the same for both enterprise and non-enterprise builds
|
||||
pub use config::meta::service_streams::{
|
||||
CardinalityClass, CorrelationResponse, DimensionAnalytics, DimensionAnalyticsSummary,
|
||||
RelatedStreams, StreamInfo,
|
||||
GroupedServicesResponse, RelatedStreams, ServiceFqnGroup, ServiceInGroup, ServiceStreams,
|
||||
StreamInfo, StreamSummary,
|
||||
};
|
||||
|
||||
@@ -163,6 +163,12 @@ struct ConfigResponse<'a> {
|
||||
service_streams_enabled: bool,
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
service_streams_enabled: bool,
|
||||
/// Available FQN priority dimensions from O2_FQN_PRIORITY_DIMENSIONS env var
|
||||
/// Used by UI to populate the FQN priority dimension selector
|
||||
#[cfg(feature = "enterprise")]
|
||||
fqn_priority_dimensions: Vec<String>,
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
fqn_priority_dimensions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, serde::Deserialize)]
|
||||
@@ -430,6 +436,12 @@ pub async fn zo_config() -> Result<HttpResponse, Error> {
|
||||
mysql_deprecated_warning: cfg.common.meta_store.starts_with("mysql"),
|
||||
service_graph_enabled,
|
||||
service_streams_enabled,
|
||||
#[cfg(feature = "enterprise")]
|
||||
fqn_priority_dimensions: o2_enterprise::enterprise::common::config::get_config()
|
||||
.service_streams
|
||||
.get_fqn_priority_dimensions(),
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
fqn_priority_dimensions: vec![],
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -406,6 +406,13 @@ pub fn get_service_routes(svc: &mut web::ServiceConfig) {
|
||||
.service(organization::settings::delete_logo)
|
||||
.service(organization::settings::set_logo_text)
|
||||
.service(organization::settings::delete_logo_text)
|
||||
// System settings v2 (multi-level)
|
||||
.service(organization::system_settings::get_setting)
|
||||
.service(organization::system_settings::list_settings)
|
||||
.service(organization::system_settings::set_org_setting)
|
||||
.service(organization::system_settings::set_user_setting)
|
||||
.service(organization::system_settings::delete_org_setting)
|
||||
.service(organization::system_settings::delete_user_setting)
|
||||
.service(organization::org::org_summary)
|
||||
.service(organization::org::get_user_passcode)
|
||||
.service(organization::org::update_user_passcode)
|
||||
@@ -646,7 +653,8 @@ pub fn get_service_routes(svc: &mut web::ServiceConfig) {
|
||||
.service(alerts::deduplication::preview_semantic_groups_diff)
|
||||
.service(alerts::deduplication::save_semantic_groups)
|
||||
.service(service_streams::get_dimension_analytics)
|
||||
.service(service_streams::correlate_streams);
|
||||
.service(service_streams::correlate_streams)
|
||||
.service(service_streams::get_services_grouped);
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
let service = service
|
||||
|
||||
@@ -40,6 +40,12 @@ use crate::{common::meta, handler::http::request};
|
||||
request::organization::org::create_user_rumtoken,
|
||||
request::organization::settings::get,
|
||||
request::organization::settings::create,
|
||||
request::organization::system_settings::get_setting,
|
||||
request::organization::system_settings::list_settings,
|
||||
request::organization::system_settings::set_org_setting,
|
||||
request::organization::system_settings::set_user_setting,
|
||||
request::organization::system_settings::delete_org_setting,
|
||||
request::organization::system_settings::delete_user_setting,
|
||||
request::stream::list,
|
||||
request::stream::schema,
|
||||
request::stream::create,
|
||||
|
||||
@@ -26,6 +26,7 @@ pub mod search_jobs;
|
||||
pub mod search_queue;
|
||||
pub mod sessions;
|
||||
pub mod system_prompts;
|
||||
pub mod system_settings;
|
||||
pub mod templates;
|
||||
pub mod timed_annotation_panels;
|
||||
pub mod timed_annotations;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! SeaORM Entity for system_settings table
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "system_settings")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i64,
|
||||
pub scope: String,
|
||||
pub org_id: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
pub setting_key: String,
|
||||
pub setting_category: Option<String>,
|
||||
pub setting_value: String, // JSON stored as text
|
||||
pub description: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub created_by: Option<String>,
|
||||
pub updated_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -0,0 +1,259 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use sea_orm::DbBackend;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use super::get_text_type;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(create_system_settings_table(manager.get_database_backend()))
|
||||
.await?;
|
||||
|
||||
// Create indexes
|
||||
manager.create_index(create_scope_index()).await?;
|
||||
|
||||
manager.create_index(create_org_index()).await?;
|
||||
|
||||
manager.create_index(create_user_index()).await?;
|
||||
|
||||
manager.create_index(create_key_index()).await?;
|
||||
|
||||
manager.create_index(create_category_index()).await?;
|
||||
|
||||
// Create unique constraint index
|
||||
manager.create_index(create_unique_setting_index()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_system_settings_unique")
|
||||
.table(SystemSettings::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_system_settings_category")
|
||||
.table(SystemSettings::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_system_settings_key")
|
||||
.table(SystemSettings::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_system_settings_user")
|
||||
.table(SystemSettings::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_system_settings_org")
|
||||
.table(SystemSettings::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_index(
|
||||
Index::drop()
|
||||
.name("idx_system_settings_scope")
|
||||
.table(SystemSettings::Table)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.drop_table(Table::drop().table(SystemSettings::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn create_system_settings_table(backend: DbBackend) -> TableCreateStatement {
|
||||
let text_type = get_text_type();
|
||||
|
||||
let mut table = Table::create()
|
||||
.table(SystemSettings::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::Id)
|
||||
.big_integer()
|
||||
.auto_increment()
|
||||
.primary_key()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::Scope)
|
||||
.string_len(20)
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(SystemSettings::OrgId).string_len(100).null())
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::UserId)
|
||||
.string_len(256)
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::SettingKey)
|
||||
.string_len(255)
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::SettingCategory)
|
||||
.string_len(100)
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::Description)
|
||||
.custom(Alias::new(&text_type))
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::CreatedAt)
|
||||
.big_integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::UpdatedAt)
|
||||
.big_integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::CreatedBy)
|
||||
.string_len(256)
|
||||
.null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(SystemSettings::UpdatedBy)
|
||||
.string_len(256)
|
||||
.null(),
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
// Use JSON type for PostgreSQL, TEXT for others
|
||||
match backend {
|
||||
DbBackend::Postgres => {
|
||||
table.col(
|
||||
ColumnDef::new(SystemSettings::SettingValue)
|
||||
.json_binary()
|
||||
.not_null(),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
table.col(
|
||||
ColumnDef::new(SystemSettings::SettingValue)
|
||||
.custom(Alias::new(&text_type))
|
||||
.not_null(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
fn create_scope_index() -> IndexCreateStatement {
|
||||
Index::create()
|
||||
.name("idx_system_settings_scope")
|
||||
.table(SystemSettings::Table)
|
||||
.col(SystemSettings::Scope)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn create_org_index() -> IndexCreateStatement {
|
||||
Index::create()
|
||||
.name("idx_system_settings_org")
|
||||
.table(SystemSettings::Table)
|
||||
.col(SystemSettings::OrgId)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn create_user_index() -> IndexCreateStatement {
|
||||
Index::create()
|
||||
.name("idx_system_settings_user")
|
||||
.table(SystemSettings::Table)
|
||||
.col(SystemSettings::OrgId)
|
||||
.col(SystemSettings::UserId)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn create_key_index() -> IndexCreateStatement {
|
||||
Index::create()
|
||||
.name("idx_system_settings_key")
|
||||
.table(SystemSettings::Table)
|
||||
.col(SystemSettings::SettingKey)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn create_category_index() -> IndexCreateStatement {
|
||||
Index::create()
|
||||
.name("idx_system_settings_category")
|
||||
.table(SystemSettings::Table)
|
||||
.col(SystemSettings::SettingCategory)
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
fn create_unique_setting_index() -> IndexCreateStatement {
|
||||
Index::create()
|
||||
.name("idx_system_settings_unique")
|
||||
.table(SystemSettings::Table)
|
||||
.col(SystemSettings::Scope)
|
||||
.col(SystemSettings::OrgId)
|
||||
.col(SystemSettings::UserId)
|
||||
.col(SystemSettings::SettingKey)
|
||||
.unique()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum SystemSettings {
|
||||
Table,
|
||||
Id,
|
||||
Scope,
|
||||
OrgId,
|
||||
UserId,
|
||||
SettingKey,
|
||||
SettingCategory,
|
||||
SettingValue,
|
||||
Description,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
CreatedBy,
|
||||
UpdatedBy,
|
||||
}
|
||||
@@ -76,6 +76,7 @@ mod m20251118_000002_create_sessions_table;
|
||||
mod m20251118_000003_delete_meta_sessions;
|
||||
mod m20251126_100001_create_service_streams_table;
|
||||
mod m20251126_100002_create_service_streams_dimensions_table;
|
||||
mod m20251207_000001_create_system_settings_table;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
@@ -141,6 +142,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20251118_000003_delete_meta_sessions::Migration),
|
||||
Box::new(m20251126_100001_create_service_streams_table::Migration),
|
||||
Box::new(m20251126_100002_create_service_streams_dimensions_table::Migration),
|
||||
Box::new(m20251207_000001_create_system_settings_table::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ pub mod service_streams_dimensions;
|
||||
pub mod sessions;
|
||||
pub mod short_urls;
|
||||
pub mod system_prompts;
|
||||
pub mod system_settings;
|
||||
pub mod templates;
|
||||
pub mod timed_annotation_panels;
|
||||
pub mod timed_annotations;
|
||||
|
||||
@@ -48,7 +48,7 @@ pub struct Model {
|
||||
#[sea_orm(column_type = "String(StringLen::N(128))")]
|
||||
pub org_id: String,
|
||||
|
||||
#[sea_orm(column_type = "String(StringLen::N(512))")]
|
||||
#[sea_orm(column_type = "String(StringLen::N(2048))")]
|
||||
pub service_key: String,
|
||||
|
||||
/// Correlation key (hash of stable dimensions only)
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! System Settings Table Operations
|
||||
//!
|
||||
//! Provides CRUD operations for the multi-level settings system.
|
||||
//! Settings resolution order (most specific wins):
|
||||
//! 1. User level (if user_id provided)
|
||||
//! 2. Org level (if org_id provided)
|
||||
//! 3. System level (global defaults)
|
||||
|
||||
use anyhow::anyhow;
|
||||
use config::meta::system_settings::{SettingScope, SystemSetting};
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, QueryOrder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::{ORM_CLIENT, connect_to_orm},
|
||||
table::entity::system_settings::{ActiveModel, Column, Entity, Model},
|
||||
};
|
||||
|
||||
/// Get a single setting by scope, org_id, user_id, and key
|
||||
pub async fn get(
|
||||
scope: &SettingScope,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> Result<Option<SystemSetting>, anyhow::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
|
||||
let mut query = Entity::find()
|
||||
.filter(Column::Scope.eq(scope.as_str()))
|
||||
.filter(Column::SettingKey.eq(key));
|
||||
|
||||
match scope {
|
||||
SettingScope::System => {
|
||||
query = query
|
||||
.filter(Column::OrgId.is_null())
|
||||
.filter(Column::UserId.is_null());
|
||||
}
|
||||
SettingScope::Org => {
|
||||
if let Some(org) = org_id {
|
||||
query = query.filter(Column::OrgId.eq(org));
|
||||
} else {
|
||||
return Err(anyhow!("org_id required for org scope"));
|
||||
}
|
||||
query = query.filter(Column::UserId.is_null());
|
||||
}
|
||||
SettingScope::User => {
|
||||
if let Some(org) = org_id {
|
||||
query = query.filter(Column::OrgId.eq(org));
|
||||
} else {
|
||||
return Err(anyhow!("org_id required for user scope"));
|
||||
}
|
||||
if let Some(user) = user_id {
|
||||
query = query.filter(Column::UserId.eq(user));
|
||||
} else {
|
||||
return Err(anyhow!("user_id required for user scope"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = query
|
||||
.one(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(result.map(model_to_setting))
|
||||
}
|
||||
|
||||
/// Get a resolved setting value by checking all levels (user -> org -> system)
|
||||
/// Returns the most specific setting found
|
||||
pub async fn get_resolved(
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> Result<Option<SystemSetting>, anyhow::Error> {
|
||||
// Check user level first if user_id provided
|
||||
if let (Some(org), Some(user)) = (org_id, user_id)
|
||||
&& let Some(setting) = get(&SettingScope::User, Some(org), Some(user), key).await?
|
||||
{
|
||||
return Ok(Some(setting));
|
||||
}
|
||||
|
||||
// Check org level if org_id provided
|
||||
if let Some(org) = org_id
|
||||
&& let Some(setting) = get(&SettingScope::Org, Some(org), None, key).await?
|
||||
{
|
||||
return Ok(Some(setting));
|
||||
}
|
||||
|
||||
// Check system level
|
||||
get(&SettingScope::System, None, None, key).await
|
||||
}
|
||||
|
||||
/// List all settings for a given scope
|
||||
pub async fn list(
|
||||
scope: Option<&SettingScope>,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
category: Option<&str>,
|
||||
) -> Result<Vec<SystemSetting>, anyhow::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
|
||||
let mut query = Entity::find().order_by_asc(Column::SettingKey);
|
||||
|
||||
if let Some(scope) = scope {
|
||||
query = query.filter(Column::Scope.eq(scope.as_str()));
|
||||
}
|
||||
|
||||
if let Some(org) = org_id {
|
||||
query = query.filter(Column::OrgId.eq(org));
|
||||
}
|
||||
|
||||
if let Some(user) = user_id {
|
||||
query = query.filter(Column::UserId.eq(user));
|
||||
}
|
||||
|
||||
if let Some(cat) = category {
|
||||
query = query.filter(Column::SettingCategory.eq(cat));
|
||||
}
|
||||
|
||||
let results = query
|
||||
.all(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(results.into_iter().map(model_to_setting).collect())
|
||||
}
|
||||
|
||||
/// List all resolved settings for org/user (merging all levels)
|
||||
pub async fn list_resolved(
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
category: Option<&str>,
|
||||
) -> Result<std::collections::HashMap<String, SystemSetting>, anyhow::Error> {
|
||||
let mut resolved = std::collections::HashMap::new();
|
||||
|
||||
// Get system-level settings first (lowest priority)
|
||||
let system_settings = list(Some(&SettingScope::System), None, None, category).await?;
|
||||
for setting in system_settings {
|
||||
resolved.insert(setting.setting_key.clone(), setting);
|
||||
}
|
||||
|
||||
// Override with org-level settings if org_id provided
|
||||
if let Some(org) = org_id {
|
||||
let org_settings = list(Some(&SettingScope::Org), Some(org), None, category).await?;
|
||||
for setting in org_settings {
|
||||
resolved.insert(setting.setting_key.clone(), setting);
|
||||
}
|
||||
}
|
||||
|
||||
// Override with user-level settings if both org_id and user_id provided
|
||||
if let (Some(org), Some(user)) = (org_id, user_id) {
|
||||
let user_settings =
|
||||
list(Some(&SettingScope::User), Some(org), Some(user), category).await?;
|
||||
for setting in user_settings {
|
||||
resolved.insert(setting.setting_key.clone(), setting);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
/// Create or update a setting (upsert)
|
||||
pub async fn set(setting: &SystemSetting) -> Result<SystemSetting, anyhow::Error> {
|
||||
setting.validate().map_err(|e| anyhow!(e))?;
|
||||
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
let now = chrono::Utc::now().timestamp_micros();
|
||||
|
||||
// Check if setting already exists
|
||||
let existing = get(
|
||||
&setting.scope,
|
||||
setting.org_id.as_deref(),
|
||||
setting.user_id.as_deref(),
|
||||
&setting.setting_key,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(existing) = existing {
|
||||
// Update existing
|
||||
let active_model = ActiveModel {
|
||||
id: Set(existing.id.unwrap()),
|
||||
scope: Set(setting.scope.as_str().to_string()),
|
||||
org_id: Set(setting.org_id.clone()),
|
||||
user_id: Set(setting.user_id.clone()),
|
||||
setting_key: Set(setting.setting_key.clone()),
|
||||
setting_category: Set(setting.setting_category.clone()),
|
||||
setting_value: Set(setting.setting_value.to_string()),
|
||||
description: Set(setting.description.clone()),
|
||||
created_at: Set(existing.created_at),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(existing.created_by.clone()),
|
||||
updated_by: Set(setting.updated_by.clone()),
|
||||
};
|
||||
|
||||
let result = active_model
|
||||
.update(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(model_to_setting(result))
|
||||
} else {
|
||||
// Insert new
|
||||
let active_model = ActiveModel {
|
||||
id: Set(0), // Auto-increment
|
||||
scope: Set(setting.scope.as_str().to_string()),
|
||||
org_id: Set(setting.org_id.clone()),
|
||||
user_id: Set(setting.user_id.clone()),
|
||||
setting_key: Set(setting.setting_key.clone()),
|
||||
setting_category: Set(setting.setting_category.clone()),
|
||||
setting_value: Set(setting.setting_value.to_string()),
|
||||
description: Set(setting.description.clone()),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
created_by: Set(setting.created_by.clone()),
|
||||
updated_by: Set(setting.updated_by.clone()),
|
||||
};
|
||||
|
||||
let result = active_model
|
||||
.insert(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(model_to_setting(result))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a setting
|
||||
pub async fn delete(
|
||||
scope: &SettingScope,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
|
||||
let mut query = Entity::delete_many()
|
||||
.filter(Column::Scope.eq(scope.as_str()))
|
||||
.filter(Column::SettingKey.eq(key));
|
||||
|
||||
match scope {
|
||||
SettingScope::System => {
|
||||
query = query
|
||||
.filter(Column::OrgId.is_null())
|
||||
.filter(Column::UserId.is_null());
|
||||
}
|
||||
SettingScope::Org => {
|
||||
if let Some(org) = org_id {
|
||||
query = query.filter(Column::OrgId.eq(org));
|
||||
} else {
|
||||
return Err(anyhow!("org_id required for org scope"));
|
||||
}
|
||||
query = query.filter(Column::UserId.is_null());
|
||||
}
|
||||
SettingScope::User => {
|
||||
if let Some(org) = org_id {
|
||||
query = query.filter(Column::OrgId.eq(org));
|
||||
} else {
|
||||
return Err(anyhow!("org_id required for user scope"));
|
||||
}
|
||||
if let Some(user) = user_id {
|
||||
query = query.filter(Column::UserId.eq(user));
|
||||
} else {
|
||||
return Err(anyhow!("user_id required for user scope"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = query
|
||||
.exec(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(result.rows_affected > 0)
|
||||
}
|
||||
|
||||
/// Delete all settings for an organization
|
||||
pub async fn delete_org_settings(org_id: &str) -> Result<u64, anyhow::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
|
||||
let result = Entity::delete_many()
|
||||
.filter(Column::OrgId.eq(org_id))
|
||||
.exec(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
|
||||
/// Delete all settings for a user in an organization
|
||||
pub async fn delete_user_settings(org_id: &str, user_id: &str) -> Result<u64, anyhow::Error> {
|
||||
let client = ORM_CLIENT.get_or_init(connect_to_orm).await;
|
||||
|
||||
let result = Entity::delete_many()
|
||||
.filter(Column::OrgId.eq(org_id))
|
||||
.filter(Column::UserId.eq(user_id))
|
||||
.exec(client)
|
||||
.await
|
||||
.map_err(|e| anyhow!("DbError# {e}"))?;
|
||||
|
||||
Ok(result.rows_affected)
|
||||
}
|
||||
|
||||
/// Convert DB model to domain type
|
||||
fn model_to_setting(model: Model) -> SystemSetting {
|
||||
let setting_value = serde_json::from_str(&model.setting_value)
|
||||
.unwrap_or(serde_json::Value::String(model.setting_value.clone()));
|
||||
|
||||
SystemSetting {
|
||||
id: Some(model.id),
|
||||
scope: model.scope.parse().unwrap_or(SettingScope::System),
|
||||
org_id: model.org_id,
|
||||
user_id: model.user_id,
|
||||
setting_key: model.setting_key,
|
||||
setting_category: model.setting_category,
|
||||
setting_value,
|
||||
description: model.description,
|
||||
created_at: model.created_at,
|
||||
updated_at: model.updated_at,
|
||||
created_by: model.created_by,
|
||||
updated_by: model.updated_by,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_setting_scope_conversion() {
|
||||
assert_eq!(SettingScope::System.as_str(), "system");
|
||||
assert_eq!(SettingScope::Org.as_str(), "org");
|
||||
assert_eq!(SettingScope::User.as_str(), "user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_to_setting() {
|
||||
let model = Model {
|
||||
id: 1,
|
||||
scope: "org".to_string(),
|
||||
org_id: Some("default".to_string()),
|
||||
user_id: None,
|
||||
setting_key: "test_key".to_string(),
|
||||
setting_category: Some("correlation".to_string()),
|
||||
setting_value: r#"["a","b","c"]"#.to_string(),
|
||||
description: Some("Test setting".to_string()),
|
||||
created_at: 1000000,
|
||||
updated_at: 2000000,
|
||||
created_by: Some("admin".to_string()),
|
||||
updated_by: Some("admin".to_string()),
|
||||
};
|
||||
|
||||
let setting = model_to_setting(model);
|
||||
|
||||
assert_eq!(setting.id, Some(1));
|
||||
assert_eq!(setting.scope, SettingScope::Org);
|
||||
assert_eq!(setting.org_id, Some("default".to_string()));
|
||||
assert_eq!(setting.user_id, None);
|
||||
assert_eq!(setting.setting_key, "test_key");
|
||||
assert_eq!(setting.setting_value, serde_json::json!(["a", "b", "c"]));
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,7 @@ pub async fn get_semantic_groups(org_id: &str) -> Result<Vec<SemanticFieldGroup>
|
||||
alert_dedup_enabled: false,
|
||||
alert_fingerprint_groups: vec![],
|
||||
time_window_minutes: None,
|
||||
fqn_priority_dimensions: GlobalDeduplicationConfig::default_fqn_priority(),
|
||||
};
|
||||
|
||||
// Try to save, but don't fail if it doesn't work
|
||||
@@ -137,6 +138,7 @@ pub async fn initialize_semantic_groups(org_id: &str) -> Result<(), anyhow::Erro
|
||||
alert_dedup_enabled: false,
|
||||
alert_fingerprint_groups: vec![],
|
||||
time_window_minutes: None,
|
||||
fqn_priority_dimensions: GlobalDeduplicationConfig::default_fqn_priority(),
|
||||
};
|
||||
|
||||
set_deduplication_config(org_id, &config).await?;
|
||||
|
||||
@@ -50,6 +50,7 @@ pub mod schema;
|
||||
pub mod search_job;
|
||||
pub mod session;
|
||||
pub mod short_url;
|
||||
pub mod system_settings;
|
||||
pub mod user;
|
||||
|
||||
pub(crate) use infra_db::{Event, NEED_WATCH, NO_NEED_WATCH, get_coordinator};
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright 2025 OpenObserve Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! System Settings Service Layer
|
||||
//!
|
||||
//! Provides cached access to system settings with multi-level resolution.
|
||||
//! Cache is populated on startup and updated on write operations.
|
||||
|
||||
use config::meta::system_settings::{SettingScope, SystemSetting};
|
||||
use infra::{errors::Result, table::system_settings as db};
|
||||
|
||||
use crate::common::infra::config::SYSTEM_SETTINGS;
|
||||
|
||||
/// Generate a cache key for a system setting
|
||||
fn cache_key(
|
||||
scope: &SettingScope,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> String {
|
||||
let org = org_id.unwrap_or("_");
|
||||
let user = user_id.unwrap_or("_");
|
||||
format!("{}:{}:{}:{}", scope.as_str(), org, user, key)
|
||||
}
|
||||
|
||||
/// Get a single setting from cache or database
|
||||
pub async fn get(
|
||||
scope: &SettingScope,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> Result<Option<SystemSetting>> {
|
||||
let cache_k = cache_key(scope, org_id, user_id, key);
|
||||
|
||||
// Check cache first
|
||||
if let Some(setting) = SYSTEM_SETTINGS.read().await.get(&cache_k) {
|
||||
return Ok(Some(setting.clone()));
|
||||
}
|
||||
|
||||
// Get from database
|
||||
let setting = db::get(scope, org_id, user_id, key)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))?;
|
||||
|
||||
// Cache the result if found
|
||||
if let Some(ref s) = setting {
|
||||
SYSTEM_SETTINGS.write().await.insert(cache_k, s.clone());
|
||||
}
|
||||
|
||||
Ok(setting)
|
||||
}
|
||||
|
||||
/// Get a resolved setting by checking all levels (user -> org -> system)
|
||||
/// Returns the most specific setting found
|
||||
pub async fn get_resolved(
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> Result<Option<SystemSetting>> {
|
||||
// Check user level first if user_id provided
|
||||
if let (Some(org), Some(user)) = (org_id, user_id)
|
||||
&& let Some(setting) = get(&SettingScope::User, Some(org), Some(user), key).await?
|
||||
{
|
||||
return Ok(Some(setting));
|
||||
}
|
||||
|
||||
// Check org level if org_id provided
|
||||
if let Some(org) = org_id
|
||||
&& let Some(setting) = get(&SettingScope::Org, Some(org), None, key).await?
|
||||
{
|
||||
return Ok(Some(setting));
|
||||
}
|
||||
|
||||
// Check system level
|
||||
get(&SettingScope::System, None, None, key).await
|
||||
}
|
||||
|
||||
/// List all settings for a given scope
|
||||
pub async fn list(
|
||||
scope: Option<&SettingScope>,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
category: Option<&str>,
|
||||
) -> Result<Vec<SystemSetting>> {
|
||||
db::list(scope, org_id, user_id, category)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))
|
||||
}
|
||||
|
||||
/// List all resolved settings for org/user (merging all levels)
|
||||
pub async fn list_resolved(
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
category: Option<&str>,
|
||||
) -> Result<std::collections::HashMap<String, SystemSetting>> {
|
||||
db::list_resolved(org_id, user_id, category)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))
|
||||
}
|
||||
|
||||
/// Create or update a setting (upsert)
|
||||
pub async fn set(setting: &SystemSetting) -> Result<SystemSetting> {
|
||||
let result = db::set(setting)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))?;
|
||||
|
||||
// Update cache
|
||||
let cache_k = cache_key(
|
||||
&result.scope,
|
||||
result.org_id.as_deref(),
|
||||
result.user_id.as_deref(),
|
||||
&result.setting_key,
|
||||
);
|
||||
SYSTEM_SETTINGS
|
||||
.write()
|
||||
.await
|
||||
.insert(cache_k, result.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Delete a setting
|
||||
pub async fn delete(
|
||||
scope: &SettingScope,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) -> Result<bool> {
|
||||
let result = db::delete(scope, org_id, user_id, key)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))?;
|
||||
|
||||
// Remove from cache
|
||||
let cache_k = cache_key(scope, org_id, user_id, key);
|
||||
SYSTEM_SETTINGS.write().await.remove(&cache_k);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Delete all settings for an organization
|
||||
pub async fn delete_org_settings(org_id: &str) -> Result<u64> {
|
||||
let result = db::delete_org_settings(org_id)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))?;
|
||||
|
||||
// Clear org settings from cache
|
||||
let mut cache = SYSTEM_SETTINGS.write().await;
|
||||
let keys_to_remove: Vec<String> = cache
|
||||
.iter()
|
||||
.filter(|(k, _)| k.split(':').nth(1) == Some(org_id))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
for key in keys_to_remove {
|
||||
cache.remove(&key);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Delete all settings for a user in an organization
|
||||
pub async fn delete_user_settings(org_id: &str, user_id: &str) -> Result<u64> {
|
||||
let result = db::delete_user_settings(org_id, user_id)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))?;
|
||||
|
||||
// Clear user settings from cache
|
||||
let mut cache = SYSTEM_SETTINGS.write().await;
|
||||
let keys_to_remove: Vec<String> = cache
|
||||
.iter()
|
||||
.filter(|(k, _)| {
|
||||
let parts: Vec<&str> = k.split(':').collect();
|
||||
parts.len() >= 3 && parts[1] == org_id && parts[2] == user_id
|
||||
})
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
for key in keys_to_remove {
|
||||
cache.remove(&key);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Initialize cache with all system settings from database
|
||||
pub async fn cache() -> Result<()> {
|
||||
let settings = db::list(None, None, None, None)
|
||||
.await
|
||||
.map_err(|e| infra::errors::Error::Message(e.to_string()))?;
|
||||
|
||||
let mut cache = SYSTEM_SETTINGS.write().await;
|
||||
for setting in settings {
|
||||
let cache_k = cache_key(
|
||||
&setting.scope,
|
||||
setting.org_id.as_deref(),
|
||||
setting.user_id.as_deref(),
|
||||
&setting.setting_key,
|
||||
);
|
||||
cache.insert(cache_k, setting);
|
||||
}
|
||||
log::info!("System settings cached");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Invalidate cache for a specific setting
|
||||
pub async fn invalidate_cache(
|
||||
scope: &SettingScope,
|
||||
org_id: Option<&str>,
|
||||
user_id: Option<&str>,
|
||||
key: &str,
|
||||
) {
|
||||
let cache_k = cache_key(scope, org_id, user_id, key);
|
||||
SYSTEM_SETTINGS.write().await.remove(&cache_k);
|
||||
}
|
||||
|
||||
/// Invalidate entire cache
|
||||
pub async fn invalidate_all_cache() {
|
||||
SYSTEM_SETTINGS.write().await.clear();
|
||||
}
|
||||
|
||||
/// Get FQN priority dimensions for an organization
|
||||
///
|
||||
/// Resolution order:
|
||||
/// 1. Check settings v2 for org-level setting
|
||||
/// 2. Fall back to system defaults from O2_FQN_PRIORITY_DIMENSIONS env var
|
||||
///
|
||||
/// Returns empty vec for OSS builds if env var is not set.
|
||||
pub async fn get_fqn_priority_dimensions(org_id: &str) -> Vec<String> {
|
||||
use config::meta::system_settings::keys::FQN_PRIORITY_DIMENSIONS;
|
||||
|
||||
// Try to get from settings v2 (org level)
|
||||
if let Ok(Some(setting)) = get_resolved(Some(org_id), None, FQN_PRIORITY_DIMENSIONS).await
|
||||
&& let Ok(dims) = serde_json::from_value::<Vec<String>>(setting.setting_value)
|
||||
&& !dims.is_empty()
|
||||
{
|
||||
return dims;
|
||||
}
|
||||
|
||||
// Fall back to system defaults
|
||||
get_default_fqn_priority_dimensions()
|
||||
}
|
||||
|
||||
/// Get the default FQN priority dimensions from config
|
||||
///
|
||||
/// For enterprise builds, uses O2_FQN_PRIORITY_DIMENSIONS env var.
|
||||
/// For OSS builds, returns empty vec.
|
||||
pub fn get_default_fqn_priority_dimensions() -> Vec<String> {
|
||||
#[cfg(feature = "enterprise")]
|
||||
{
|
||||
use o2_enterprise::enterprise::common::config::get_config as get_o2_config;
|
||||
get_o2_config()
|
||||
.service_streams
|
||||
.get_fqn_priority_dimensions()
|
||||
}
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
{
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cache_key() {
|
||||
// System scope
|
||||
assert_eq!(
|
||||
cache_key(&SettingScope::System, None, None, "test_key"),
|
||||
"system:_:_:test_key"
|
||||
);
|
||||
|
||||
// Org scope
|
||||
assert_eq!(
|
||||
cache_key(&SettingScope::Org, Some("default"), None, "test_key"),
|
||||
"org:default:_:test_key"
|
||||
);
|
||||
|
||||
// User scope
|
||||
assert_eq!(
|
||||
cache_key(
|
||||
&SettingScope::User,
|
||||
Some("default"),
|
||||
Some("user@example.com"),
|
||||
"test_key"
|
||||
),
|
||||
"user:default:user@example.com:test_key"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -84,15 +84,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
data-test="alert-insights-btn"
|
||||
icon="insights"
|
||||
/>
|
||||
<q-btn
|
||||
class="q-ml-sm o2-secondary-button tw-h-[36px]"
|
||||
no-caps
|
||||
flat
|
||||
:label="t('settings.header')"
|
||||
@click="showCorrelationDrawer = true"
|
||||
data-test="correlation-settings-btn"
|
||||
icon="settings"
|
||||
/>
|
||||
<q-btn
|
||||
class="q-ml-sm o2-secondary-button tw-h-[36px]"
|
||||
no-caps
|
||||
@@ -570,47 +561,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Correlation Settings Drawer -->
|
||||
<q-drawer
|
||||
v-model="showCorrelationDrawer"
|
||||
side="right"
|
||||
:width="800"
|
||||
bordered
|
||||
overlay
|
||||
behavior="mobile"
|
||||
data-test="correlation-settings-drawer"
|
||||
>
|
||||
<div class="tw-h-full tw-flex tw-flex-col">
|
||||
<!-- Drawer Header -->
|
||||
<div class="tw-px-6 tw-py-4 tw-border-b tw-flex tw-items-center tw-justify-between">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<q-icon name="group_work" size="24px" class="tw-mr-2" />
|
||||
<h6 class="tw-text-lg tw-font-semibold tw-m-0">
|
||||
{{ t('settings.alertCorrelation') }}
|
||||
</h6>
|
||||
</div>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
@click="showCorrelationDrawer = false"
|
||||
data-test="close-correlation-drawer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Drawer Content -->
|
||||
<div class="tw-flex-1 tw-overflow-y-auto">
|
||||
<OrganizationDeduplicationSettings
|
||||
:org-id="store.state.selectedOrganization.identifier"
|
||||
:config="store.state.organizationSettings?.deduplication_config"
|
||||
@saved="onCorrelationSettingsSaved"
|
||||
@cancel="showCorrelationDrawer = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-drawer>
|
||||
|
||||
<ConfirmDialog
|
||||
title="Delete Alert"
|
||||
message="Are you sure you want to delete this alert?"
|
||||
@@ -870,7 +820,6 @@ import ConfirmDialog from "@/components/ConfirmDialog.vue";
|
||||
import segment from "@/services/segment_analytics";
|
||||
import config from "@/aws-exports";
|
||||
import ImportAlert from "@/components/alerts/ImportAlert.vue";
|
||||
import OrganizationDeduplicationSettings from "@/components/alerts/OrganizationDeduplicationSettings.vue";
|
||||
import DedupSummaryCards from "@/components/alerts/DedupSummaryCards.vue";
|
||||
import {
|
||||
getImageURL,
|
||||
@@ -908,7 +857,6 @@ export default defineComponent({
|
||||
NoData,
|
||||
ConfirmDialog,
|
||||
ImportAlert,
|
||||
OrganizationDeduplicationSettings,
|
||||
DedupSummaryCards,
|
||||
FolderList,
|
||||
MoveAcrossFolders,
|
||||
@@ -946,7 +894,6 @@ export default defineComponent({
|
||||
const showHistoryDrawer = ref(false);
|
||||
const selectedHistoryAlertId = ref("");
|
||||
const selectedHistoryAlertName = ref("");
|
||||
const showCorrelationDrawer = ref(false);
|
||||
|
||||
const { getStreams } = useStreams();
|
||||
|
||||
@@ -2039,12 +1986,6 @@ export default defineComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const onCorrelationSettingsSaved = async () => {
|
||||
// Reload organization settings to get updated dedup config
|
||||
await store.dispatch("getDefaultOrganizationSettings");
|
||||
showCorrelationDrawer.value = false;
|
||||
};
|
||||
|
||||
const exportAlert = async (row: any) => {
|
||||
// Find the alert based on uuid
|
||||
const alertToBeExported = await getAlertById(row.alert_id);
|
||||
@@ -2601,8 +2542,6 @@ export default defineComponent({
|
||||
showHistoryDrawer,
|
||||
selectedHistoryAlertId,
|
||||
selectedHistoryAlertName,
|
||||
showCorrelationDrawer,
|
||||
onCorrelationSettingsSaved,
|
||||
refreshImportedAlerts,
|
||||
folderIdToBeCloned,
|
||||
updateFolderIdToBeCloned,
|
||||
|
||||
@@ -17,12 +17,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
<template>
|
||||
<q-card flat class="tw-p-6">
|
||||
<div class="tw-mb-6">
|
||||
<div class="text-h6 tw-mb-2">Alert Deduplication</div>
|
||||
<div class="text-h6 tw-mb-2">Alert Correlation</div>
|
||||
<div class="text-body2 text-grey-7">
|
||||
Configure organization-wide semantic field groups and default time windows.
|
||||
Semantic groups define which field name variations represent the same dimension
|
||||
(e.g., "host", "hostname", "node" all map to "host"). Each alert specifies which
|
||||
fields to use for fingerprinting in its own configuration.
|
||||
Configure how alerts are correlated and deduplicated across the organization.
|
||||
Alerts with matching fingerprint values will be grouped together.
|
||||
</div>
|
||||
<div class="text-body2 text-grey-6 tw-mt-2 tw-italic">
|
||||
Note: Semantic field mappings are configured in the Service Identity tab.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,14 +57,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Semantic Field Groups Configuration -->
|
||||
<div class="tw-mb-6" v-if="localConfig.enabled">
|
||||
<SemanticFieldGroupsConfig
|
||||
v-model:semantic-field-groups="localSemanticGroups"
|
||||
@update:semantic-field-groups="handleSemanticGroupsUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Cross-Alert Fingerprint Groups -->
|
||||
<div class="tw-mb-6" v-if="localConfig.alert_dedup_enabled">
|
||||
<div class="tw-font-semibold tw-pb-2 tw-flex tw-items-center">
|
||||
@@ -165,7 +158,6 @@ import { ref, watch } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { outlinedInfo } from "@quasar/extras/material-icons-outlined";
|
||||
import SemanticFieldGroupsConfig from "./SemanticFieldGroupsConfig.vue";
|
||||
import alertsService from "@/services/alerts";
|
||||
|
||||
const store = useStore();
|
||||
@@ -185,6 +177,7 @@ interface OrganizationDeduplicationConfig {
|
||||
alert_dedup_enabled?: boolean;
|
||||
alert_fingerprint_groups?: string[];
|
||||
time_window_minutes?: number;
|
||||
fqn_priority_dimensions?: string[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -209,17 +202,13 @@ const localConfig = ref<OrganizationDeduplicationConfig>({
|
||||
alert_fingerprint_groups: props.config?.alert_fingerprint_groups ?? [],
|
||||
time_window_minutes: props.config?.time_window_minutes ?? undefined,
|
||||
semantic_field_groups: props.config?.semantic_field_groups ?? [],
|
||||
fqn_priority_dimensions: props.config?.fqn_priority_dimensions,
|
||||
});
|
||||
|
||||
const localSemanticGroups = ref<SemanticFieldGroup[]>(
|
||||
props.config?.semantic_field_groups ?? [],
|
||||
);
|
||||
|
||||
const handleSemanticGroupsUpdate = (groups: SemanticFieldGroup[]) => {
|
||||
localConfig.value.semantic_field_groups = groups;
|
||||
localSemanticGroups.value = groups;
|
||||
};
|
||||
|
||||
const toggleFingerprintGroup = (groupId: string, checked: boolean) => {
|
||||
if (!localConfig.value.alert_fingerprint_groups) {
|
||||
localConfig.value.alert_fingerprint_groups = [];
|
||||
@@ -294,6 +283,7 @@ const loadConfig = async () => {
|
||||
alert_fingerprint_groups: config.alert_fingerprint_groups ?? [],
|
||||
time_window_minutes: config.time_window_minutes ?? undefined,
|
||||
semantic_field_groups: config.semantic_field_groups ?? [],
|
||||
fqn_priority_dimensions: config.fqn_priority_dimensions,
|
||||
};
|
||||
localSemanticGroups.value = config.semantic_field_groups ?? [];
|
||||
} catch (error) {
|
||||
@@ -311,6 +301,7 @@ const loadConfig = async () => {
|
||||
alert_fingerprint_groups: [],
|
||||
time_window_minutes: undefined,
|
||||
semantic_field_groups: defaultGroups,
|
||||
fqn_priority_dimensions: undefined,
|
||||
};
|
||||
localSemanticGroups.value = defaultGroups;
|
||||
} catch (semanticError) {
|
||||
@@ -339,6 +330,7 @@ watch(
|
||||
alert_fingerprint_groups: newVal.alert_fingerprint_groups ?? [],
|
||||
time_window_minutes: newVal.time_window_minutes ?? undefined,
|
||||
semantic_field_groups: newVal.semantic_field_groups ?? [],
|
||||
fqn_priority_dimensions: newVal.fqn_priority_dimensions,
|
||||
};
|
||||
localSemanticGroups.value = newVal.semantic_field_groups ?? [];
|
||||
console.log("Updated localConfig:", localConfig.value);
|
||||
|
||||
@@ -144,6 +144,10 @@ interface SemanticGroup {
|
||||
normalize: boolean;
|
||||
}
|
||||
|
||||
// Reserved IDs that should not be used as semantic groups
|
||||
// service-fqn is the OUTPUT of correlation, not an input dimension
|
||||
const RESERVED_GROUP_IDS = ['service-fqn', 'servicefqn', 'fqn'];
|
||||
|
||||
interface Props {
|
||||
semanticFieldGroups?: SemanticGroup[];
|
||||
fingerprintFields?: string[];
|
||||
@@ -161,7 +165,10 @@ const emit = defineEmits<{
|
||||
(e: "update:fingerprintFields", fields: string[]): void;
|
||||
}>();
|
||||
|
||||
const localGroups = ref<SemanticGroup[]>([...props.semanticFieldGroups]);
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
const localGroups = ref<SemanticGroup[]>(
|
||||
props.semanticFieldGroups.filter(g => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()))
|
||||
);
|
||||
const localFingerprintFields = ref<string[]>([...props.fingerprintFields]);
|
||||
const selectedPreset = ref<string | null>(null);
|
||||
const availableSemanticGroups = ref<SemanticGroup[]>([]);
|
||||
@@ -171,7 +178,8 @@ const loadingPresets = ref(false);
|
||||
watch(
|
||||
() => props.semanticFieldGroups,
|
||||
(newGroups) => {
|
||||
localGroups.value = [...newGroups];
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
localGroups.value = newGroups.filter(g => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
@@ -223,7 +231,9 @@ const loadSemanticGroups = async () => {
|
||||
try {
|
||||
const orgId = store.state.selectedOrganization.identifier;
|
||||
const response = await alertsService.getSemanticGroups(orgId);
|
||||
availableSemanticGroups.value = response.data;
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
availableSemanticGroups.value = (response.data ?? [])
|
||||
.filter((g: SemanticGroup) => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
console.log(`Loaded ${availableSemanticGroups.value.length} semantic groups from backend`);
|
||||
} catch (error) {
|
||||
console.error("Failed to load semantic groups:", error);
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<!-- Copyright 2023 OpenObserve Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="tw-w-full tw-h-full">
|
||||
<div class="q-px-md q-py-md">
|
||||
<div class="general-page-title">
|
||||
{{ t("settings.correlation.title") }}
|
||||
</div>
|
||||
<div class="general-page-subtitle">
|
||||
{{ t("settings.correlation.subtitle") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-mx-4">
|
||||
<q-tabs
|
||||
v-model="activeTab"
|
||||
dense
|
||||
class="text-grey"
|
||||
active-color="primary"
|
||||
indicator-color="primary"
|
||||
align="left"
|
||||
narrow-indicator
|
||||
>
|
||||
<q-tab name="identity" :label="t('settings.correlation.serviceIdentityTab')" icon="fingerprint" />
|
||||
<q-tab name="services" :label="t('settings.correlation.discoveredServicesTab')" icon="hub" />
|
||||
<q-tab name="alert-correlation" :label="t('settings.correlation.alertCorrelationTab')" icon="link" />
|
||||
</q-tabs>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-tab-panels v-model="activeTab" animated class="tw-mt-4">
|
||||
<q-tab-panel name="identity" class="tw-p-0">
|
||||
<ServiceIdentityConfig
|
||||
:org-id="store.state.selectedOrganization.identifier"
|
||||
:config="store.state.organizationSettings?.deduplication_config"
|
||||
@saved="onCorrelationSettingsSaved"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="services" class="tw-p-0">
|
||||
<DiscoveredServices />
|
||||
</q-tab-panel>
|
||||
|
||||
<q-tab-panel name="alert-correlation" class="tw-p-0">
|
||||
<OrganizationDeduplicationSettings
|
||||
:org-id="store.state.selectedOrganization.identifier"
|
||||
:config="store.state.organizationSettings?.deduplication_config"
|
||||
@saved="onCorrelationSettingsSaved"
|
||||
/>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import OrganizationDeduplicationSettings from "@/components/alerts/OrganizationDeduplicationSettings.vue";
|
||||
import DiscoveredServices from "@/components/settings/DiscoveredServices.vue";
|
||||
import ServiceIdentityConfig from "@/components/settings/ServiceIdentityConfig.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CorrelationSettings",
|
||||
components: {
|
||||
OrganizationDeduplicationSettings,
|
||||
DiscoveredServices,
|
||||
ServiceIdentityConfig,
|
||||
},
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
const activeTab = ref("identity");
|
||||
|
||||
const onCorrelationSettingsSaved = async () => {
|
||||
// Reload organization settings to get updated dedup config
|
||||
await store.dispatch("getDefaultOrganizationSettings");
|
||||
q.notify({
|
||||
type: "positive",
|
||||
message: t("settings.correlation.correlationSettingsSaved"),
|
||||
timeout: 2000,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
store,
|
||||
activeTab,
|
||||
onCorrelationSettingsSaved,
|
||||
t,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.general-page-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
.general-page-subtitle {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,6 +100,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
Manage your organization's theme colors for both light and dark modes. These colors will be applied at the organization level.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span> </span>
|
||||
|
||||
<div class="flex justify-start">
|
||||
|
||||
@@ -0,0 +1,641 @@
|
||||
<!-- Copyright 2025 OpenObserve Inc.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<q-card flat class="tw-p-6">
|
||||
<div class="tw-mb-4">
|
||||
<div class="text-h6 tw-mb-2">{{ t("settings.correlation.serviceIdentityTitle") }}</div>
|
||||
<div class="text-body2 text-grey-7">
|
||||
{{ t("settings.correlation.serviceIdentityDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="tw-mb-4" />
|
||||
|
||||
<!-- How it works explanation -->
|
||||
<q-expansion-item
|
||||
dense
|
||||
dense-toggle
|
||||
icon="help_outline"
|
||||
:label="t('settings.correlation.howItWorksTitle')"
|
||||
class="tw-mb-4 tw-rounded-lg"
|
||||
:class="store.state.theme === 'dark' ? 'bg-grey-9' : 'bg-grey-2'"
|
||||
>
|
||||
<div class="tw-p-4 text-body2 tw-leading-relaxed">
|
||||
<div class="tw-mb-3">
|
||||
<span class="tw-font-semibold text-primary">Service FQN</span>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.howItWorksDescription") }}</span>
|
||||
</div>
|
||||
<div class="tw-mb-3">
|
||||
<span class="tw-font-semibold text-primary">{{ t("settings.correlation.priorityOrderLabel") }}</span>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.priorityOrderDescription") }}</span>
|
||||
</div>
|
||||
<div class="tw-mb-3 tw-p-3 tw-rounded" :class="store.state.theme === 'dark' ? 'bg-grey-10' : 'bg-white'">
|
||||
<span class="tw-font-semibold text-primary">{{ t("settings.correlation.exampleLabel") }}</span>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.exampleText1") }} </span>
|
||||
<q-chip dense size="sm" color="primary" text-color="white" class="tw-mx-1">k8s-deployment=my-app</q-chip>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.exampleText2") }} </span>
|
||||
<q-chip dense size="sm" color="grey-7" text-color="white" class="tw-mx-1">service=myapp</q-chip>
|
||||
<span class="text-grey-7">, {{ t("settings.correlation.exampleText3") }} </span>
|
||||
<q-chip dense size="sm" color="positive" text-color="white" class="tw-mx-1">my-app</q-chip>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.exampleText4") }} </span>
|
||||
<span class="tw-font-semibold">k8s-deployment</span>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.exampleText5") }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="tw-font-semibold text-primary">{{ t("settings.correlation.correlationDescription").split(' ')[0] }}:</span>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.correlationDescription") }} </span>
|
||||
<span class="tw-font-mono tw-font-semibold">{{ t("settings.correlation.correlationDescriptionField") }}</span>
|
||||
<span class="text-grey-7"> {{ t("settings.correlation.correlationDescriptionEnd") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-expansion-item>
|
||||
|
||||
<!-- FQN Priority Dimensions -->
|
||||
<div class="tw-mb-6">
|
||||
<div class="tw-font-semibold tw-pb-2 tw-flex tw-items-center tw-text-lg">
|
||||
{{ t("settings.correlation.fqnPriorityTitle") }}
|
||||
<q-icon
|
||||
:name="outlinedInfo"
|
||||
size="1.125rem"
|
||||
class="q-ml-xs cursor-pointer"
|
||||
:class="store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center right"
|
||||
self="center left"
|
||||
max-width="21.875rem"
|
||||
class="tooltip-text"
|
||||
>
|
||||
{{ t("settings.correlation.fqnPriorityTooltip") }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-mb-3">
|
||||
{{ t("settings.correlation.fqnPriorityDescription") }}
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-gap-2 tw-mb-3">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
size="sm"
|
||||
icon="restart_alt"
|
||||
:label="t('settings.correlation.resetToDefaults')"
|
||||
@click="resetFqnPriority"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-list bordered class="tw-rounded-lg">
|
||||
<q-item
|
||||
v-for="(dim, index) in localFqnPriority"
|
||||
:key="dim"
|
||||
dense
|
||||
class="tw-py-2"
|
||||
>
|
||||
<q-item-section avatar class="tw-min-w-0">
|
||||
<q-badge
|
||||
:color="index < 4 ? 'primary' : 'grey'"
|
||||
text-color="white"
|
||||
class="tw-w-6 tw-h-6 tw-flex tw-items-center tw-justify-center tw-rounded-full"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</q-badge>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="tw-font-medium">{{ getDimensionDisplay(dim) }}</q-item-label>
|
||||
<q-item-label caption class="tw-font-mono tw-text-xs">{{ dim }}</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<div class="tw-flex tw-gap-1">
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
size="sm"
|
||||
icon="arrow_upward"
|
||||
:disable="index === 0"
|
||||
@click="moveFqnDimensionUp(index)"
|
||||
>
|
||||
<q-tooltip>{{ t("settings.correlation.moveUp") }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
size="sm"
|
||||
icon="arrow_downward"
|
||||
:disable="index === localFqnPriority.length - 1"
|
||||
@click="moveFqnDimensionDown(index)"
|
||||
>
|
||||
<q-tooltip>{{ t("settings.correlation.moveDown") }}</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
round
|
||||
size="sm"
|
||||
icon="delete"
|
||||
color="negative"
|
||||
@click="removeFqnDimension(index)"
|
||||
>
|
||||
<q-tooltip>{{ t("settings.correlation.removeFromList") }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-if="localFqnPriority.length === 0" class="tw-py-4 tw-text-center tw-text-gray-500">
|
||||
{{ t("settings.correlation.noDimensionsConfigured") }}
|
||||
</q-item>
|
||||
</q-list>
|
||||
|
||||
<!-- Add new dimension from semantic groups - Two step: 1) Select group 2) Select field -->
|
||||
<div class="tw-mt-3">
|
||||
<div class="tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-mb-2">
|
||||
{{ t("settings.correlation.addDimensionHint") }}
|
||||
</div>
|
||||
<div class="tw-flex tw-gap-2 tw-items-end">
|
||||
<!-- Step 1: Select Semantic Group -->
|
||||
<q-select
|
||||
v-model="selectedSemanticGroup"
|
||||
:options="availableSemanticGroups"
|
||||
dense
|
||||
filled
|
||||
:label="t('settings.correlation.selectSemanticGroup')"
|
||||
class="tw-flex-1"
|
||||
emit-value
|
||||
map-options
|
||||
clearable
|
||||
:disable="availableSemanticGroups.length === 0"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
<q-item-label caption class="tw-text-xs">
|
||||
{{ scope.opt.fieldCount }} {{ t("settings.correlation.fieldsAvailable") }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ t("settings.correlation.noSemanticGroupsAvailable") }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<!-- Step 2: Select Field from Group -->
|
||||
<q-select
|
||||
v-model="newFqnDimension"
|
||||
:options="availableFieldsFromGroup"
|
||||
dense
|
||||
filled
|
||||
:label="t('settings.correlation.selectField')"
|
||||
class="tw-flex-1"
|
||||
emit-value
|
||||
map-options
|
||||
clearable
|
||||
:disable="!selectedSemanticGroup || availableFieldsFromGroup.length === 0"
|
||||
>
|
||||
<template v-slot:option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section>
|
||||
<q-item-label class="tw-font-mono">{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
<q-item-section class="text-grey">
|
||||
{{ selectedSemanticGroup ? t("settings.correlation.allFieldsAdded") : t("settings.correlation.selectGroupFirst") }}
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
icon="add"
|
||||
color="primary"
|
||||
:disable="!newFqnDimension"
|
||||
@click="addFqnDimension"
|
||||
>
|
||||
<q-tooltip>{{ t("settings.correlation.addDimensionLabel") }}</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="availableSemanticGroups.length === 0 && localSemanticGroups.length === 0" class="tw-mt-2 tw-text-sm tw-text-amber-600 dark:tw-text-amber-400">
|
||||
{{ t("settings.correlation.noSemanticGroupsConfigured") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-separator class="tw-my-4" />
|
||||
|
||||
<!-- Semantic Field Groups (for dimension extraction) -->
|
||||
<div class="tw-mb-6">
|
||||
<div class="tw-font-semibold tw-pb-2 tw-flex tw-items-center tw-text-lg">
|
||||
{{ t("settings.correlation.semanticFieldTitle") }}
|
||||
<q-icon
|
||||
:name="outlinedInfo"
|
||||
size="1.125rem"
|
||||
class="q-ml-xs cursor-pointer"
|
||||
:class="store.state.theme === 'dark' ? 'text-grey-5' : 'text-grey-7'"
|
||||
>
|
||||
<q-tooltip
|
||||
anchor="center right"
|
||||
self="center left"
|
||||
max-width="21.875rem"
|
||||
class="tooltip-text"
|
||||
>
|
||||
{{ t("settings.correlation.semanticFieldTooltip") }}
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
<div class="tw-text-sm tw-text-gray-600 dark:tw-text-gray-400 tw-mb-3">
|
||||
{{ t("settings.correlation.semanticFieldDescription") }}
|
||||
</div>
|
||||
|
||||
<SemanticFieldGroupsConfig
|
||||
v-model:semantic-field-groups="localSemanticGroups"
|
||||
@update:semantic-field-groups="handleSemanticGroupsUpdate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="tw-flex tw-justify-end tw-gap-3 tw-mt-6">
|
||||
<q-btn outline :label="t('common.cancel')" @click="$emit('cancel')" class="tw-px-4" />
|
||||
<q-btn
|
||||
:label="t('settings.correlation.saveConfiguration')"
|
||||
color="primary"
|
||||
@click="saveSettings"
|
||||
:loading="saving"
|
||||
class="tw-px-4"
|
||||
/>
|
||||
</div>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useQuasar } from "quasar";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { outlinedInfo } from "@quasar/extras/material-icons-outlined";
|
||||
import SemanticFieldGroupsConfig from "@/components/alerts/SemanticFieldGroupsConfig.vue";
|
||||
import alertsService from "@/services/alerts";
|
||||
import settingsService from "@/services/settings";
|
||||
|
||||
const store = useStore();
|
||||
const $q = useQuasar();
|
||||
const { t } = useI18n();
|
||||
|
||||
interface SemanticFieldGroup {
|
||||
id: string;
|
||||
display: string;
|
||||
group?: string;
|
||||
fields: string[];
|
||||
normalize: boolean;
|
||||
}
|
||||
|
||||
interface OrganizationDeduplicationConfig {
|
||||
enabled: boolean;
|
||||
semantic_field_groups?: SemanticFieldGroup[];
|
||||
alert_dedup_enabled?: boolean;
|
||||
alert_fingerprint_groups?: string[];
|
||||
time_window_minutes?: number;
|
||||
fqn_priority_dimensions?: string[];
|
||||
}
|
||||
|
||||
// Default FQN priority dimensions - empty by default, uses O2_FQN_PRIORITY_DIMENSIONS env var
|
||||
const DEFAULT_FQN_PRIORITY: string[] = [];
|
||||
|
||||
interface Props {
|
||||
orgId: string;
|
||||
config?: OrganizationDeduplicationConfig | null;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
config: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "saved"): void;
|
||||
(e: "cancel"): void;
|
||||
}>();
|
||||
|
||||
const saving = ref(false);
|
||||
const localFqnPriority = ref<string[]>([...DEFAULT_FQN_PRIORITY]);
|
||||
const localSemanticGroups = ref<SemanticFieldGroup[]>([]);
|
||||
const selectedSemanticGroup = ref<string | null>(null);
|
||||
const newFqnDimension = ref<string | null>(null);
|
||||
|
||||
// Full config for saving
|
||||
const localConfig = ref<OrganizationDeduplicationConfig>({
|
||||
enabled: true,
|
||||
alert_dedup_enabled: props.config?.alert_dedup_enabled ?? true,
|
||||
alert_fingerprint_groups: props.config?.alert_fingerprint_groups ?? [],
|
||||
time_window_minutes: props.config?.time_window_minutes ?? undefined,
|
||||
semantic_field_groups: props.config?.semantic_field_groups ?? [],
|
||||
fqn_priority_dimensions: props.config?.fqn_priority_dimensions ?? [...DEFAULT_FQN_PRIORITY],
|
||||
});
|
||||
|
||||
// Reserved IDs that should not be used as semantic groups
|
||||
// service-fqn is the OUTPUT of correlation, not an input dimension
|
||||
const RESERVED_GROUP_IDS = ['service-fqn', 'servicefqn', 'fqn'];
|
||||
|
||||
// Computed: Get all semantic groups for the first dropdown
|
||||
// Shows groups that have at least one field not yet in the priority list
|
||||
const availableSemanticGroups = computed(() => {
|
||||
const semanticGroups = localSemanticGroups.value.length > 0
|
||||
? localSemanticGroups.value
|
||||
: localConfig.value.semantic_field_groups || [];
|
||||
|
||||
return semanticGroups
|
||||
.filter(group => {
|
||||
// Exclude reserved/computed fields like service-fqn (it's the output, not an input)
|
||||
if (RESERVED_GROUP_IDS.includes(group.id?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
// Check if this group has any fields not already in the priority list
|
||||
const availableFields = group.fields?.filter(field => !localFqnPriority.value.includes(field)) || [];
|
||||
return availableFields.length > 0;
|
||||
})
|
||||
.map(group => ({
|
||||
label: group.display || group.id,
|
||||
value: group.id,
|
||||
fieldCount: (group.fields?.filter(field => !localFqnPriority.value.includes(field)) || []).length
|
||||
}));
|
||||
});
|
||||
|
||||
// Computed: Get fields from the selected semantic group that aren't already in priority list
|
||||
const availableFieldsFromGroup = computed(() => {
|
||||
if (!selectedSemanticGroup.value) return [];
|
||||
|
||||
const semanticGroups = localSemanticGroups.value.length > 0
|
||||
? localSemanticGroups.value
|
||||
: localConfig.value.semantic_field_groups || [];
|
||||
|
||||
const group = semanticGroups.find(g => g.id === selectedSemanticGroup.value);
|
||||
if (!group?.fields) return [];
|
||||
|
||||
// Filter out reserved IDs and fields already in the priority list
|
||||
return group.fields
|
||||
.filter(field => {
|
||||
// Exclude reserved/computed field names
|
||||
if (RESERVED_GROUP_IDS.includes(field?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
return !localFqnPriority.value.includes(field);
|
||||
})
|
||||
.map(field => ({
|
||||
label: field,
|
||||
value: field
|
||||
}));
|
||||
});
|
||||
|
||||
// Get display name for a dimension - check semantic groups first, then format the ID
|
||||
const getDimensionDisplay = (dimId: string): string => {
|
||||
const semanticGroups = localSemanticGroups.value.length > 0
|
||||
? localSemanticGroups.value
|
||||
: localConfig.value.semantic_field_groups;
|
||||
const group = semanticGroups.find(g => g.id === dimId);
|
||||
if (group?.display) {
|
||||
return group.display;
|
||||
}
|
||||
// Format the dimension ID as a display name (e.g., "k8s-deployment" -> "K8s Deployment")
|
||||
return dimId
|
||||
.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Get fields for a dimension from semantic groups (for description)
|
||||
const getFieldsForDimension = (dimId: string): string => {
|
||||
const semanticGroups = localSemanticGroups.value.length > 0
|
||||
? localSemanticGroups.value
|
||||
: localConfig.value.semantic_field_groups;
|
||||
const group = semanticGroups.find(g => g.id === dimId);
|
||||
if (group?.fields?.length) {
|
||||
return group.fields.slice(0, 3).join(", ") + (group.fields.length > 3 ? "..." : "");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const handleSemanticGroupsUpdate = (groups: SemanticFieldGroup[]) => {
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
const filteredGroups = groups.filter(g => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
localConfig.value.semantic_field_groups = filteredGroups;
|
||||
localSemanticGroups.value = filteredGroups;
|
||||
};
|
||||
|
||||
// FQN Priority dimension management
|
||||
const moveFqnDimensionUp = (index: number) => {
|
||||
if (index === 0) return;
|
||||
const dims = [...localFqnPriority.value];
|
||||
[dims[index - 1], dims[index]] = [dims[index], dims[index - 1]];
|
||||
localFqnPriority.value = dims;
|
||||
};
|
||||
|
||||
const moveFqnDimensionDown = (index: number) => {
|
||||
if (index >= localFqnPriority.value.length - 1) return;
|
||||
const dims = [...localFqnPriority.value];
|
||||
[dims[index], dims[index + 1]] = [dims[index + 1], dims[index]];
|
||||
localFqnPriority.value = dims;
|
||||
};
|
||||
|
||||
const removeFqnDimension = (index: number) => {
|
||||
localFqnPriority.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const addFqnDimension = () => {
|
||||
if (!newFqnDimension.value) return;
|
||||
localFqnPriority.value.push(newFqnDimension.value);
|
||||
newFqnDimension.value = null;
|
||||
// Clear group selection if no more fields available from this group
|
||||
if (availableFieldsFromGroup.value.length === 0) {
|
||||
selectedSemanticGroup.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const resetFqnPriority = () => {
|
||||
// Reset to backend defaults from O2_FQN_PRIORITY_DIMENSIONS
|
||||
const backendDefaults = store.state.zoConfig?.fqn_priority_dimensions || [];
|
||||
localFqnPriority.value = [...backendDefaults];
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
saving.value = true;
|
||||
try {
|
||||
// Merge FQN priority into config
|
||||
localConfig.value.fqn_priority_dimensions = localFqnPriority.value;
|
||||
localConfig.value.semantic_field_groups = localSemanticGroups.value;
|
||||
|
||||
// Save FQN priority dimensions using settings v2 API (org-level setting)
|
||||
await settingsService.setOrgSetting(
|
||||
props.orgId,
|
||||
"fqn_priority_dimensions",
|
||||
localFqnPriority.value,
|
||||
"correlation",
|
||||
"FQN priority dimensions for service correlation"
|
||||
);
|
||||
|
||||
// Also save semantic field groups to deduplication config API
|
||||
await alertsService.setOrganizationDeduplicationConfig(
|
||||
props.orgId,
|
||||
localConfig.value,
|
||||
);
|
||||
|
||||
$q.notify({
|
||||
type: "positive",
|
||||
message: t("settings.correlation.configSavedSuccess"),
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
emit("saved");
|
||||
} catch (error: any) {
|
||||
console.error("Error saving service identity settings:", error);
|
||||
$q.notify({
|
||||
type: "negative",
|
||||
message: error?.message || t("settings.correlation.configSaveFailed"),
|
||||
timeout: 3000,
|
||||
});
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch config on mount if not provided
|
||||
const loadConfig = async () => {
|
||||
// Get backend defaults for FQN priority dimensions
|
||||
const backendDefaults = store.state.zoConfig?.fqn_priority_dimensions || [];
|
||||
|
||||
if (!props.config) {
|
||||
try {
|
||||
// First, try to load FQN priority from settings v2 API
|
||||
let fqnPriorityFromSettings: string[] | null = null;
|
||||
try {
|
||||
const settingResponse = await settingsService.getSetting(props.orgId, "fqn_priority_dimensions");
|
||||
const setting = settingResponse.data;
|
||||
console.log("ServiceIdentityConfig: Loaded FQN setting from v2 API:", setting);
|
||||
if (setting?.setting_value && Array.isArray(setting.setting_value) && setting.setting_value.length > 0) {
|
||||
fqnPriorityFromSettings = setting.setting_value;
|
||||
}
|
||||
} catch (settingError: any) {
|
||||
// 404 means setting not found, which is fine - use defaults
|
||||
if (settingError?.response?.status !== 404) {
|
||||
console.log("ServiceIdentityConfig: Error loading settings v2, using defaults:", settingError);
|
||||
}
|
||||
}
|
||||
|
||||
// Load deduplication config for semantic field groups
|
||||
const response = await alertsService.getOrganizationDeduplicationConfig(props.orgId);
|
||||
const config = response.data;
|
||||
console.log("ServiceIdentityConfig: Loaded dedup config:", config);
|
||||
|
||||
// Use settings v2 FQN priority if available, otherwise use backend defaults
|
||||
const fqnPriority = fqnPriorityFromSettings ?? [...backendDefaults];
|
||||
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
const filteredSemanticGroups = (config.semantic_field_groups ?? [])
|
||||
.filter((g: SemanticFieldGroup) => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
|
||||
localConfig.value = {
|
||||
enabled: config.enabled ?? true,
|
||||
alert_dedup_enabled: config.alert_dedup_enabled ?? true,
|
||||
alert_fingerprint_groups: config.alert_fingerprint_groups ?? [],
|
||||
time_window_minutes: config.time_window_minutes ?? undefined,
|
||||
semantic_field_groups: filteredSemanticGroups,
|
||||
fqn_priority_dimensions: fqnPriority,
|
||||
};
|
||||
localFqnPriority.value = fqnPriority;
|
||||
localSemanticGroups.value = filteredSemanticGroups;
|
||||
} catch (error) {
|
||||
console.log("ServiceIdentityConfig: No existing config, loading defaults", error);
|
||||
|
||||
// Load default semantic groups from backend
|
||||
try {
|
||||
const semanticGroupsResponse = await alertsService.getSemanticGroups(props.orgId);
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
const defaultGroups = (semanticGroupsResponse.data ?? [])
|
||||
.filter((g: SemanticFieldGroup) => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
console.log(`Loaded ${defaultGroups.length} default semantic groups`);
|
||||
|
||||
localConfig.value = {
|
||||
enabled: true,
|
||||
alert_dedup_enabled: true,
|
||||
alert_fingerprint_groups: [],
|
||||
time_window_minutes: undefined,
|
||||
semantic_field_groups: defaultGroups,
|
||||
fqn_priority_dimensions: [...backendDefaults],
|
||||
};
|
||||
localFqnPriority.value = [...backendDefaults];
|
||||
localSemanticGroups.value = defaultGroups;
|
||||
} catch (semanticError) {
|
||||
console.error("Failed to load default semantic groups:", semanticError);
|
||||
localFqnPriority.value = [...backendDefaults];
|
||||
localSemanticGroups.value = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("ServiceIdentityConfig: Using config from props:", props.config);
|
||||
localFqnPriority.value = props.config.fqn_priority_dimensions ?? [...backendDefaults];
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
localSemanticGroups.value = (props.config.semantic_field_groups ?? [])
|
||||
.filter(g => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
}
|
||||
};
|
||||
|
||||
// Load config on mount
|
||||
loadConfig();
|
||||
|
||||
// Watch for external changes
|
||||
watch(
|
||||
() => props.config,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
const backendDefaults = store.state.zoConfig?.fqn_priority_dimensions || [];
|
||||
console.log("ServiceIdentityConfig: Config changed from props:", newVal);
|
||||
// Filter out reserved IDs like service-fqn (it's the output, not an input)
|
||||
const filteredSemanticGroups = (newVal.semantic_field_groups ?? [])
|
||||
.filter(g => !RESERVED_GROUP_IDS.includes(g.id?.toLowerCase()));
|
||||
localConfig.value = {
|
||||
enabled: newVal.enabled ?? true,
|
||||
alert_dedup_enabled: newVal.alert_dedup_enabled ?? true,
|
||||
alert_fingerprint_groups: newVal.alert_fingerprint_groups ?? [],
|
||||
time_window_minutes: newVal.time_window_minutes ?? undefined,
|
||||
semantic_field_groups: filteredSemanticGroups,
|
||||
fqn_priority_dimensions: newVal.fqn_priority_dimensions ?? [...backendDefaults],
|
||||
};
|
||||
localFqnPriority.value = newVal.fqn_priority_dimensions ?? [...backendDefaults];
|
||||
localSemanticGroups.value = filteredSemanticGroups;
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.q-card {
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
:deep(.tooltip-text) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -196,6 +196,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
>{{ t('regex_patterns.title') }}</span>
|
||||
</div>
|
||||
</q-route-tab>
|
||||
<q-route-tab
|
||||
v-if="config.isEnterprise == 'true'"
|
||||
data-test="correlation-settings-tab"
|
||||
name="correlation_settings"
|
||||
:to="{
|
||||
name: 'correlationSettings',
|
||||
query: {
|
||||
org_identifier: store.state.selectedOrganization?.identifier,
|
||||
},
|
||||
}"
|
||||
icon="group_work"
|
||||
:label="t('settings.correlationSettings')"
|
||||
content-class="tab_content"
|
||||
/>
|
||||
</q-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -350,9 +350,9 @@ describe("useManagementRoutes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should have exactly 10 children routes when enterprise is enabled", () => {
|
||||
it("should have exactly 12 children routes when enterprise is enabled", () => {
|
||||
const routes = useManagementRoutes();
|
||||
expect(routes[0].children).toHaveLength(11); // 4 base + 6 enterprise
|
||||
expect(routes[0].children).toHaveLength(12); // 4 base + 8 enterprise (query_management, cipherKeys, pipelineDestinations, nodes, domainManagement, regexPatterns, correlationSettings, license)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -420,9 +420,9 @@ describe("useManagementRoutes", () => {
|
||||
expect(routes[0].children.length).toBeGreaterThan(10);
|
||||
});
|
||||
|
||||
it("should have exactly 11 children routes when both enterprise and cloud are enabled", () => {
|
||||
it("should have exactly 13 children routes when both enterprise and cloud are enabled", () => {
|
||||
const routes = useManagementRoutes();
|
||||
expect(routes[0].children).toHaveLength(12); // 4 base + 6 enterprise + 1 cloud
|
||||
expect(routes[0].children).toHaveLength(13); // 4 base + 8 enterprise + 1 cloud
|
||||
});
|
||||
|
||||
it("should have all enterprise routes when both are enabled", () => {
|
||||
|
||||
@@ -141,7 +141,19 @@ const useManagementRoutes = () => {
|
||||
beforeEnter(to: any, from: any, next: any) {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
path: "correlation_settings",
|
||||
name: "correlationSettings",
|
||||
component: () => import("@/components/settings/CorrelationSettings.vue"),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
title: "Correlation Settings",
|
||||
},
|
||||
beforeEnter(to: any, from: any, next: any) {
|
||||
routeGuard(to, from, next);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "license",
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface MetricsCorrelationConfig {
|
||||
sourceStream?: string; // Original stream being viewed
|
||||
sourceType?: string; // Type of source stream
|
||||
availableDimensions?: Record<string, string>; // Actual field names (for source stream queries)
|
||||
metricSchemas?: Record<string, any>; // Cached metric schemas with metrics_meta
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,18 +49,7 @@ export function useMetricsCorrelationDashboard() {
|
||||
return createMetricPanel(stream, index, config);
|
||||
});
|
||||
|
||||
// Create dashboard variables from matched dimensions (using correct structure)
|
||||
const variablesList = Object.entries(config.matchedDimensions).map(([key, value]) => ({
|
||||
type: "textbox" as const,
|
||||
name: key,
|
||||
label: key,
|
||||
value: value,
|
||||
multiSelect: false,
|
||||
isLoading: false,
|
||||
isVariableLoading: false,
|
||||
description: `Filter by ${key}`,
|
||||
}));
|
||||
|
||||
// No variables in the metrics dashboard - dimensions are managed at the top level
|
||||
const dashboard = {
|
||||
version: 5,
|
||||
dashboardId: ``,
|
||||
@@ -69,7 +59,7 @@ export function useMetricsCorrelationDashboard() {
|
||||
owner: "",
|
||||
created: new Date().toISOString(),
|
||||
variables: {
|
||||
list: variablesList,
|
||||
list: [],
|
||||
showDynamicFilters: false,
|
||||
},
|
||||
tabs: [
|
||||
@@ -99,8 +89,37 @@ export function useMetricsCorrelationDashboard() {
|
||||
index: number,
|
||||
config: MetricsCorrelationConfig
|
||||
) => {
|
||||
// Get schema information for this metric stream
|
||||
const schema = config.metricSchemas?.[stream.stream_name];
|
||||
const metricsMeta = schema?.metrics_meta;
|
||||
const rawUnit = metricsMeta?.unit || "";
|
||||
const metricType = (metricsMeta?.metric_type || "").toLowerCase();
|
||||
|
||||
console.log(`[useMetricsCorrelationDashboard] Stream: ${stream.stream_name}, Unit: ${rawUnit}, Type: ${metricType}`);
|
||||
|
||||
// Map OpenTelemetry/Prometheus units to dashboard units
|
||||
const unitMapping: Record<string, string> = {
|
||||
"By": "bytes",
|
||||
"s": "seconds",
|
||||
"ms": "milliseconds",
|
||||
"us": "microseconds",
|
||||
"ns": "nanoseconds",
|
||||
"{cpu}": "percentunit", // CPU as percentage
|
||||
"1": "percentunit", // Dimensionless ratio (0-1)
|
||||
"%": "percent",
|
||||
};
|
||||
const unit = unitMapping[rawUnit] || rawUnit || "short";
|
||||
|
||||
// Determine aggregation function based on metric type
|
||||
// Counter: sum to see total increase over time buckets
|
||||
// Gauge: avg or latest value
|
||||
// Histogram/Summary: need special handling
|
||||
const isCounter = metricType === "counter";
|
||||
const aggregationFunc = isCounter ? "sum" : "avg";
|
||||
|
||||
// Build WHERE clause from stream filters
|
||||
// Quote field names that contain special characters (hyphens, dots, etc.)
|
||||
// Note: "_o2_all_" is a special value that the backend recognizes to skip filtering
|
||||
const whereConditions = Object.entries(stream.filters)
|
||||
.map(([field, value]) => {
|
||||
// Quote field name if it contains special characters
|
||||
@@ -114,7 +133,8 @@ export function useMetricsCorrelationDashboard() {
|
||||
|
||||
// Time-series SQL query for metrics
|
||||
// Note: Time range comes from dashboard defaultDatetimeDuration, not embedded in SQL
|
||||
const query = `SELECT histogram(_timestamp) as x_axis_1, avg(value) as y_axis_1
|
||||
// For counters, we sum the values to see total increase over time buckets
|
||||
const query = `SELECT histogram(_timestamp) as x_axis_1, ${aggregationFunc}(value) as y_axis_1
|
||||
FROM "${stream.stream_name}"
|
||||
${whereClause}
|
||||
GROUP BY x_axis_1
|
||||
@@ -128,11 +148,11 @@ ORDER BY x_axis_1`;
|
||||
id: `panel_${stream.stream_name}_${index}`,
|
||||
type: "line",
|
||||
title: stream.stream_name,
|
||||
description: `Time series for ${stream.stream_name}`,
|
||||
description: `Time series for ${stream.stream_name}${metricType ? ` (${metricType})` : ""}`,
|
||||
config: {
|
||||
show_legends: false,
|
||||
legends_position: "bottom",
|
||||
unit: "short",
|
||||
unit: unit,
|
||||
unit_custom: "",
|
||||
promql_legend: "",
|
||||
axis_border_show: true,
|
||||
@@ -263,20 +283,51 @@ ORDER BY x_axis_1`;
|
||||
// Use only the matched dimensions from availableDimensions
|
||||
// Extract only the fields that correspond to matched dimension keys
|
||||
filters = {};
|
||||
|
||||
console.log("[useMetricsCorrelationDashboard] availableDimensions:", config.availableDimensions);
|
||||
console.log("[useMetricsCorrelationDashboard] matchedDimensions:", config.matchedDimensions);
|
||||
|
||||
if (config.availableDimensions && config.matchedDimensions) {
|
||||
// For each matched dimension, find the actual field name in availableDimensions
|
||||
// Build a reverse mapping: value -> field name from availableDimensions
|
||||
const valueToFieldMap = new Map<string, string>();
|
||||
for (const [fieldName, fieldValue] of Object.entries(config.availableDimensions)) {
|
||||
if (typeof fieldValue === 'string') {
|
||||
valueToFieldMap.set(String(fieldValue), fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[useMetricsCorrelationDashboard] valueToFieldMap:", Object.fromEntries(valueToFieldMap));
|
||||
|
||||
// For each matched dimension, find the actual field name
|
||||
for (const [semanticKey, value] of Object.entries(config.matchedDimensions)) {
|
||||
// Try to find a field in availableDimensions that matches this value
|
||||
for (const [fieldName, fieldValue] of Object.entries(config.availableDimensions)) {
|
||||
if (String(fieldValue) === String(value) && typeof fieldValue === 'string') {
|
||||
filters[fieldName] = String(fieldValue);
|
||||
break; // Found the field, move to next dimension
|
||||
if (value === "_o2_all_") {
|
||||
// For _o2_all_, we need to find the field name by matching against the semantic key
|
||||
// Try exact match first with underscores
|
||||
const normalizedKey = semanticKey.replace(/-/g, '_');
|
||||
if (config.availableDimensions[normalizedKey] !== undefined) {
|
||||
filters[normalizedKey] = "_o2_all_";
|
||||
} else {
|
||||
// Try to find by partial match
|
||||
for (const fieldName of Object.keys(config.availableDimensions)) {
|
||||
if (fieldName.toLowerCase() === normalizedKey.toLowerCase()) {
|
||||
filters[fieldName] = "_o2_all_";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For actual values, use the reverse mapping
|
||||
const fieldName = valueToFieldMap.get(value);
|
||||
if (fieldName) {
|
||||
filters[fieldName] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filters = config.matchedDimensions;
|
||||
}
|
||||
|
||||
console.log("[useMetricsCorrelationDashboard] Final filters for logs:", filters);
|
||||
} else if (streams && streams.length > 0) {
|
||||
// Use correlated log streams from API response
|
||||
const primaryStream = streams[0];
|
||||
@@ -289,6 +340,7 @@ ORDER BY x_axis_1`;
|
||||
|
||||
// Build WHERE clause from filters
|
||||
// Filter out non-string values and internal fields
|
||||
// Note: "_o2_all_" is a special value that the backend recognizes to skip filtering
|
||||
const whereConditions = Object.entries(filters)
|
||||
.filter(([field, value]) => {
|
||||
// Only include string values and skip internal fields
|
||||
@@ -335,7 +387,16 @@ ORDER BY x_axis_1`;
|
||||
fields: {
|
||||
stream: streamName,
|
||||
stream_type: "logs",
|
||||
x: [],
|
||||
x: [
|
||||
{
|
||||
label: "",
|
||||
alias: "x_axis_1",
|
||||
column: "x_axis_1",
|
||||
color: null,
|
||||
isDerived: false,
|
||||
havingConditions: [],
|
||||
},
|
||||
],
|
||||
y: [],
|
||||
z: [],
|
||||
breakdown: [],
|
||||
@@ -348,7 +409,7 @@ ORDER BY x_axis_1`;
|
||||
config: {
|
||||
limit: 150,
|
||||
promql_legend: "",
|
||||
layer_type: "scatter",
|
||||
layer_type: "",
|
||||
weight_fixed: 1,
|
||||
min: 0,
|
||||
max: 100,
|
||||
@@ -360,8 +421,8 @@ ORDER BY x_axis_1`;
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 192,
|
||||
h: 48,
|
||||
i: "logs_table_panel",
|
||||
h: 36,
|
||||
i: 1,
|
||||
},
|
||||
htmlContent: "",
|
||||
markdownContent: "",
|
||||
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Fehler beim Speichern der Domain-Einstellungen",
|
||||
"noDomainMessage": "Keine Domänen konfiguriert. Fügen Sie oben die Domains hinzu, um den Zugriff einzuschränken.",
|
||||
"license": "Lizenz",
|
||||
"alertCorrelation": "Warnungs-Deduplizierung"
|
||||
"alertCorrelation": "Warnungs-Deduplizierung",
|
||||
"correlationSettings": "Korrelationseinstellungen",
|
||||
"correlation": {
|
||||
"title": "Korrelationseinstellungen",
|
||||
"subtitle": "Erkennen und verwalten Sie Servicekorrelationen zwischen Protokollen, Traces und Metriken",
|
||||
"serviceIdentityTab": "Identität des Dienstes",
|
||||
"discoveredServicesTab": "Entdeckte Dienste",
|
||||
"alertCorrelationTab": "Korrelation von Warnmeldungen",
|
||||
"discoveredServicesTitle": "Entdeckte Dienste",
|
||||
"discoveredServicesDescription": "Dienste werden automatisch anhand Ihrer Telemetriedaten erkannt und zur Korrelation nach ihrem vollqualifizierten Namen (FQN) gruppiert. Dienste, die denselben FQN verwenden, werden anhand von Protokollen, Traces und Metriken miteinander korreliert.",
|
||||
"noServicesYet": "Es wurden noch keine Dienste entdeckt",
|
||||
"noServicesDescription": "Dienste werden hier angezeigt, wenn Telemetriedaten mit Dienstidentifikationsattributen (wie k8s-deployment, Dienstname usw.) aufgenommen werden.",
|
||||
"fqns": "FQNs",
|
||||
"services": "Dienstleistungen",
|
||||
"fullyCorrelated": "Vollständig korreliert",
|
||||
"suggestions": "Anregungen",
|
||||
"clickToViewSuggestions": "Klicken Sie hier, um Vorschläge zur Verbesserung der Korrelation anzuzeigen",
|
||||
"byFqn": "Von FQN",
|
||||
"byService": "Von Service",
|
||||
"byStream": "Von Stream",
|
||||
"searchFqnOrService": "Suche nach FQN oder Dienstnamen...",
|
||||
"searchServiceName": "Dienstname suchen...",
|
||||
"searchStreamName": "Stream-Namen suchen...",
|
||||
"allServices": "Alle Dienste",
|
||||
"missingTelemetry": "Fehlende Telemetrie",
|
||||
"fqn": "LÜFTER",
|
||||
"correlationKey": "Korrelationsschlüssel",
|
||||
"telemetryCoverage": "Telemetrieabdeckung",
|
||||
"serviceName": "Name des Dienstes",
|
||||
"streams": "Streams",
|
||||
"fqnSource": "FQN-Quelle",
|
||||
"viewDimensions": "Abmessungen ansehen",
|
||||
"viewServices": "Klicken Sie hier, um Dienste anzuzeigen",
|
||||
"dimensionsFor": "Abmessungen für",
|
||||
"correlatedViaSharedDimensions": "Dienst (e) korreliert über gemeinsame Dimensionen",
|
||||
"correlationDimensions": "Korrelationsdimensionen",
|
||||
"noSharedDimensions": "Keine gemeinsamen Dimensionen für alle Dienste gefunden",
|
||||
"suggestionsDialogTitle": "Korrelationsvorschläge",
|
||||
"suggestionsDialogSubtitle": "Dienste, die korreliert werden könnten, denen jedoch Telemetrietypen fehlen",
|
||||
"suggestionsInfoText": "Um eine vollständige Korrelation zu ermöglichen, stellen Sie sicher, dass Ihre Telemetriedaten konsistente Service-Identifikationsattribute (wie k8s-deployment, Dienstname) für Protokolle, Traces und Metriken enthalten.",
|
||||
"addTracingSuggestion": "Fügen Sie OpenTelemetry-Tracing-Instrumente hinzu, um eine vollständige Korrelation zu ermöglichen",
|
||||
"addLogsSuggestion": "Konfigurieren Sie die Protokollerfassung mit passenden Serviceattributen",
|
||||
"addMetricsSuggestion": "Metriksammlung mit passenden Servicedimensionen hinzufügen",
|
||||
"missing": "Fehlt",
|
||||
"deployment": "Einsatz",
|
||||
"statefulSet": "StatefulSet",
|
||||
"daemonSet": "Dämonset",
|
||||
"ecsTask": "ECS-Aufgabe",
|
||||
"faas": "Häufig gestellte Fragen",
|
||||
"service": "Bedienung",
|
||||
"serviceIdentityTitle": "Konfiguration der Dienstidentität",
|
||||
"serviceIdentityDescription": "Konfigurieren Sie, wie Dienste anhand von Protokollen, Traces und Metriken identifiziert und korreliert werden. Der Service-FQN (Fully Qualified Name) wird aus den Workload-Dimensionen in der Reihenfolge ihrer Priorität abgeleitet.",
|
||||
"howItWorksTitle": "So funktioniert Service Identity",
|
||||
"howItWorksDescription": "(Vollständig qualifizierter Name) wird anhand Ihrer Telemetriedimensionen berechnet, um Dienste aus verschiedenen Datenquellen eindeutig zu identifizieren und zu korrelieren.",
|
||||
"priorityOrderLabel": "Prioritätsreihenfolge:",
|
||||
"priorityOrderDescription": "Die Abmessungen werden in der unten aufgeführten Reihenfolge überprüft. Die erste Dimension mit einem Wert wird zur Service-FQN.",
|
||||
"exampleLabel": "Beispiel:",
|
||||
"exampleText1": "Wenn du beides hast",
|
||||
"exampleText2": "und",
|
||||
"exampleText3": "das FQN wird",
|
||||
"exampleText4": "da",
|
||||
"exampleText5": "hat eine höhere Priorität.",
|
||||
"correlationDescription": "Dienste mit derselben FQN sind korreliert, auch wenn sie unterschiedliche",
|
||||
"correlationDescriptionField": "Bedienung",
|
||||
"correlationDescriptionEnd": "Namen.",
|
||||
"fqnPriorityTitle": "FQN-Prioritätsdimensionen",
|
||||
"fqnPriorityTooltip": "Definiert, welche Dimensionen verwendet werden, um den Service-FQN für die Korrelation abzuleiten. Die erste Dimension mit einem Wert gewinnt. Ordnen Sie die Bestellung entsprechend Ihrer Infrastruktur neu an.",
|
||||
"fqnPriorityDescription": "Ziehen Sie, um die Reihenfolge zu ändern, oder verwenden Sie die Pfeiltasten. Die erste übereinstimmende Dimension bestimmt die Service-FQN.",
|
||||
"resetToDefaults": "Auf Standardwerte zurücksetzen",
|
||||
"noDimensionsConfigured": "Keine Dimensionen konfiguriert. Klicken Sie auf „Auf Standardwerte zurücksetzen“ oder fügen Sie unten Dimensionen hinzu.",
|
||||
"addDimensionLabel": "Dimension zur Prioritätsliste hinzufügen",
|
||||
"moveUp": "Aufsteigen (höhere Priorität)",
|
||||
"moveDown": "Nach unten bewegen (niedrigere Priorität)",
|
||||
"removeFromList": "Aus der Prioritätenliste entfernen",
|
||||
"semanticFieldTitle": "Semantische Feldzuordnungen",
|
||||
"semanticFieldTooltip": "Ordnet verschiedene Feldnamen semantischen Dimensionen zu. Beispielsweise werden „deployment“, „k8s.deployment.name“ und „kube_deployment“ alle „k8s-deployment“ zugeordnet.",
|
||||
"semanticFieldDescription": "Konfigurieren Sie, wie Feldnamen aus Ihrer Telemetrie semantischen Dimensionen zugeordnet werden.",
|
||||
"saveConfiguration": "Konfiguration speichern",
|
||||
"configSavedSuccess": "Die Konfiguration der Dienstidentität wurde erfolgreich gespeichert",
|
||||
"configSaveFailed": "Einstellungen konnten nicht gespeichert werden",
|
||||
"correlationSettingsSaved": "Die Korrelationseinstellungen wurden erfolgreich gespeichert.",
|
||||
"logs": "Logs",
|
||||
"traces": "Spuren",
|
||||
"metrics": "Metriken",
|
||||
"retry": "Erneut versuchen",
|
||||
"k8sDeployment": "Name der Kubernetes-Bereitstellung",
|
||||
"k8sStatefulSet": "Kubernetes StatefulSet-Name",
|
||||
"k8sDaemonSet": "Kubernetes DaemonSet (Name)",
|
||||
"k8sJob": "Kubernetes-Jobname",
|
||||
"k8sReplicaSet": "Kubernetes ReplicaSet-Name",
|
||||
"k8sNamespace": "Kubernetes-Namespace",
|
||||
"awsEcsTask": "AWS ECS Task-Familie",
|
||||
"faasName": "Lambda, Cloud-Funktionen usw.",
|
||||
"gcpCloudRun": "Google Cloud Run-Dienst",
|
||||
"azureCloudRole": "Azure Cloud-Rollenname",
|
||||
"processName": "Name des Bare-Metal-Prozesses",
|
||||
"serviceDim": "Telemetriedienst.name öffnen",
|
||||
"host": "Host-/Knotenname",
|
||||
"environment": "Bereitstellungsumgebung"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Nutzer",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Abgestimmte Abmessungen",
|
||||
"metricsButton": "{count} Metrik (en)",
|
||||
"metricsTooltip": "Wählen Sie die anzuzeigenden Metrik-Streams aus",
|
||||
"selectMetrics": "Wählen Sie Metric Streams"
|
||||
"selectMetrics": "Wählen Sie Metric Streams",
|
||||
"correlatedLogs": "Korrelierte Protokolle",
|
||||
"correlatedMetrics": "Korrelierte Metriken",
|
||||
"correlatedTraces": "Korrelierte Spuren",
|
||||
"clickToLoadLogs": "Klicken Sie hier, um korrelierte Protokolle zu laden",
|
||||
"clickToLoadMetrics": "Klicken Sie hier, um korrelierte Metriken zu laden",
|
||||
"clickToLoadTraces": "Klicken Sie hier, um korrelierte Traces zu laden",
|
||||
"loadingLogs": "Logs werden geladen...",
|
||||
"noLogsFound": "Keine korrelierten Protokolle gefunden",
|
||||
"service": "Dienst: {service}",
|
||||
"tracesPlaceholder": "Traces werden hier implementiert",
|
||||
"correlatedTracesFor": "Korrelierte Traces für Service: {service}",
|
||||
"metricsSelector": "{ausgewählt} von {total} Metrik (en)",
|
||||
"dimensions": "Filter",
|
||||
"all": "Alle",
|
||||
"loadingTraces": "Korrelierte Traces werden geladen...",
|
||||
"tracesError": "Ablaufverfolgungen konnten nicht geladen werden",
|
||||
"directTraceMatch": "Direkter Trace-Abgleich",
|
||||
"spans": "Spanne (n)",
|
||||
"dimensionBasedCorrelation": "Dimensionsbasierte Korrelation",
|
||||
"tracesFromService": "Spuren vom Dienst: {service}",
|
||||
"noTracesFound": "Keine korrelierten Spuren gefunden",
|
||||
"noTracesDescription": "Keine Spuren für Service {service} im ausgewählten Zeitraum gefunden"
|
||||
}
|
||||
}
|
||||
@@ -1485,6 +1485,7 @@
|
||||
"settings": {
|
||||
"header": "Management",
|
||||
"alertCorrelation": "Alert Deduplication",
|
||||
"correlationSettings": "Correlation Settings",
|
||||
"userKeyLabel": "User Key",
|
||||
"rumKeyLabel": "RUM Key",
|
||||
"newUserKeyBtnLbl": "Add User Key",
|
||||
@@ -1574,7 +1575,100 @@
|
||||
"domainSettingsSaved": "Domain settings saved successfully",
|
||||
"errorSavingDomainSettings": "Error saving domain settings",
|
||||
"noDomainMessage": "No domains configured. Add domains above to restrict access.",
|
||||
"license": "License"
|
||||
"license": "License",
|
||||
"correlation": {
|
||||
"title": "Correlation Settings",
|
||||
"subtitle": "Discover and manage service correlations across logs, traces, and metrics",
|
||||
"serviceIdentityTab": "Service Identity",
|
||||
"discoveredServicesTab": "Discovered Services",
|
||||
"alertCorrelationTab": "Alert Correlation",
|
||||
"discoveredServicesTitle": "Discovered Services",
|
||||
"discoveredServicesDescription": "Services are automatically discovered from your telemetry data and grouped by their Fully Qualified Name (FQN) for correlation. Services sharing the same FQN are correlated together across logs, traces, and metrics.",
|
||||
"noServicesYet": "No services discovered yet",
|
||||
"noServicesDescription": "Services will appear here as telemetry data is ingested with service identification attributes (like k8s-deployment, service name, etc.)",
|
||||
"fqns": "FQNs",
|
||||
"services": "Services",
|
||||
"fullyCorrelated": "Fully Correlated",
|
||||
"suggestions": "suggestions",
|
||||
"clickToViewSuggestions": "Click to view correlation improvement suggestions",
|
||||
"byFqn": "By FQN",
|
||||
"byService": "By Service",
|
||||
"byStream": "By Stream",
|
||||
"searchFqnOrService": "Search FQN or service name...",
|
||||
"searchServiceName": "Search service name...",
|
||||
"searchStreamName": "Search stream name...",
|
||||
"allServices": "All Services",
|
||||
"missingTelemetry": "Missing Telemetry",
|
||||
"fqn": "FQN",
|
||||
"correlationKey": "Correlation Key",
|
||||
"telemetryCoverage": "Telemetry Coverage",
|
||||
"serviceName": "Service Name",
|
||||
"streams": "Streams",
|
||||
"fqnSource": "FQN Source",
|
||||
"viewDimensions": "View dimensions",
|
||||
"viewServices": "Click to view services",
|
||||
"dimensionsFor": "Dimensions for",
|
||||
"correlatedViaSharedDimensions": "service(s) correlated via shared dimensions",
|
||||
"correlationDimensions": "Correlation Dimensions",
|
||||
"noSharedDimensions": "No shared dimensions found across services",
|
||||
"suggestionsDialogTitle": "Correlation Suggestions",
|
||||
"suggestionsDialogSubtitle": "Services that could be correlated but are missing telemetry types",
|
||||
"suggestionsInfoText": "To enable full correlation, ensure your telemetry data includes consistent service identification attributes (like k8s-deployment, service name) across logs, traces, and metrics.",
|
||||
"addTracingSuggestion": "Add OpenTelemetry tracing instrumentation to enable full correlation",
|
||||
"addLogsSuggestion": "Configure log collection with matching service attributes",
|
||||
"addMetricsSuggestion": "Add metrics collection with matching service dimensions",
|
||||
"missing": "Missing",
|
||||
"deployment": "Deployment",
|
||||
"statefulSet": "StatefulSet",
|
||||
"daemonSet": "DaemonSet",
|
||||
"ecsTask": "ECS Task",
|
||||
"faas": "FaaS",
|
||||
"service": "Service",
|
||||
"serviceIdentityTitle": "Service Identity Configuration",
|
||||
"serviceIdentityDescription": "Configure how services are identified and correlated across logs, traces, and metrics. The Service FQN (Fully Qualified Name) is derived from workload dimensions in priority order.",
|
||||
"howItWorksTitle": "How Service Identity Works",
|
||||
"howItWorksDescription": "(Fully Qualified Name) is computed from your telemetry dimensions to uniquely identify and correlate services across different data sources.",
|
||||
"priorityOrderLabel": "Priority Order:",
|
||||
"priorityOrderDescription": "Dimensions are checked in the order listed below. The first dimension with a value becomes the Service FQN.",
|
||||
"exampleLabel": "Example:",
|
||||
"exampleText1": "If you have both",
|
||||
"exampleText2": "and",
|
||||
"exampleText3": "the FQN will be",
|
||||
"exampleText4": "because",
|
||||
"exampleText5": "has higher priority.",
|
||||
"correlationDescription": "Services with the same FQN are correlated, even if they have different",
|
||||
"correlationDescriptionField": "service",
|
||||
"correlationDescriptionEnd": "names.",
|
||||
"fqnPriorityTitle": "FQN Priority Dimensions",
|
||||
"fqnPriorityTooltip": "Defines which dimensions are used to derive the Service FQN for correlation. First dimension with a value wins. Reorder to match your infrastructure.",
|
||||
"fqnPriorityDescription": "Drag to reorder or use arrow buttons. First matching dimension determines Service FQN.",
|
||||
"resetToDefaults": "Reset to Defaults",
|
||||
"noDimensionsConfigured": "No dimensions configured. Click \"Reset to Defaults\" or add dimensions below.",
|
||||
"addDimensionLabel": "Add dimension to priority list",
|
||||
"addDimensionHint": "Select a semantic group, then choose a field to add to the priority list.",
|
||||
"selectSemanticGroup": "Select Semantic Group",
|
||||
"selectField": "Select Field",
|
||||
"fieldsAvailable": "fields available",
|
||||
"allFieldsAdded": "All fields from this group are already added",
|
||||
"selectGroupFirst": "Select a semantic group first",
|
||||
"noSemanticGroupsAvailable": "No semantic groups available. Configure field mappings below first.",
|
||||
"noSemanticGroupsConfigured": "No semantic groups configured. Add field mappings below to define available dimensions.",
|
||||
"configureSemanticGroupsFirst": "Configure semantic field mappings below to define available dimensions for FQN priority.",
|
||||
"moveUp": "Move up (higher priority)",
|
||||
"moveDown": "Move down (lower priority)",
|
||||
"removeFromList": "Remove from priority list",
|
||||
"semanticFieldTitle": "Semantic Field Mappings",
|
||||
"semanticFieldTooltip": "Maps different field names to semantic dimensions. For example, \"deployment\", \"k8s.deployment.name\", and \"kube_deployment\" all map to \"k8s-deployment\".",
|
||||
"semanticFieldDescription": "Configure how field names from your telemetry are mapped to semantic dimensions.",
|
||||
"saveConfiguration": "Save Configuration",
|
||||
"configSavedSuccess": "Service identity configuration saved successfully",
|
||||
"configSaveFailed": "Failed to save settings",
|
||||
"correlationSettingsSaved": "Correlation settings saved successfully.",
|
||||
"logs": "Logs",
|
||||
"traces": "Traces",
|
||||
"metrics": "Metrics",
|
||||
"retry": "Retry"
|
||||
}
|
||||
},
|
||||
"reports": {
|
||||
"header": "Reports",
|
||||
@@ -1770,7 +1864,29 @@
|
||||
"matchedDimensions": "Matched Dimensions",
|
||||
"metricsButton": "{count} Metric(s)",
|
||||
"metricsTooltip": "Select metric streams to display",
|
||||
"selectMetrics": "Select Metric Streams"
|
||||
"selectMetrics": "Select Metric Streams",
|
||||
"correlatedLogs": "Correlated Logs",
|
||||
"correlatedMetrics": "Correlated Metrics",
|
||||
"correlatedTraces": "Correlated Traces",
|
||||
"clickToLoadLogs": "Click to load correlated logs",
|
||||
"clickToLoadMetrics": "Click to load correlated metrics",
|
||||
"clickToLoadTraces": "Click to load correlated traces",
|
||||
"loadingLogs": "Loading logs...",
|
||||
"noLogsFound": "No correlated logs found",
|
||||
"service": "Service: {service}",
|
||||
"tracesPlaceholder": "Traces will be implemented here",
|
||||
"correlatedTracesFor": "Correlated traces for service: {service}",
|
||||
"metricsSelector": "{selected} of {total} Metric(s)",
|
||||
"dimensions": "Filters",
|
||||
"all": "All",
|
||||
"loadingTraces": "Loading correlated traces...",
|
||||
"tracesError": "Failed to load traces",
|
||||
"directTraceMatch": "Direct Trace Match",
|
||||
"spans": "span(s)",
|
||||
"dimensionBasedCorrelation": "Dimension-based Correlation",
|
||||
"tracesFromService": "Traces from service: {service}",
|
||||
"noTracesFound": "No correlated traces found",
|
||||
"noTracesDescription": "No traces found for service {service} in the selected time range"
|
||||
},
|
||||
"search_scheduler_job": {
|
||||
"title": "Search Job Scheduler",
|
||||
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Error al guardar la configuración del dominio",
|
||||
"noDomainMessage": "No hay dominios configurados. Agregue los dominios anteriores para restringir el acceso.",
|
||||
"license": "Licencia",
|
||||
"alertCorrelation": "Deduplicación de alertas"
|
||||
"alertCorrelation": "Deduplicación de alertas",
|
||||
"correlationSettings": "Configuración de correlación",
|
||||
"correlation": {
|
||||
"title": "Configuración de correlación",
|
||||
"subtitle": "Descubra y gestione las correlaciones de servicio entre registros, seguimientos y métricas",
|
||||
"serviceIdentityTab": "Identidad de servicio",
|
||||
"discoveredServicesTab": "Servicios descubiertos",
|
||||
"alertCorrelationTab": "Correlación de alertas",
|
||||
"discoveredServicesTitle": "Servicios descubiertos",
|
||||
"discoveredServicesDescription": "Los servicios se descubren automáticamente a partir de sus datos de telemetría y se agrupan por su nombre completo (FQN) para su correlación. Los servicios que comparten el mismo FQN se correlacionan entre sí en los registros, los seguimientos y las métricas.",
|
||||
"noServicesYet": "Aún no se ha descubierto ningún servicio",
|
||||
"noServicesDescription": "Los servicios aparecerán aquí a medida que los datos de telemetría se ingieran con los atributos de identificación del servicio (como k8s-deployment, nombre del servicio, etc.)",
|
||||
"fqns": "FQN",
|
||||
"services": "Servicios",
|
||||
"fullyCorrelated": "Totalmente correlacionado",
|
||||
"suggestions": "sugerencias",
|
||||
"clickToViewSuggestions": "Haga clic para ver las sugerencias de mejora de la correlación",
|
||||
"byFqn": "Por FQN",
|
||||
"byService": "Por servicio",
|
||||
"byStream": "Por Stream",
|
||||
"searchFqnOrService": "Buscar FQN o nombre de servicio...",
|
||||
"searchServiceName": "Nombre del servicio de búsqueda...",
|
||||
"searchStreamName": "Nombre de la secuencia de búsqueda...",
|
||||
"allServices": "Todos los servicios",
|
||||
"missingTelemetry": "Falta telemetría",
|
||||
"fqn": "FAN",
|
||||
"correlationKey": "Clave de correlación",
|
||||
"telemetryCoverage": "Cobertura de telemetría",
|
||||
"serviceName": "Nombre del servicio",
|
||||
"streams": "Transmisiones",
|
||||
"fqnSource": "Fuente FQN",
|
||||
"viewDimensions": "Ver dimensiones",
|
||||
"viewServices": "Haga clic para ver los servicios",
|
||||
"dimensionsFor": "Dimensiones para",
|
||||
"correlatedViaSharedDimensions": "servicio (s) correlacionados mediante dimensiones compartidas",
|
||||
"correlationDimensions": "Dimensiones de correlación",
|
||||
"noSharedDimensions": "No se han encontrado dimensiones compartidas en todos los servicios",
|
||||
"suggestionsDialogTitle": "Sugerencias de correlación",
|
||||
"suggestionsDialogSubtitle": "Servicios que podrían estar correlacionados pero a los que les faltan tipos de telemetría",
|
||||
"suggestionsInfoText": "Para permitir una correlación total, asegúrese de que sus datos de telemetría incluyan atributos de identificación de servicio coherentes (como k8s-deployment y el nombre del servicio) en todos los registros, rastreos y métricas.",
|
||||
"addTracingSuggestion": "Agregue la instrumentación de rastreo de OpenTelemetry para permitir una correlación total",
|
||||
"addLogsSuggestion": "Configurar la recopilación de registros con los atributos de servicio coincidentes",
|
||||
"addMetricsSuggestion": "Agregue la recopilación de métricas con dimensiones de servicio coincidentes",
|
||||
"missing": "Falta",
|
||||
"deployment": "Despliegue",
|
||||
"statefulSet": "Conjunto de estado",
|
||||
"daemonSet": "DaemonSet",
|
||||
"ecsTask": "Tarea ECS",
|
||||
"faas": "Preguntas frecuentes",
|
||||
"service": "Servicio",
|
||||
"serviceIdentityTitle": "Configuración de identidad de servicio",
|
||||
"serviceIdentityDescription": "Configure cómo se identifican y correlacionan los servicios en los registros, los seguimientos y las métricas. El FQN (nombre completo) del servicio se deriva de las dimensiones de la carga de trabajo en orden de prioridad.",
|
||||
"howItWorksTitle": "Cómo funciona Service Identity",
|
||||
"howItWorksDescription": "(Nombre completo) se calcula a partir de sus dimensiones de telemetría para identificar y correlacionar de forma única los servicios en diferentes fuentes de datos.",
|
||||
"priorityOrderLabel": "Orden de prioridad:",
|
||||
"priorityOrderDescription": "Las dimensiones se comprueban en el orden que se indica a continuación. La primera dimensión con un valor se convierte en el FQN del servicio.",
|
||||
"exampleLabel": "Ejemplo:",
|
||||
"exampleText1": "Si tienes ambos",
|
||||
"exampleText2": "y",
|
||||
"exampleText3": "el FQN será",
|
||||
"exampleText4": "porque",
|
||||
"exampleText5": "tiene mayor prioridad.",
|
||||
"correlationDescription": "Los servicios con el mismo FQN están correlacionados, incluso si tienen diferentes",
|
||||
"correlationDescriptionField": "servicio",
|
||||
"correlationDescriptionEnd": "nombres.",
|
||||
"fqnPriorityTitle": "Dimensiones prioritarias de FQN",
|
||||
"fqnPriorityTooltip": "Define qué dimensiones se utilizan para derivar el FQN del servicio para la correlación. La primera dimensión con un valor gana. Reordene para adaptarlo a su infraestructura.",
|
||||
"fqnPriorityDescription": "Arrastra para reordenar o usa los botones de flecha. La primera dimensión coincidente determina el FQN del servicio.",
|
||||
"resetToDefaults": "Restablecer los valores predeterminados",
|
||||
"noDimensionsConfigured": "No hay dimensiones configuradas. Haga clic en «Restablecer valores predeterminados» o añada las dimensiones a continuación.",
|
||||
"addDimensionLabel": "Agregar dimensión a la lista de prioridades",
|
||||
"moveUp": "Subir (prioridad más alta)",
|
||||
"moveDown": "Bajar (prioridad más baja)",
|
||||
"removeFromList": "Eliminar de la lista de prioridades",
|
||||
"semanticFieldTitle": "Mapeos de campos semánticos",
|
||||
"semanticFieldTooltip": "Asigna diferentes nombres de campo a dimensiones semánticas. Por ejemplo, «deployment», «k8s.deployment.name» y «kube_deployment» se asignan todos a «k8s-deployment».",
|
||||
"semanticFieldDescription": "Configure cómo se asignan los nombres de campo de su telemetría a las dimensiones semánticas.",
|
||||
"saveConfiguration": "Guardar configuración",
|
||||
"configSavedSuccess": "La configuración de identidad del servicio se guardó correctamente",
|
||||
"configSaveFailed": "No se pudo guardar la configuración",
|
||||
"correlationSettingsSaved": "La configuración de correlación se guardó correctamente.",
|
||||
"logs": "Registros",
|
||||
"traces": "Huellas",
|
||||
"metrics": "Métricas",
|
||||
"retry": "Reintentar",
|
||||
"k8sDeployment": "Nombre de implementación de Kubernetes",
|
||||
"k8sStatefulSet": "Nombre de Kubernetes StatefulSet",
|
||||
"k8sDaemonSet": "Nombre del daemonSet de Kubernetes",
|
||||
"k8sJob": "Nombre del trabajo de Kubernetes",
|
||||
"k8sReplicaSet": "Nombre de Kubernetes ReplicaSet",
|
||||
"k8sNamespace": "Espacio de nombres de Kubernetes",
|
||||
"awsEcsTask": "Familia de tareas AWS ECS",
|
||||
"faasName": "Lambda, Cloud Functions, etc.",
|
||||
"gcpCloudRun": "Servicio Google Cloud Run",
|
||||
"azureCloudRole": "Nombre del rol de Azure Cloud",
|
||||
"processName": "Nombre del proceso de metal desnudo",
|
||||
"serviceDim": "Nombre del servicio OpenTelemetry",
|
||||
"host": "Nombre de host/nodo",
|
||||
"environment": "Entorno de despliegue"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Usuarios",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Dimensiones coincidentes",
|
||||
"metricsButton": "{count} Métricas",
|
||||
"metricsTooltip": "Seleccione los flujos métricos que desea mostrar",
|
||||
"selectMetrics": "Seleccione Metric Streams"
|
||||
"selectMetrics": "Seleccione Metric Streams",
|
||||
"correlatedLogs": "Registros correlacionados",
|
||||
"correlatedMetrics": "Métricas correlacionadas",
|
||||
"correlatedTraces": "Rastros correlacionados",
|
||||
"clickToLoadLogs": "Haga clic para cargar los registros correlacionados",
|
||||
"clickToLoadMetrics": "Haga clic para cargar métricas correlacionadas",
|
||||
"clickToLoadTraces": "Haga clic para cargar trazas correlacionadas",
|
||||
"loadingLogs": "Cargando registros...",
|
||||
"noLogsFound": "No se han encontrado registros correlacionados",
|
||||
"service": "Servicio: {service}",
|
||||
"tracesPlaceholder": "Los rastros se implementarán aquí",
|
||||
"correlatedTracesFor": "Rastros correlacionados para el servicio: {service}",
|
||||
"metricsSelector": "{seleccionado} de {total} métricas",
|
||||
"dimensions": "Filtros",
|
||||
"all": "Todas",
|
||||
"loadingTraces": "Cargando trazas correlacionadas...",
|
||||
"tracesError": "No se pudieron cargar las trazas",
|
||||
"directTraceMatch": "Direct Trace Match",
|
||||
"spans": "lapso (s)",
|
||||
"dimensionBasedCorrelation": "Correlación basada en dimensiones",
|
||||
"tracesFromService": "Rastros del servicio: {service}",
|
||||
"noTracesFound": "No se han encontrado rastros correlacionados",
|
||||
"noTracesDescription": "No se han encontrado rastros del servicio {service} en el intervalo de tiempo seleccionado"
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Erreur lors de l'enregistrement des paramètres du domaine",
|
||||
"noDomainMessage": "Aucun domaine n'a été configuré. Ajoutez des domaines ci-dessus pour restreindre l'accès.",
|
||||
"license": "Licence",
|
||||
"alertCorrelation": "Déduplication des alertes"
|
||||
"alertCorrelation": "Déduplication des alertes",
|
||||
"correlationSettings": "Paramètres de corrélation",
|
||||
"correlation": {
|
||||
"title": "Paramètres de corrélation",
|
||||
"subtitle": "Découvrez et gérez les corrélations entre les services entre les journaux, les traces et les métriques",
|
||||
"serviceIdentityTab": "Identité du service",
|
||||
"discoveredServicesTab": "Services découverts",
|
||||
"alertCorrelationTab": "Corrélation des alertes",
|
||||
"discoveredServicesTitle": "Services découverts",
|
||||
"discoveredServicesDescription": "Les services sont automatiquement découverts à partir de vos données de télémétrie et regroupés selon leur nom complet (FQN) à des fins de corrélation. Les services partageant le même FQN sont corrélés entre eux entre les journaux, les traces et les métriques.",
|
||||
"noServicesYet": "Aucun service n'a encore été découvert",
|
||||
"noServicesDescription": "Les services apparaîtront ici lorsque les données de télémétrie seront ingérées avec des attributs d'identification de service (tels que k8s-deployment, nom du service, etc.)",
|
||||
"fqns": "FQN",
|
||||
"services": "Des services",
|
||||
"fullyCorrelated": "Entièrement corrélé",
|
||||
"suggestions": "suggestions",
|
||||
"clickToViewSuggestions": "Cliquez pour voir les suggestions d'amélioration de la corrélation",
|
||||
"byFqn": "Par FQN",
|
||||
"byService": "Par Service",
|
||||
"byStream": "Par Stream",
|
||||
"searchFqnOrService": "Recherchez le FQN ou le nom du service...",
|
||||
"searchServiceName": "Nom du service de recherche...",
|
||||
"searchStreamName": "Nom du flux de recherche...",
|
||||
"allServices": "Tous les services",
|
||||
"missingTelemetry": "Télémétrie manquante",
|
||||
"fqn": "FAN",
|
||||
"correlationKey": "Clé de corrélation",
|
||||
"telemetryCoverage": "Couverture télémétrique",
|
||||
"serviceName": "Nom du service",
|
||||
"streams": "Ruisseaux",
|
||||
"fqnSource": "Source du FQN",
|
||||
"viewDimensions": "Afficher les dimensions",
|
||||
"viewServices": "Cliquez pour voir les services",
|
||||
"dimensionsFor": "Dimensions pour",
|
||||
"correlatedViaSharedDimensions": "service (s) corrélé (s) via des dimensions partagées",
|
||||
"correlationDimensions": "Dimensions de corrélation",
|
||||
"noSharedDimensions": "Aucune dimension partagée n'a été trouvée entre les services",
|
||||
"suggestionsDialogTitle": "Suggestions de",
|
||||
"suggestionsDialogSubtitle": "Services qui pourraient être corrélés mais ne disposent pas de types de télémétrie",
|
||||
"suggestionsInfoText": "Pour permettre une corrélation complète, assurez-vous que vos données de télémétrie incluent des attributs d'identification de service cohérents (tels que k8s-deployment, nom du service) dans les journaux, les traces et les métriques.",
|
||||
"addTracingSuggestion": "Ajoutez l'instrumentation de traçage OpenTelemetry pour permettre une corrélation complète",
|
||||
"addLogsSuggestion": "Configurer la collecte de journaux avec les attributs de service correspondants",
|
||||
"addMetricsSuggestion": "Ajouter une collection de métriques avec les dimensions de service correspondantes",
|
||||
"missing": "Disparus",
|
||||
"deployment": "Déploiement",
|
||||
"statefulSet": "Set StatefulSet",
|
||||
"daemonSet": "Daemon Set",
|
||||
"ecsTask": "Tâche ECS",
|
||||
"faas": "FAAs",
|
||||
"service": "Service",
|
||||
"serviceIdentityTitle": "Configuration de l'identité de service",
|
||||
"serviceIdentityDescription": "Configurez la manière dont les services sont identifiés et corrélés entre les journaux, les traces et les métriques. Le FQN (nom complet) du service est dérivé des dimensions de la charge de travail par ordre de priorité.",
|
||||
"howItWorksTitle": "Comment fonctionne Service Identity",
|
||||
"howItWorksDescription": "(Nom complet) est calculé à partir de vos dimensions de télémétrie pour identifier et corréler de manière unique les services entre différentes sources de données.",
|
||||
"priorityOrderLabel": "Ordre de priorité :",
|
||||
"priorityOrderDescription": "Les dimensions sont vérifiées dans l'ordre indiqué ci-dessous. La première dimension comportant une valeur devient le Service FQN.",
|
||||
"exampleLabel": "Exemple :",
|
||||
"exampleText1": "Si vous avez les deux",
|
||||
"exampleText2": "et",
|
||||
"exampleText3": "le FQN sera",
|
||||
"exampleText4": "parce que",
|
||||
"exampleText5": "a une priorité plus élevée.",
|
||||
"correlationDescription": "Les services ayant le même FQN sont corrélés, même s'ils ont des caractéristiques différentes",
|
||||
"correlationDescriptionField": "service",
|
||||
"correlationDescriptionEnd": "noms.",
|
||||
"fqnPriorityTitle": "Dimensions prioritaires de la FQN",
|
||||
"fqnPriorityTooltip": "Définit les dimensions utilisées pour calculer le FQN du service à des fins de corrélation. La première dimension avec une valeur l'emporte. Réorganisez en fonction de votre infrastructure.",
|
||||
"fqnPriorityDescription": "Faites glisser pour réorganiser ou utilisez les boutons fléchés. La première dimension correspondante détermine le Service FQN.",
|
||||
"resetToDefaults": "Rétablir les valeurs par défaut",
|
||||
"noDimensionsConfigured": "Aucune dimension n'est configurée. Cliquez sur « Rétablir les paramètres par défaut » ou ajoutez les dimensions ci-dessous.",
|
||||
"addDimensionLabel": "Ajouter une dimension à la liste des priorités",
|
||||
"moveUp": "Déplacer vers le haut (priorité plus élevée)",
|
||||
"moveDown": "Déplacer vers le bas (priorité inférieure)",
|
||||
"removeFromList": "Supprimer de la liste des priorités",
|
||||
"semanticFieldTitle": "Mappages de champs sémantiques",
|
||||
"semanticFieldTooltip": "Associe différents noms de champs à des dimensions sémantiques. Par exemple, « deployment », « k8s.deployment.name » et « kube_deployment » correspondent tous à « k8s-deployment ».",
|
||||
"semanticFieldDescription": "Configurez la façon dont les noms de champs de votre télémétrie sont mappés aux dimensions sémantiques.",
|
||||
"saveConfiguration": "Enregistrer la configuration",
|
||||
"configSavedSuccess": "Configuration de l'identité de service enregistrée avec succès",
|
||||
"configSaveFailed": "Impossible d'enregistrer les paramètres",
|
||||
"correlationSettingsSaved": "Les paramètres de corrélation ont été enregistrés avec succès.",
|
||||
"logs": "Journaux",
|
||||
"traces": "Des traces",
|
||||
"metrics": "Métriques",
|
||||
"retry": "Réessayez",
|
||||
"k8sDeployment": "Nom du déploiement Kubernetes",
|
||||
"k8sStatefulSet": "Nom Kubernetes StatefulSet",
|
||||
"k8sDaemonSet": "Nom du DaemonSet Kubernetes",
|
||||
"k8sJob": "Nom du job Kubernetes",
|
||||
"k8sReplicaSet": "Nom du Kubernetes ReplicaSet",
|
||||
"k8sNamespace": "Espace de noms Kubernetes",
|
||||
"awsEcsTask": "Famille de tâches AWS ECS",
|
||||
"faasName": "Lambda, fonctions cloud, etc.",
|
||||
"gcpCloudRun": "Service Google Cloud Run",
|
||||
"azureCloudRole": "Nom du rôle Azure Cloud",
|
||||
"processName": "Nom du procédé Bare Metal",
|
||||
"serviceDim": "OpenTelemetry service.name",
|
||||
"host": "Nom de l'hôte/du nœud",
|
||||
"environment": "Environnement de déploiement"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Les utilisateurs",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Dimensions assorties",
|
||||
"metricsButton": "{count} Métrique (s)",
|
||||
"metricsTooltip": "Sélectionnez les flux métriques à afficher",
|
||||
"selectMetrics": "Sélectionnez Metric Streams"
|
||||
"selectMetrics": "Sélectionnez Metric Streams",
|
||||
"correlatedLogs": "Journaux corrélés",
|
||||
"correlatedMetrics": "Métriques corrélées",
|
||||
"correlatedTraces": "Traces corrélées",
|
||||
"clickToLoadLogs": "Cliquez pour charger les journaux corrélés",
|
||||
"clickToLoadMetrics": "Cliquez pour charger les métriques corrélées",
|
||||
"clickToLoadTraces": "Cliquez pour charger les traces corrélées",
|
||||
"loadingLogs": "Chargement des journaux...",
|
||||
"noLogsFound": "Aucun journal corrélé n'a été trouvé",
|
||||
"service": "Service : {service}",
|
||||
"tracesPlaceholder": "Les traces seront implémentées ici",
|
||||
"correlatedTracesFor": "Traces corrélées pour le service : {service}",
|
||||
"metricsSelector": "{sélectionné} de {total} métrique (s)",
|
||||
"dimensions": "Filtres",
|
||||
"all": "Tous",
|
||||
"loadingTraces": "Chargement de traces corrélées...",
|
||||
"tracesError": "Impossible de charger les traces",
|
||||
"directTraceMatch": "Direct Trace Match",
|
||||
"spans": "envergure (s)",
|
||||
"dimensionBasedCorrelation": "Corrélation basée sur les dimensions",
|
||||
"tracesFromService": "Traces provenant du service : {service}",
|
||||
"noTracesFound": "Aucune trace corrélée n'a été trouvée",
|
||||
"noTracesDescription": "Aucune trace n'a été trouvée pour le service {service} dans la plage de temps sélectionnée"
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Errore durante il salvataggio delle impostazioni del dominio",
|
||||
"noDomainMessage": "Nessun dominio configurato. Aggiungi i domini sopra per limitare l'accesso.",
|
||||
"license": "Licenza",
|
||||
"alertCorrelation": "Deduplicazione degli avvisi"
|
||||
"alertCorrelation": "Deduplicazione degli avvisi",
|
||||
"correlationSettings": "Impostazioni di correlazione",
|
||||
"correlation": {
|
||||
"title": "Impostazioni di correlazione",
|
||||
"subtitle": "Scopri e gestisci le correlazioni dei servizi tra log, tracce e metriche",
|
||||
"serviceIdentityTab": "Identità del servizio",
|
||||
"discoveredServicesTab": "Servizi scoperti",
|
||||
"alertCorrelationTab": "Correlazione degli avvisi",
|
||||
"discoveredServicesTitle": "Servizi scoperti",
|
||||
"discoveredServicesDescription": "I servizi vengono rilevati automaticamente dai dati di telemetria e raggruppati in base al nome completo (FQN) per la correlazione. I servizi che condividono lo stesso FQN sono correlati tra loro tramite log, tracce e metriche.",
|
||||
"noServicesYet": "Nessun servizio ancora scoperto",
|
||||
"noServicesDescription": "I servizi verranno visualizzati qui quando i dati di telemetria vengono inseriti con gli attributi di identificazione del servizio (come k8s-deployment, nome del servizio, ecc.)",
|
||||
"fqns": "FQN",
|
||||
"services": "Servizi",
|
||||
"fullyCorrelated": "Completamente correlato",
|
||||
"suggestions": "suggerimenti",
|
||||
"clickToViewSuggestions": "Fai clic per visualizzare i suggerimenti per il miglioramento della correlazione",
|
||||
"byFqn": "Di FQN",
|
||||
"byService": "Per servizio",
|
||||
"byStream": "Di Stream",
|
||||
"searchFqnOrService": "Cerca FQN o nome del servizio...",
|
||||
"searchServiceName": "Nome del servizio di ricerca...",
|
||||
"searchStreamName": "Cerca nel nome dello stream...",
|
||||
"allServices": "Tutti i servizi",
|
||||
"missingTelemetry": "Telemetria mancante",
|
||||
"fqn": "VENTOLA",
|
||||
"correlationKey": "Chiave di correlazione",
|
||||
"telemetryCoverage": "Copertura telemetrica",
|
||||
"serviceName": "Nome del servizio",
|
||||
"streams": "Stream",
|
||||
"fqnSource": "Fonte FQN",
|
||||
"viewDimensions": "Visualizza le dimensioni",
|
||||
"viewServices": "Clicca per visualizzare i servizi",
|
||||
"dimensionsFor": "Dimensioni per",
|
||||
"correlatedViaSharedDimensions": "servizio/i correlati tramite dimensioni condivise",
|
||||
"correlationDimensions": "Dimensioni di correlazione",
|
||||
"noSharedDimensions": "Nessuna dimensione condivisa trovata tra i servizi",
|
||||
"suggestionsDialogTitle": "Suggerimenti di correlazione",
|
||||
"suggestionsDialogSubtitle": "Servizi che potrebbero essere correlati ma mancano i tipi di telemetria",
|
||||
"suggestionsInfoText": "Per consentire una correlazione completa, assicurati che i dati di telemetria includano attributi di identificazione del servizio coerenti (come k8s-deployment, nome del servizio) tra log, tracce e metriche.",
|
||||
"addTracingSuggestion": "Aggiungi la strumentazione di tracciamento OpenTelemetry per abilitare la correlazione completa",
|
||||
"addLogsSuggestion": "Configurare la raccolta di log con gli attributi di servizio corrispondenti",
|
||||
"addMetricsSuggestion": "Aggiungi una raccolta di metriche con le dimensioni del servizio corrispondenti",
|
||||
"missing": "Mancante",
|
||||
"deployment": "Distribuzione",
|
||||
"statefulSet": "Set statico",
|
||||
"daemonSet": "Set demone",
|
||||
"ecsTask": "Attività ECS",
|
||||
"faas": "Domande frequenti",
|
||||
"service": "Servizio",
|
||||
"serviceIdentityTitle": "Configurazione dell'identità del servizio",
|
||||
"serviceIdentityDescription": "Configura il modo in cui i servizi vengono identificati e correlati tra log, tracce e metriche. Il Service FQN (Fully Qualified Name) è derivato dalle dimensioni del carico di lavoro in ordine di priorità.",
|
||||
"howItWorksTitle": "Come funziona Service Identity",
|
||||
"howItWorksDescription": "(Fully Qualified Name) viene calcolato a partire dalle dimensioni di telemetria per identificare e correlare in modo univoco i servizi tra diverse fonti di dati.",
|
||||
"priorityOrderLabel": "Ordine prioritario:",
|
||||
"priorityOrderDescription": "Le dimensioni vengono controllate nell'ordine indicato di seguito. La prima dimensione con un valore diventa il Service FQN.",
|
||||
"exampleLabel": "Esempio:",
|
||||
"exampleText1": "Se li hai entrambi",
|
||||
"exampleText2": "e",
|
||||
"exampleText3": "il FQN sarà",
|
||||
"exampleText4": "perché",
|
||||
"exampleText5": "ha una priorità più alta.",
|
||||
"correlationDescription": "I servizi con lo stesso FQN sono correlati, anche se sono diversi",
|
||||
"correlationDescriptionField": "servizio",
|
||||
"correlationDescriptionEnd": "nomi.",
|
||||
"fqnPriorityTitle": "Dimensioni prioritarie FQN",
|
||||
"fqnPriorityTooltip": "Definisce quali dimensioni vengono utilizzate per derivare il Service FQN per la correlazione. La prima dimensione con un valore vince. Riordina in base alla tua infrastruttura.",
|
||||
"fqnPriorityDescription": "Trascina per riordinare o usa i pulsanti freccia. La prima dimensione corrispondente determina Service FQN.",
|
||||
"resetToDefaults": "Ripristina i valori predefiniti",
|
||||
"noDimensionsConfigured": "Nessuna dimensione configurata. Fai clic su «Ripristina i valori predefiniti» o aggiungi le dimensioni di seguito.",
|
||||
"addDimensionLabel": "Aggiungi dimensione all'elenco delle priorità",
|
||||
"moveUp": "Sposta verso l'alto (priorità più alta)",
|
||||
"moveDown": "Sposta verso il basso (priorità inferiore)",
|
||||
"removeFromList": "Rimuovi dall'elenco delle priorità",
|
||||
"semanticFieldTitle": "Mappature semantiche dei campi",
|
||||
"semanticFieldTooltip": "Associa diversi nomi di campo a dimensioni semantiche. Ad esempio, «deployment», «k8s.deployment.name» e «kube_deployment» sono tutti mappati a «k8s-deployment».",
|
||||
"semanticFieldDescription": "Configura in che modo i nomi dei campi della tua telemetria vengono mappati alle dimensioni semantiche.",
|
||||
"saveConfiguration": "Salva configurazione",
|
||||
"configSavedSuccess": "Configurazione dell'identità del servizio salvata correttamente",
|
||||
"configSaveFailed": "Impossibile salvare le impostazioni",
|
||||
"correlationSettingsSaved": "Le impostazioni di correlazione sono state salvate correttamente.",
|
||||
"logs": "Registri",
|
||||
"traces": "Tracce",
|
||||
"metrics": "Metriche",
|
||||
"retry": "Riprova",
|
||||
"k8sDeployment": "Nome della distribuzione Kubernetes",
|
||||
"k8sStatefulSet": "Nome Kubernetes StatefulSet",
|
||||
"k8sDaemonSet": "Nome del demone Kubernetes",
|
||||
"k8sJob": "Nome del job Kubernetes",
|
||||
"k8sReplicaSet": "Nome Kubernetes ReplicaSet",
|
||||
"k8sNamespace": "Namespace Kubernetes",
|
||||
"awsEcsTask": "Famiglia AWS ECS Task",
|
||||
"faasName": "Lambda, Cloud Functions, ecc.",
|
||||
"gcpCloudRun": "Servizio Google Cloud Run",
|
||||
"azureCloudRole": "Nome del ruolo nel cloud di Azure",
|
||||
"processName": "Nome del processo in metallo nudo",
|
||||
"serviceDim": "nome del servizio di telemetria OpenTelemetry",
|
||||
"host": "Nome host/nodo",
|
||||
"environment": "Ambiente di distribuzione"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Utenti",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Dimensioni abbinate",
|
||||
"metricsButton": "{count} Metriche",
|
||||
"metricsTooltip": "Seleziona gli stream metrici da visualizzare",
|
||||
"selectMetrics": "Seleziona Metric Streams"
|
||||
"selectMetrics": "Seleziona Metric Streams",
|
||||
"correlatedLogs": "Log correlati",
|
||||
"correlatedMetrics": "Metriche correlate",
|
||||
"correlatedTraces": "Tracce correlate",
|
||||
"clickToLoadLogs": "Fai clic per caricare i log correlati",
|
||||
"clickToLoadMetrics": "Fai clic per caricare le metriche correlate",
|
||||
"clickToLoadTraces": "Fai clic per caricare le tracce correlate",
|
||||
"loadingLogs": "Caricamento dei log...",
|
||||
"noLogsFound": "Nessun registro correlato trovato",
|
||||
"service": "Servizio: {service}",
|
||||
"tracesPlaceholder": "Le tracce verranno implementate qui",
|
||||
"correlatedTracesFor": "Tracce correlate per il servizio: {service}",
|
||||
"metricsSelector": "{selezionato} di {totale} metriche",
|
||||
"dimensions": "Filtri",
|
||||
"all": "Tutti",
|
||||
"loadingTraces": "Caricamento di tracce correlate...",
|
||||
"tracesError": "Caricamento delle tracce non riuscito",
|
||||
"directTraceMatch": "Direct Trace Match",
|
||||
"spans": "campata (s)",
|
||||
"dimensionBasedCorrelation": "Correlazione basata sulle dimensioni",
|
||||
"tracesFromService": "Tracce dal servizio: {service}",
|
||||
"noTracesFound": "Nessuna traccia correlata trovata",
|
||||
"noTracesDescription": "Nessuna traccia trovata per service {service} nell'intervallo di tempo selezionato"
|
||||
}
|
||||
}
|
||||
@@ -1455,7 +1455,106 @@
|
||||
"errorSavingDomainSettings": "ドメイン設定を保存中にエラーが発生しました",
|
||||
"noDomainMessage": "ドメインが設定されていません。上記にドメインを追加してアクセスを制限してください。",
|
||||
"license": "ライセンス",
|
||||
"alertCorrelation": "アラート重複排除"
|
||||
"alertCorrelation": "アラート重複排除",
|
||||
"correlationSettings": "相関設定",
|
||||
"correlation": {
|
||||
"title": "相関設定",
|
||||
"subtitle": "ログ、トレース、メトリクス間のサービス相関の検出と管理",
|
||||
"serviceIdentityTab": "サービスアイデンティティ",
|
||||
"discoveredServicesTab": "検出されたサービス",
|
||||
"alertCorrelationTab": "アラート相関",
|
||||
"discoveredServicesTitle": "検出されたサービス",
|
||||
"discoveredServicesDescription": "サービスはテレメトリデータから自動的に検出され、完全修飾名 (FQN) 別にグループ化されて関連付けられます。同じ FQN を共有するサービスは、ログ、トレース、メトリックス間で相互に関連付けられます。",
|
||||
"noServicesYet": "まだサービスが見つかりませんでした",
|
||||
"noServicesDescription": "テレメトリデータがサービス識別属性 (k8s-deployment、サービス名など) とともに取り込まれると、サービスがここに表示されます。",
|
||||
"fqns": "FQN",
|
||||
"services": "サービス",
|
||||
"fullyCorrelated": "完全相関関係",
|
||||
"suggestions": "提案",
|
||||
"clickToViewSuggestions": "クリックして相関関係の改善提案を表示",
|
||||
"byFqn": "レビュー投稿者:FQN",
|
||||
"byService": "サービス別",
|
||||
"byStream": "ストリーム別",
|
||||
"searchFqnOrService": "FQN またはサービス名を検索...",
|
||||
"searchServiceName": "サービス名を検索中...",
|
||||
"searchStreamName": "ストリーム名を検索...",
|
||||
"allServices": "すべてのサービス",
|
||||
"missingTelemetry": "テレメトリが見つからない",
|
||||
"fqn": "ファン",
|
||||
"correlationKey": "相関キー",
|
||||
"telemetryCoverage": "テレメトリーカバレッジ",
|
||||
"serviceName": "[サービス名]",
|
||||
"streams": "ストリーム",
|
||||
"fqnSource": "FQN ソース",
|
||||
"viewDimensions": "寸法を表示",
|
||||
"viewServices": "クリックしてサービスを表示",
|
||||
"dimensionsFor": "の寸法",
|
||||
"correlatedViaSharedDimensions": "共有ディメンションを介して関連付けられたサービス",
|
||||
"correlationDimensions": "相関ディメンション",
|
||||
"noSharedDimensions": "サービス間で共有ディメンションが見つかりません",
|
||||
"suggestionsDialogTitle": "相関関係の提案",
|
||||
"suggestionsDialogSubtitle": "相関関係はあるがテレメトリタイプが欠落しているサービス",
|
||||
"suggestionsInfoText": "完全な相関関係を実現するには、テレメトリデータにログ、トレース、メトリックス全体で一貫したサービス識別属性 (k8s-deployment、サービス名など) が含まれていることを確認してください。",
|
||||
"addTracingSuggestion": "OpenTelemetry トレースインストゥルメンテーションを追加して完全な相関関係を実現",
|
||||
"addLogsSuggestion": "一致するサービス属性を使用してログ収集を設定",
|
||||
"addMetricsSuggestion": "一致するサービスディメンションを含む指標コレクションを追加",
|
||||
"missing": "行方不明です",
|
||||
"deployment": "デプロイ",
|
||||
"statefulSet": "ステートフルセット",
|
||||
"daemonSet": "デーモンセット",
|
||||
"ecsTask": "ECS タスク",
|
||||
"faas": "FAA",
|
||||
"service": "サービス",
|
||||
"serviceIdentityTitle": "サービス ID 設定",
|
||||
"serviceIdentityDescription": "ログ、トレース、メトリクス全体でサービスを識別して関連付ける方法を設定します。サービス FQN (完全修飾名) は、ワークロードディメンションから優先順位順に導き出されます。",
|
||||
"howItWorksTitle": "サービス ID の仕組み",
|
||||
"howItWorksDescription": "(完全修飾名)はテレメトリディメンションから計算され、さまざまなデータソースにわたってサービスを一意に識別して関連付けることができます。",
|
||||
"priorityOrderLabel": "優先順位:",
|
||||
"priorityOrderDescription": "寸法は以下の順序で確認されます。値のある最初のディメンションがサービス FQN になります。",
|
||||
"exampleLabel": "例:",
|
||||
"exampleText1": "両方ある場合",
|
||||
"exampleText2": "そして",
|
||||
"exampleText3": "FQNは",
|
||||
"exampleText4": "なぜなら",
|
||||
"exampleText5": "優先度が高い。",
|
||||
"correlationDescription": "同じFQNのサービスは、異なっていても相関関係にあります",
|
||||
"correlationDescriptionField": "サービス",
|
||||
"correlationDescriptionEnd": "名前。",
|
||||
"fqnPriorityTitle": "FQN優先ディメンション",
|
||||
"fqnPriorityTooltip": "相関のためのサービスFQNの導出に使用するディメンションを定義します。値が指定されている最初のディメンションが優先されます。インフラストラクチャーに合わせて順序を変更してください。",
|
||||
"fqnPriorityDescription": "ドラッグして順序を変更するか、矢印ボタンを使用します。最初に一致するディメンションがサービス FQN を決定します。",
|
||||
"resetToDefaults": "[既定値にリセット]",
|
||||
"noDimensionsConfigured": "ディメンションは設定されていません。[デフォルトにリセット] をクリックするか、下にディメンションを追加してください。",
|
||||
"addDimensionLabel": "ディメンションを優先リストに追加",
|
||||
"moveUp": "上へ移動 (優先度が高い)",
|
||||
"moveDown": "下に移動 (優先度を下げる)",
|
||||
"removeFromList": "優先リストから削除",
|
||||
"semanticFieldTitle": "セマンティックフィールドマッピング",
|
||||
"semanticFieldTooltip": "さまざまなフィールド名をセマンティックディメンションにマップします。たとえば、「デプロイメント」、「k8s.deployment.name」、「kube_deployment」はすべて「k8s-deployment」にマップされます。",
|
||||
"semanticFieldDescription": "テレメトリのフィールド名をセマンティックディメンションにマッピングする方法を設定します。",
|
||||
"saveConfiguration": "[設定を保存]",
|
||||
"configSavedSuccess": "サービス ID 設定は正常に保存されました",
|
||||
"configSaveFailed": "設定を保存できませんでした",
|
||||
"correlationSettingsSaved": "相関設定は正常に保存されました。",
|
||||
"logs": "ログ",
|
||||
"traces": "トレース",
|
||||
"metrics": "指標",
|
||||
"retry": "リトライ",
|
||||
"k8sDeployment": "Kubernetes デプロイメント名",
|
||||
"k8sStatefulSet": "Kubernetes ステートフルセット名",
|
||||
"k8sDaemonSet": "Kubernetes デーモンセット名",
|
||||
"k8sJob": "クベルネテスジョブ名",
|
||||
"k8sReplicaSet": "Kubernetes レプリカセット名",
|
||||
"k8sNamespace": "クベルネテス名前空間",
|
||||
"awsEcsTask": "AWS ECS タスクファミリー",
|
||||
"faasName": "ラムダ、クラウド関数など",
|
||||
"gcpCloudRun": "Google クラウドランサービス",
|
||||
"azureCloudRole": "Azure クラウドロール名",
|
||||
"processName": "ベアメタルプロセス名",
|
||||
"serviceDim": "テレメトリサービス.name を開きます",
|
||||
"host": "ホスト/ノード名",
|
||||
"environment": "デプロイ環境"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "ユーザー",
|
||||
@@ -1833,6 +1932,28 @@
|
||||
"matchedDimensions": "寸法を合わせたもの",
|
||||
"metricsButton": "{カウント} メトリックス",
|
||||
"metricsTooltip": "表示するメトリックスストリームを選択",
|
||||
"selectMetrics": "メトリックスストリームの選択"
|
||||
"selectMetrics": "メトリックスストリームの選択",
|
||||
"correlatedLogs": "相関ログ",
|
||||
"correlatedMetrics": "相関指標",
|
||||
"correlatedTraces": "相関トレース",
|
||||
"clickToLoadLogs": "クリックして相関ログを読み込む",
|
||||
"clickToLoadMetrics": "クリックして相関指標を読み込む",
|
||||
"clickToLoadTraces": "クリックして相関トレースを読み込む",
|
||||
"loadingLogs": "ログをロード中...",
|
||||
"noLogsFound": "相関ログが見つかりません",
|
||||
"service": "サービス:{サービス}",
|
||||
"tracesPlaceholder": "トレースはここに実装されます",
|
||||
"correlatedTracesFor": "サービスの相関トレース:{service}",
|
||||
"metricsSelector": "{合計} 個のメトリックの {選択済み}",
|
||||
"dimensions": "フィルター",
|
||||
"all": "[すべて]",
|
||||
"loadingTraces": "相関トレースをロード中...",
|
||||
"tracesError": "トレースを読み込めませんでした",
|
||||
"directTraceMatch": "ダイレクト・トレース・マッチ",
|
||||
"spans": "スパン (s)",
|
||||
"dimensionBasedCorrelation": "ディメンションベースの相関",
|
||||
"tracesFromService": "サービスからのトレース:{service}",
|
||||
"noTracesFound": "相関トレースは見つかりませんでした",
|
||||
"noTracesDescription": "選択した時間範囲のサービス {service} のトレースが見つかりません"
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "도메인 설정을 저장하는 중 오류 발생",
|
||||
"noDomainMessage": "도메인이 구성되지 않았습니다.위에 도메인을 추가하여 액세스를 제한하세요.",
|
||||
"license": "라이선스",
|
||||
"alertCorrelation": "경고 중복 제거"
|
||||
"alertCorrelation": "경고 중복 제거",
|
||||
"correlationSettings": "상관관계 설정",
|
||||
"correlation": {
|
||||
"title": "상관관계 설정",
|
||||
"subtitle": "로그, 추적, 지표 전반의 서비스 상관관계를 발견하고 관리합니다.",
|
||||
"serviceIdentityTab": "서비스 아이덴티티",
|
||||
"discoveredServicesTab": "검색된 서비스",
|
||||
"alertCorrelationTab": "경고 상관관계",
|
||||
"discoveredServicesTitle": "검색된 서비스",
|
||||
"discoveredServicesDescription": "서비스는 텔레메트리 데이터에서 자동으로 검색되며 상관 관계를 파악할 수 있도록 FQN (FQualified Name) 별로 그룹화됩니다.동일한 FQN을 공유하는 서비스는 로그, 추적 및 지표 전반에서 서로 상관관계가 있습니다.",
|
||||
"noServicesYet": "아직 검색된 서비스가 없습니다.",
|
||||
"noServicesDescription": "원격 측정 데이터가 서비스 식별 속성 (예: k8s-배포, 서비스 이름 등) 과 함께 수집되면 서비스가 여기에 표시됩니다.",
|
||||
"fqns": "FQN",
|
||||
"services": "서비스",
|
||||
"fullyCorrelated": "완전한 상관관계",
|
||||
"suggestions": "제안 사항",
|
||||
"clickToViewSuggestions": "상관관계 개선 제안을 보려면 클릭하십시오.",
|
||||
"byFqn": "글쓴이: FQN",
|
||||
"byService": "서비스별",
|
||||
"byStream": "스트림별",
|
||||
"searchFqnOrService": "FQN 또는 서비스 이름 검색...",
|
||||
"searchServiceName": "서비스 이름 검색...",
|
||||
"searchStreamName": "스트림 이름 검색...",
|
||||
"allServices": "전체 서비스",
|
||||
"missingTelemetry": "사라진 텔레메트리",
|
||||
"fqn": "팬",
|
||||
"correlationKey": "상관관계 키",
|
||||
"telemetryCoverage": "텔레메트리 커버리지",
|
||||
"serviceName": "서비스 이름",
|
||||
"streams": "스트림",
|
||||
"fqnSource": "FQN 소스",
|
||||
"viewDimensions": "치수 보기",
|
||||
"viewServices": "서비스를 보려면 클릭",
|
||||
"dimensionsFor": "에 대한 치수",
|
||||
"correlatedViaSharedDimensions": "공유 차원을 통해 상관관계가 있는 서비스",
|
||||
"correlationDimensions": "상관관계 차원",
|
||||
"noSharedDimensions": "서비스 전체에서 공유된 차원을 찾을 수 없습니다.",
|
||||
"suggestionsDialogTitle": "상관관계 제안",
|
||||
"suggestionsDialogSubtitle": "상관관계가 있을 수 있지만 텔레메트리 유형이 누락된 서비스",
|
||||
"suggestionsInfoText": "완전한 상관 관계를 파악하려면 원격 측정 데이터에 로그, 추적 및 메트릭에 걸쳐 일관된 서비스 식별 속성 (예: k8s-Deployment, 서비스 이름) 이 포함되도록 해야 합니다.",
|
||||
"addTracingSuggestion": "OpenTelemetry 추적 계측을 추가하여 완전한 상관 관계 파악 가능",
|
||||
"addLogsSuggestion": "일치하는 서비스 속성으로 로그 수집 구성",
|
||||
"addMetricsSuggestion": "일치하는 서비스 차원이 포함된 지표 컬렉션 추가",
|
||||
"missing": "실종",
|
||||
"deployment": "배포",
|
||||
"statefulSet": "스테이트풀셋",
|
||||
"daemonSet": "데몬셋",
|
||||
"ecsTask": "ECS 태스크",
|
||||
"faas": "자주 묻는 질문",
|
||||
"service": "서비스",
|
||||
"serviceIdentityTitle": "서비스 ID 구성",
|
||||
"serviceIdentityDescription": "로그, 추적 및 지표 전반에서 서비스를 식별하고 상호 연관시키는 방법을 구성합니다.서비스 FQN (정규화된 이름) 은 워크로드 차원에서 우선 순위에 따라 파생됩니다.",
|
||||
"howItWorksTitle": "서비스 아이덴티티 작동 방식",
|
||||
"howItWorksDescription": "(Fully Qualified Name) 은 다양한 데이터 소스에서 서비스를 고유하게 식별하고 상호 연관시키기 위해 원격 측정 차원에서 계산됩니다.",
|
||||
"priorityOrderLabel": "우선 순위:",
|
||||
"priorityOrderDescription": "치수는 아래 나열된 순서대로 검사됩니다.값이 있는 첫 번째 차원이 서비스 FQN이 됩니다.",
|
||||
"exampleLabel": "예시:",
|
||||
"exampleText1": "둘 다 있는 경우",
|
||||
"exampleText2": "과",
|
||||
"exampleText3": "FQN은 다음과 같습니다.",
|
||||
"exampleText4": "왜냐하면",
|
||||
"exampleText5": "우선 순위가 더 높습니다.",
|
||||
"correlationDescription": "FQN이 동일한 서비스는 서로 다르더라도 상관 관계가 있습니다.",
|
||||
"correlationDescriptionField": "서비스",
|
||||
"correlationDescriptionEnd": "이름.",
|
||||
"fqnPriorityTitle": "FQN 우선순위 치수",
|
||||
"fqnPriorityTooltip": "상관 관계를 위해 서비스 FQN을 도출하는 데 사용되는 차원을 정의합니다.값이 있는 첫 번째 차원이 우선합니다.인프라에 맞게 재정렬하세요.",
|
||||
"fqnPriorityDescription": "드래그하여 순서를 바꾸거나 화살표 버튼을 사용합니다.첫 번째로 일치하는 차원이 서비스 FQN을 결정합니다.",
|
||||
"resetToDefaults": "기본값으로 재설정",
|
||||
"noDimensionsConfigured": "치수가 구성되지 않았습니다.“기본값으로 재설정”을 클릭하거나 아래에 치수를 추가하십시오.",
|
||||
"addDimensionLabel": "우선순위 목록에 차원 추가",
|
||||
"moveUp": "위로 이동 (우선 순위가 높음)",
|
||||
"moveDown": "아래로 이동 (우선 순위가 낮음)",
|
||||
"removeFromList": "우선순위 목록에서 제거",
|
||||
"semanticFieldTitle": "시맨틱 필드 매핑",
|
||||
"semanticFieldTooltip": "다양한 필드 이름을 시맨틱 차원에 매핑합니다.예를 들어 “배포”, “k8s.배포.name”, “kube_deployment”는 모두 “k8s-배포”에 매핑됩니다.",
|
||||
"semanticFieldDescription": "텔레메트리의 필드 이름을 시맨틱 차원에 매핑하는 방법을 구성하세요.",
|
||||
"saveConfiguration": "구성 저장",
|
||||
"configSavedSuccess": "서비스 ID 구성이 성공적으로 저장되었습니다.",
|
||||
"configSaveFailed": "설정 저장 실패",
|
||||
"correlationSettingsSaved": "상호연관 설정이 성공적으로 저장되었습니다.",
|
||||
"logs": "로그",
|
||||
"traces": "트레이스",
|
||||
"metrics": "측정항목",
|
||||
"retry": "재시도",
|
||||
"k8sDeployment": "쿠버네티스 디플로이먼트 이름",
|
||||
"k8sStatefulSet": "쿠버네티스 스테이트풀셋 이름",
|
||||
"k8sDaemonSet": "쿠버네티스 데몬셋 이름",
|
||||
"k8sJob": "쿠버네티스 작업 이름",
|
||||
"k8sReplicaSet": "쿠버네티스 레플리카셋 이름",
|
||||
"k8sNamespace": "쿠버네티스 네임스페이스",
|
||||
"awsEcsTask": "AWS ECS 태스크 패밀리",
|
||||
"faasName": "람다, 클라우드 함수 등",
|
||||
"gcpCloudRun": "구글 클라우드 런 서비스",
|
||||
"azureCloudRole": "Azure 클라우드 역할 이름",
|
||||
"processName": "베어메탈 프로세스 이름",
|
||||
"serviceDim": "오픈 텔레메트리 서비스. 이름",
|
||||
"host": "호스트/노드 이름",
|
||||
"environment": "배포 환경"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "유저",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "일치하는 치수",
|
||||
"metricsButton": "{개수} 지표",
|
||||
"metricsTooltip": "표시할 지표 스트림 선택",
|
||||
"selectMetrics": "지표 스트림 선택"
|
||||
"selectMetrics": "지표 스트림 선택",
|
||||
"correlatedLogs": "상관관계가 있는 로그",
|
||||
"correlatedMetrics": "상관관계가 있는 지표",
|
||||
"correlatedTraces": "상관관계가 있는 트레이스",
|
||||
"clickToLoadLogs": "상호 관련된 로그를 로드하려면 클릭하십시오.",
|
||||
"clickToLoadMetrics": "상관관계가 있는 지표를 로드하려면 클릭하십시오.",
|
||||
"clickToLoadTraces": "상관관계가 있는 트레이스를 로드하려면 클릭하십시오.",
|
||||
"loadingLogs": "로그 로드 중...",
|
||||
"noLogsFound": "상관관계가 있는 로그를 찾을 수 없습니다.",
|
||||
"service": "서비스: {서비스}",
|
||||
"tracesPlaceholder": "여기에서 추적이 구현됩니다.",
|
||||
"correlatedTracesFor": "서비스에 대한 상관관계가 있는 트레이스: {service}",
|
||||
"metricsSelector": "{총} 개 지표 중 {선택된} 개",
|
||||
"dimensions": "필터",
|
||||
"all": "모두",
|
||||
"loadingTraces": "상관관계가 있는 트레이스 불러오는 중...",
|
||||
"tracesError": "트레이스를 로드하지 못했습니다.",
|
||||
"directTraceMatch": "다이렉트 트레이스 매치",
|
||||
"spans": "스팬 (들)",
|
||||
"dimensionBasedCorrelation": "차원 기반 상관관계",
|
||||
"tracesFromService": "서비스의 추적: {service}",
|
||||
"noTracesFound": "상관관계가 있는 흔적을 찾을 수 없습니다.",
|
||||
"noTracesDescription": "선택한 시간 범위에서 서비스 {service} 에 대한 추적을 찾을 수 없습니다."
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Fout bij opslaan van domeininstellingen",
|
||||
"noDomainMessage": "Geen domeinen geconfigureerd. Voeg hierboven domeinen toe om de toegang te beperken.",
|
||||
"license": "Licentie",
|
||||
"alertCorrelation": "Deduplicatie van waarschuwingen"
|
||||
"alertCorrelation": "Deduplicatie van waarschuwingen",
|
||||
"correlationSettings": "Correlatie-instellingen",
|
||||
"correlation": {
|
||||
"title": "Correlatie-instellingen",
|
||||
"subtitle": "Ontdek en beheer servicecorrelaties tussen logboeken, traceringen en statistieken",
|
||||
"serviceIdentityTab": "Identificatie van de service",
|
||||
"discoveredServicesTab": "Ontdekte services",
|
||||
"alertCorrelationTab": "Correlatie tussen waarschuwingen",
|
||||
"discoveredServicesTitle": "Ontdekte services",
|
||||
"discoveredServicesDescription": "Services worden automatisch ontdekt op basis van uw telemetriegegevens en gegroepeerd op basis van hun Fully Qualified Name (FQN) voor correlatie. Services die dezelfde FQN delen, worden aan elkaar gekoppeld in logboeken, traceringen en statistieken.",
|
||||
"noServicesYet": "Nog geen services ontdekt",
|
||||
"noServicesDescription": "Services worden hier weergegeven wanneer telemetriegegevens worden opgenomen met service-identificatiekenmerken (zoals k8s-deployment, servicenaam, enz.)",
|
||||
"fqns": "FQN's",
|
||||
"services": "Diensten",
|
||||
"fullyCorrelated": "Volledig gecorreleerd",
|
||||
"suggestions": "suggesties",
|
||||
"clickToViewSuggestions": "Klik om suggesties voor correlatieverbetering te bekijken",
|
||||
"byFqn": "Door FQN",
|
||||
"byService": "Per service",
|
||||
"byStream": "Door Stream",
|
||||
"searchFqnOrService": "Zoek in de FQN of de servicenaam...",
|
||||
"searchServiceName": "Naam van de zoekservice...",
|
||||
"searchStreamName": "Zoek de naam van de stream...",
|
||||
"allServices": "Alle diensten",
|
||||
"missingTelemetry": "Telemetrie ontbreekt",
|
||||
"fqn": "VENTILATOR",
|
||||
"correlationKey": "Correlatiesleutel",
|
||||
"telemetryCoverage": "Dekking van telemetrie",
|
||||
"serviceName": "Naam van de service",
|
||||
"streams": "Streams",
|
||||
"fqnSource": "FQN-bron",
|
||||
"viewDimensions": "Afmetingen bekijken",
|
||||
"viewServices": "Klik om de services te bekijken",
|
||||
"dimensionsFor": "Afmetingen voor",
|
||||
"correlatedViaSharedDimensions": "service (en) gecorreleerd via gedeelde dimensies",
|
||||
"correlationDimensions": "Correlatiedimensies",
|
||||
"noSharedDimensions": "Geen gedeelde dimensies gevonden in alle services",
|
||||
"suggestionsDialogTitle": "Voorstellen voor correlatie",
|
||||
"suggestionsDialogSubtitle": "Diensten die mogelijk gecorreleerd zijn, maar waarbij telemetrietypen ontbreken",
|
||||
"suggestionsInfoText": "Om volledige correlatie mogelijk te maken, moet u ervoor zorgen dat uw telemetriegegevens consistente service-identificatiekenmerken bevatten (zoals k8s-implementatie, servicenaam) in logboeken, traceringen en statistieken.",
|
||||
"addTracingSuggestion": "Voeg OpenTelemetry-traceringsinstrumenten toe om volledige correlatie mogelijk te maken",
|
||||
"addLogsSuggestion": "Configureer de logboekverzameling met overeenkomende servicekenmerken",
|
||||
"addMetricsSuggestion": "Voeg een verzameling van statistieken toe met bijpassende servicedimensies",
|
||||
"missing": "Ontbreekt",
|
||||
"deployment": "Implementatie",
|
||||
"statefulSet": "Stateful Set",
|
||||
"daemonSet": "Daemonet",
|
||||
"ecsTask": "ECS-taak",
|
||||
"faas": "Veelgestelde vragen",
|
||||
"service": "Service",
|
||||
"serviceIdentityTitle": "Configuratie van de service-identiteit",
|
||||
"serviceIdentityDescription": "Configureer hoe services worden geïdentificeerd en gecorreleerd in logboeken, traceringen en statistieken. De Service FQN (Fully Qualified Name) is afgeleid van de afmetingen van de werkbelasting in volgorde van prioriteit.",
|
||||
"howItWorksTitle": "Hoe Service Identity werkt",
|
||||
"howItWorksDescription": "(Fully Qualified Name) wordt berekend op basis van uw telemetriedimensies om services in verschillende gegevensbronnen op unieke wijze te identificeren en met elkaar te correleren.",
|
||||
"priorityOrderLabel": "Prioriteitsvolgorde:",
|
||||
"priorityOrderDescription": "De afmetingen worden gecontroleerd in de onderstaande volgorde. De eerste dimensie met een waarde wordt de Service FQN.",
|
||||
"exampleLabel": "Voorbeeld:",
|
||||
"exampleText1": "Als je beide hebt",
|
||||
"exampleText2": "en",
|
||||
"exampleText3": "de FQN zal zijn",
|
||||
"exampleText4": "omdat",
|
||||
"exampleText5": "heeft een hogere prioriteit.",
|
||||
"correlationDescription": "Services met dezelfde FQN zijn gecorreleerd, zelfs als ze verschillende",
|
||||
"correlationDescriptionField": "service",
|
||||
"correlationDescriptionEnd": "namen.",
|
||||
"fqnPriorityTitle": "Prioritaire dimensies van de FAQN",
|
||||
"fqnPriorityTooltip": "Definieert welke dimensies worden gebruikt om de Service FQN voor correlatie af te leiden. De eerste dimensie met een waarde wint. Herschik zodat deze past bij je infrastructuur.",
|
||||
"fqnPriorityDescription": "Sleep om de volgorde te wijzigen of gebruik de pijlknoppen. De eerste overeenkomende dimensie bepaalt de Service FQN.",
|
||||
"resetToDefaults": "Naar de standaardwaarden terugzetten",
|
||||
"noDimensionsConfigured": "Er zijn geen afmetingen geconfigureerd. Klik op „Reset to Defaults” of voeg hieronder dimensies toe.",
|
||||
"addDimensionLabel": "Dimensie toevoegen aan de prioriteitenlijst",
|
||||
"moveUp": "Naar boven gaan (hogere prioriteit)",
|
||||
"moveDown": "Omlaag gaan (lagere prioriteit)",
|
||||
"removeFromList": "Uit de prioriteitenlijst verwijderen",
|
||||
"semanticFieldTitle": "Mappings van semantische velden",
|
||||
"semanticFieldTooltip": "Hiermee worden verschillende veldnamen toegewezen aan semantische dimensies. „deployment”, „k8s.deployment.name” en „kube_deployment” worden bijvoorbeeld allemaal toegewezen aan „k8s-deployment”.",
|
||||
"semanticFieldDescription": "Configureer hoe veldnamen uit uw telemetrie worden toegewezen aan semantische dimensies.",
|
||||
"saveConfiguration": "Configuratie opslaan",
|
||||
"configSavedSuccess": "De configuratie van de service-identiteit is succesvol opgeslagen",
|
||||
"configSaveFailed": "Het opslaan van instellingen is mislukt",
|
||||
"correlationSettingsSaved": "Correlatie-instellingen zijn succesvol opgeslagen.",
|
||||
"logs": "Logboeken",
|
||||
"traces": "Sporen",
|
||||
"metrics": "Metrieken",
|
||||
"retry": "Probeer het opnieuw",
|
||||
"k8sDeployment": "Naam van de Kubernetes-implementatie",
|
||||
"k8sStatefulSet": "Kubernetes StatefulSet-naam",
|
||||
"k8sDaemonSet": "De naam van Kubernetes DaemonSet",
|
||||
"k8sJob": "Functienaam Kubernetes",
|
||||
"k8sReplicaSet": "Kubernetes ReplicaSet-naam",
|
||||
"k8sNamespace": "Kubernetes-naamruimte",
|
||||
"awsEcsTask": "AWS ECS Task-familie",
|
||||
"faasName": "Lambda, cloudfuncties, enz.",
|
||||
"gcpCloudRun": "Google Cloud Run-service",
|
||||
"azureCloudRole": "Naam van de rol van Azure Cloud",
|
||||
"processName": "Naam van het proces voor blank metaal",
|
||||
"serviceDim": "OpenTelemetry service.name",
|
||||
"host": "Naam van host/node",
|
||||
"environment": "Implementatieomgeving"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Gebruikers",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Op elkaar afgestemde afmetingen",
|
||||
"metricsButton": "{count} Metriek (en)",
|
||||
"metricsTooltip": "Selecteer metrische streams om weer te geven",
|
||||
"selectMetrics": "Selecteer Metric Streams"
|
||||
"selectMetrics": "Selecteer Metric Streams",
|
||||
"correlatedLogs": "Gecorreleerde logboeken",
|
||||
"correlatedMetrics": "Gecorreleerde statistieken",
|
||||
"correlatedTraces": "Gecorreleerde sporen",
|
||||
"clickToLoadLogs": "Klik om gecorreleerde logboeken te laden",
|
||||
"clickToLoadMetrics": "Klik om gecorreleerde statistieken te laden",
|
||||
"clickToLoadTraces": "Klik om gecorreleerde sporen te laden",
|
||||
"loadingLogs": "Logboeken worden geladen...",
|
||||
"noLogsFound": "Geen gecorreleerde logboeken gevonden",
|
||||
"service": "Dienst: {service}",
|
||||
"tracesPlaceholder": "Traces zullen hier worden geïmplementeerd",
|
||||
"correlatedTracesFor": "Gecorreleerde sporen voor service: {service}",
|
||||
"metricsSelector": "{geselecteerd} van {total} Metriek (en)",
|
||||
"dimensions": "Filters",
|
||||
"all": "Alles",
|
||||
"loadingTraces": "Gecorreleerde sporen worden geladen...",
|
||||
"tracesError": "Het laden van sporen is mislukt",
|
||||
"directTraceMatch": "Direct Trace-match",
|
||||
"spans": "spanwijdte (s)",
|
||||
"dimensionBasedCorrelation": "Correlatie op basis van dimensies",
|
||||
"tracesFromService": "Sporen van service: {service}",
|
||||
"noTracesFound": "Geen gecorreleerde sporen gevonden",
|
||||
"noTracesDescription": "Geen sporen gevonden voor service {service} in de geselecteerde periode"
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Erro ao salvar as configurações do domínio",
|
||||
"noDomainMessage": "Nenhum domínio configurado. Adicione domínios acima para restringir o acesso.",
|
||||
"license": "Licença",
|
||||
"alertCorrelation": "Desduplicação de alertas"
|
||||
"alertCorrelation": "Desduplicação de alertas",
|
||||
"correlationSettings": "Configurações de correlação",
|
||||
"correlation": {
|
||||
"title": "Configurações de correlação",
|
||||
"subtitle": "Descubra e gerencie correlações de serviços em registros, rastreamentos e métricas",
|
||||
"serviceIdentityTab": "Identidade do serviço",
|
||||
"discoveredServicesTab": "Serviços descobertos",
|
||||
"alertCorrelationTab": "Correlação de alertas",
|
||||
"discoveredServicesTitle": "Serviços descobertos",
|
||||
"discoveredServicesDescription": "Os serviços são descobertos automaticamente a partir de seus dados de telemetria e agrupados por seu nome totalmente qualificado (FQN) para correlação. Os serviços que compartilham o mesmo FQN são correlacionados entre si em registros, rastreamentos e métricas.",
|
||||
"noServicesYet": "Nenhum serviço descoberto ainda",
|
||||
"noServicesDescription": "Os serviços aparecerão aqui à medida que os dados de telemetria forem ingeridos com atributos de identificação do serviço (como k8s-deployment, nome do serviço etc.)",
|
||||
"fqns": "FQNs",
|
||||
"services": "Serviços",
|
||||
"fullyCorrelated": "Totalmente correlacionado",
|
||||
"suggestions": "sugestões",
|
||||
"clickToViewSuggestions": "Clique para ver sugestões de melhoria de correlação",
|
||||
"byFqn": "Por FQN",
|
||||
"byService": "Por serviço",
|
||||
"byStream": "Por stream",
|
||||
"searchFqnOrService": "Pesquisar FQN ou nome do serviço...",
|
||||
"searchServiceName": "Pesquisar nome do serviço...",
|
||||
"searchStreamName": "Nome do stream de pesquisa...",
|
||||
"allServices": "Todos os serviços",
|
||||
"missingTelemetry": "Telemetria ausente",
|
||||
"fqn": "VENTILADOR",
|
||||
"correlationKey": "Chave de correlação",
|
||||
"telemetryCoverage": "Cobertura de telemetria",
|
||||
"serviceName": "Nome do serviço",
|
||||
"streams": "Fluxos",
|
||||
"fqnSource": "Fonte FQN",
|
||||
"viewDimensions": "Exibir dimensões",
|
||||
"viewServices": "Clique para ver os serviços",
|
||||
"dimensionsFor": "Dimensões para",
|
||||
"correlatedViaSharedDimensions": "serviços correlacionados por meio de dimensões compartilhadas",
|
||||
"correlationDimensions": "Dimensões de correlação",
|
||||
"noSharedDimensions": "Nenhuma dimensão compartilhada encontrada entre os serviços",
|
||||
"suggestionsDialogTitle": "Sugestões de relação",
|
||||
"suggestionsDialogSubtitle": "Serviços que poderiam estar correlacionados, mas não têm tipos de telemetria",
|
||||
"suggestionsInfoText": "Para permitir a correlação total, garanta que seus dados de telemetria incluam atributos de identificação de serviço consistentes (como k8s-deployment, nome do serviço) em registros, rastreamentos e métricas.",
|
||||
"addTracingSuggestion": "Adicione instrumentação de rastreamento OpenTelemetry para permitir a correlação total",
|
||||
"addLogsSuggestion": "Configurar a coleta de registros com atributos de serviço correspondentes",
|
||||
"addMetricsSuggestion": "Adicione uma coleção de métricas com dimensões de serviço correspondentes",
|
||||
"missing": "Desaparecido",
|
||||
"deployment": "Implantação",
|
||||
"statefulSet": "StatefulSet",
|
||||
"daemonSet": "DaemonSet",
|
||||
"ecsTask": "Tarefa do ECS",
|
||||
"faas": "Perguntas frequentes",
|
||||
"service": "Serviço",
|
||||
"serviceIdentityTitle": "Configuração de identidade de serviço",
|
||||
"serviceIdentityDescription": "Configure como os serviços são identificados e correlacionados em registros, rastreamentos e métricas. O FQN do serviço (nome totalmente qualificado) é derivado das dimensões da carga de trabalho em ordem de prioridade.",
|
||||
"howItWorksTitle": "Como funciona a identidade de serviço",
|
||||
"howItWorksDescription": "(Nome totalmente qualificado) é calculado a partir de suas dimensões de telemetria para identificar e correlacionar serviços de forma exclusiva em diferentes fontes de dados.",
|
||||
"priorityOrderLabel": "Ordem prioritária:",
|
||||
"priorityOrderDescription": "As dimensões são verificadas na ordem listada abaixo. A primeira dimensão com um valor se torna o Service FQN.",
|
||||
"exampleLabel": "Exemplo:",
|
||||
"exampleText1": "Se você tiver os dois",
|
||||
"exampleText2": "e",
|
||||
"exampleText3": "o FQN será",
|
||||
"exampleText4": "porque",
|
||||
"exampleText5": "tem maior prioridade.",
|
||||
"correlationDescription": "Os serviços com o mesmo FQN são correlacionados, mesmo que sejam diferentes",
|
||||
"correlationDescriptionField": "manutenção",
|
||||
"correlationDescriptionEnd": "nomes.",
|
||||
"fqnPriorityTitle": "Dimensões prioritárias do FQN",
|
||||
"fqnPriorityTooltip": "Define quais dimensões são usadas para derivar o FQN do serviço para correlação. A primeira dimensão com um valor vence. Reordene para combinar com sua infraestrutura.",
|
||||
"fqnPriorityDescription": "Arraste para reordenar ou use os botões de seta. A primeira dimensão correspondente determina o FQN do serviço.",
|
||||
"resetToDefaults": "Redefinir para os padrões",
|
||||
"noDimensionsConfigured": "Nenhuma dimensão configurada. Clique em “Redefinir para os padrões” ou adicione as dimensões abaixo.",
|
||||
"addDimensionLabel": "Adicionar dimensão à lista de prioridades",
|
||||
"moveUp": "Subir (prioridade mais alta)",
|
||||
"moveDown": "Mover para baixo (prioridade mais baixa)",
|
||||
"removeFromList": "Remover da lista de prioridades",
|
||||
"semanticFieldTitle": "Mapeamentos de campos semânticos",
|
||||
"semanticFieldTooltip": "Mapeia nomes de campos diferentes para dimensões semânticas. Por exemplo, “deployment”, “k8s.deployment.name” e “kube_deployment” são todos mapeados para “k8s-deployment”.",
|
||||
"semanticFieldDescription": "Configure como os nomes de campo da sua telemetria são mapeados para dimensões semânticas.",
|
||||
"saveConfiguration": "Salvar configuração",
|
||||
"configSavedSuccess": "Configuração de identidade de serviço salva com sucesso",
|
||||
"configSaveFailed": "Falha ao salvar as configurações",
|
||||
"correlationSettingsSaved": "Configurações de correlação salvas com sucesso.",
|
||||
"logs": "Registros",
|
||||
"traces": "Traços",
|
||||
"metrics": "Métricas",
|
||||
"retry": "Tente novamente",
|
||||
"k8sDeployment": "Nome de implantação do Kubernetes",
|
||||
"k8sStatefulSet": "Nome do Kubernetes StatefulSet",
|
||||
"k8sDaemonSet": "Nome do DaemonSet do Kubernetes",
|
||||
"k8sJob": "Nome do trabalho do Kubernetes",
|
||||
"k8sReplicaSet": "Nome do Kubernetes ReplicaSet",
|
||||
"k8sNamespace": "Namespace Kubernetes",
|
||||
"awsEcsTask": "Família de tarefas do AWS ECS",
|
||||
"faasName": "Lambda, funções de nuvem, etc.",
|
||||
"gcpCloudRun": "Serviço Google Cloud Run",
|
||||
"azureCloudRole": "Nome da função do Azure Cloud",
|
||||
"processName": "Nome do processo de metal puro",
|
||||
"serviceDim": "OpenTelemetry service.name",
|
||||
"host": "Nome do host/node",
|
||||
"environment": "Ambiente de implantação"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Usuários",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Dimensões combinadas",
|
||||
"metricsButton": "{count} Métrica (s)",
|
||||
"metricsTooltip": "Selecione fluxos métricos a serem exibidos",
|
||||
"selectMetrics": "Selecione fluxos métricos"
|
||||
"selectMetrics": "Selecione fluxos métricos",
|
||||
"correlatedLogs": "Registros correlacionados",
|
||||
"correlatedMetrics": "Métricas correlacionadas",
|
||||
"correlatedTraces": "Traços correlacionados",
|
||||
"clickToLoadLogs": "Clique para carregar registros correlacionados",
|
||||
"clickToLoadMetrics": "Clique para carregar métricas correlacionadas",
|
||||
"clickToLoadTraces": "Clique para carregar traços correlacionados",
|
||||
"loadingLogs": "Carregando registros...",
|
||||
"noLogsFound": "Nenhum registro correlacionado foi encontrado",
|
||||
"service": "Serviço: {service}",
|
||||
"tracesPlaceholder": "Os rastreamentos serão implementados aqui",
|
||||
"correlatedTracesFor": "Rastreamentos correlacionados para o serviço: {service}",
|
||||
"metricsSelector": "{selecionado} de {total} métrica (s)",
|
||||
"dimensions": "Filtros",
|
||||
"all": "Todos",
|
||||
"loadingTraces": "Carregando traços correlacionados...",
|
||||
"tracesError": "Falha ao carregar rastreamentos",
|
||||
"directTraceMatch": "Direct Trace Match",
|
||||
"spans": "extensão (s)",
|
||||
"dimensionBasedCorrelation": "Correlação baseada em dimensões",
|
||||
"tracesFromService": "Traços do serviço: {service}",
|
||||
"noTracesFound": "Nenhum traço correlacionado encontrado",
|
||||
"noTracesDescription": "Nenhum rastreamento encontrado para o serviço {service} no intervalo de tempo selecionado"
|
||||
}
|
||||
}
|
||||
@@ -1441,7 +1441,106 @@
|
||||
"errorSavingDomainSettings": "Etki alanı ayarlarını kaydederken hata oluştu",
|
||||
"noDomainMessage": "Yapılandırılmış etki alanı yok. Erişimi kısıtlamak için yukarıya alan adlarını ekleyin.",
|
||||
"license": "Lisans",
|
||||
"alertCorrelation": "Uyarı Tekilleştirme"
|
||||
"alertCorrelation": "Uyarı Tekilleştirme",
|
||||
"correlationSettings": "Korelasyon Ayarları",
|
||||
"correlation": {
|
||||
"title": "Korelasyon Ayarları",
|
||||
"subtitle": "Günlükler, izler ve ölçümler arasındaki hizmet korelasyonlarını keşfedin ve yönetin",
|
||||
"serviceIdentityTab": "Hizmet Kimliği",
|
||||
"discoveredServicesTab": "Keşfedilen Hizmetler",
|
||||
"alertCorrelationTab": "Uyarı Korelasyonu",
|
||||
"discoveredServicesTitle": "Keşfedilen Hizmetler",
|
||||
"discoveredServicesDescription": "Hizmetler telemetri verilerinizden otomatik olarak keşfedilir ve korelasyon için Tam Nitelikli Adlarına (FQN) göre gruplandırılır. Aynı FQN'yi paylaşan hizmetler günlükler, izler ve metrikler arasında birbiriyle ilişkilendirilir.",
|
||||
"noServicesYet": "Henüz hiçbir hizmet keşfedilmedi",
|
||||
"noServicesDescription": "Hizmetler, telemetri verileri hizmet tanımlama öznitelikleriyle alındığında (k8s-dağıtımı, hizmet adı vb.) burada görünecektir.",
|
||||
"fqns": "FQNs",
|
||||
"services": "HİZMETLER",
|
||||
"fullyCorrelated": "Tamamen İlişkili",
|
||||
"suggestions": "öneriler",
|
||||
"clickToViewSuggestions": "Korelasyon iyileştirme önerilerini görüntülemek için tıklayın",
|
||||
"byFqn": "Sanatçı: FQN",
|
||||
"byService": "Servise Göre",
|
||||
"byStream": "Yayına Göre",
|
||||
"searchFqnOrService": "FQN veya hizmet adını ara...",
|
||||
"searchServiceName": "Servis adını ara...",
|
||||
"searchStreamName": "Akış adını ara...",
|
||||
"allServices": "Tüm Hizmetler",
|
||||
"missingTelemetry": "Eksik Telemetri",
|
||||
"fqn": "FQN",
|
||||
"correlationKey": "Korelasyon Anahtarı",
|
||||
"telemetryCoverage": "Telemetri Kapsamı",
|
||||
"serviceName": "Hizmet Adı",
|
||||
"streams": "Akışlar",
|
||||
"fqnSource": "FQN Kaynağı",
|
||||
"viewDimensions": "Boyutları görüntüle",
|
||||
"viewServices": "Hizmetleri görmek için tıklayın",
|
||||
"dimensionsFor": "için boyutlar",
|
||||
"correlatedViaSharedDimensions": "paylaşılan boyutlar aracılığıyla ilişkilendirilmiş hizmet (ler)",
|
||||
"correlationDimensions": "Korelasyon Boyutları",
|
||||
"noSharedDimensions": "Hizmetler arasında paylaşılan boyut bulunamadı",
|
||||
"suggestionsDialogTitle": "Korelasyon Önerileri",
|
||||
"suggestionsDialogSubtitle": "İlişkilendirilebilen ancak telemetri türleri eksik olan hizmetler",
|
||||
"suggestionsInfoText": "Tam korelasyonu etkinleştirmek için telemetri verilerinizin günlükler, izler ve ölçümler arasında tutarlı hizmet tanımlama nitelikleri (k8s-dağıtımı, hizmet adı gibi) içerdiğinden emin olun.",
|
||||
"addTracingSuggestion": "Tam korelasyonu etkinleştirmek için OpenTelemetry izleme enstrümantasyonu ekleyin",
|
||||
"addLogsSuggestion": "Günlük koleksiyonunu eşleşen hizmet öznitelikleriyle yapılandırın",
|
||||
"addMetricsSuggestion": "Eşleşen hizmet boyutlarıyla ölçüm koleksiyonu ekleme",
|
||||
"missing": "Eksik",
|
||||
"deployment": "Dağıtım",
|
||||
"statefulSet": "DurumFullSet",
|
||||
"daemonSet": "DaemonSet",
|
||||
"ecsTask": "ECS Görevi",
|
||||
"faas": "FAA'ler",
|
||||
"service": "Hizmet",
|
||||
"serviceIdentityTitle": "Hizmet Kimliği Yapılandırması",
|
||||
"serviceIdentityDescription": "Hizmetlerin günlükler, izler ve metrikler arasında nasıl tanımlandığını ve ilişkilendirileceğini yapılandırın. Hizmet FQN (Tam Nitelikli Ad), iş yükü boyutlarından öncelik sırasına göre türetilir.",
|
||||
"howItWorksTitle": "Hizmet Kimliği Nasıl Çalışır",
|
||||
"howItWorksDescription": "(Tam Nitelikli Ad), farklı veri kaynakları arasındaki hizmetleri benzersiz bir şekilde tanımlamak ve ilişkilendirmek için telemetri boyutlarınızdan hesaplanır.",
|
||||
"priorityOrderLabel": "Öncelik sırası:",
|
||||
"priorityOrderDescription": "Boyutlar aşağıda listelenen sırayla kontrol edilir. Değeri olan ilk boyut Hizmet FQN'si olur.",
|
||||
"exampleLabel": "Örnek:",
|
||||
"exampleText1": "İkisine de sahipseniz",
|
||||
"exampleText2": "ve",
|
||||
"exampleText3": "FQN olacak",
|
||||
"exampleText4": "çünkü",
|
||||
"exampleText5": "daha yüksek önceliğe sahiptir.",
|
||||
"correlationDescription": "Aynı FQN'ye sahip hizmetler, farklı olsalar bile ilişkilidir",
|
||||
"correlationDescriptionField": "hizmet",
|
||||
"correlationDescriptionEnd": "isimleri.",
|
||||
"fqnPriorityTitle": "FQN Öncelik Boyutları",
|
||||
"fqnPriorityTooltip": "Korelasyon için Hizmet FQN'sini türetmek için hangi boyutların kullanılacağını tanımlar. Değeri olan ilk boyut kazanır. Altyapınıza uyacak şekilde yeniden sıralayın.",
|
||||
"fqnPriorityDescription": "Yeniden sıralamak için sürükleyin veya ok düğmelerini kullanın. İlk eşleşen boyut Hizmet FQN'sini belirler.",
|
||||
"resetToDefaults": "Varsayılanlara Sıfırla",
|
||||
"noDimensionsConfigured": "Yapılandırılmış boyut yok. “Varsayılanlara Sıfırla” ı tıklayın veya aşağıya boyutlar ekleyin.",
|
||||
"addDimensionLabel": "Öncelik listesine boyut ekle",
|
||||
"moveUp": "Yukarı hareket et (daha yüksek öncelik)",
|
||||
"moveDown": "Aşağı hareket et (düşük öncelik)",
|
||||
"removeFromList": "Öncelikli listeden kaldır",
|
||||
"semanticFieldTitle": "Anlamsal Alan Eşlemeleri",
|
||||
"semanticFieldTooltip": "Farklı alan adlarını anlamsal boyutlara eşler. Örneğin, “deployment”, “k8s.deployment.name” ve “kube_deployment” tümü “k8s-deployment” ile eşlenir.",
|
||||
"semanticFieldDescription": "Telemetrinizdeki alan adlarının anlamsal boyutlara nasıl eşleneceğini yapılandırın.",
|
||||
"saveConfiguration": "Yapılandırmayı Kaydet",
|
||||
"configSavedSuccess": "Hizmet kimliği yapılandırması başarıyla kaydedildi",
|
||||
"configSaveFailed": "Ayarlar kaydedilemedi",
|
||||
"correlationSettingsSaved": "Korelasyon ayarları başarıyla kaydedildi.",
|
||||
"logs": "Günlükler",
|
||||
"traces": "İzleri",
|
||||
"metrics": "Metrikler",
|
||||
"retry": "Tekrar dene",
|
||||
"k8sDeployment": "Kubernetes Dağıtım adı",
|
||||
"k8sStatefulSet": "Kubernetes stateFulSet name",
|
||||
"k8sDaemonSet": "Kubernetes DaemonSet adı",
|
||||
"k8sJob": "Kubernetes İş adı",
|
||||
"k8sReplicaSet": "Kubernetes ReplicaSet adı",
|
||||
"k8sNamespace": "Kubernetes Ad Alanı",
|
||||
"awsEcsTask": "AWS ECS Görev ailesi",
|
||||
"faasName": "Lambda, Bulut İşlevleri vb.",
|
||||
"gcpCloudRun": "Google Cloud Run hizmeti",
|
||||
"azureCloudRole": "Azure Cloud rol adı",
|
||||
"processName": "Çıplak metal proses adı",
|
||||
"serviceDim": "OpenTelemetry hizmeti.adı",
|
||||
"host": "Ana bilgisayar/düğüm adı",
|
||||
"environment": "Dağıtım ortamı"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "Kullanıcılar",
|
||||
@@ -1840,6 +1939,28 @@
|
||||
"matchedDimensions": "Eşleşen Boyutlar",
|
||||
"metricsButton": "{count} Metrik (ler)",
|
||||
"metricsTooltip": "Görüntülenecek metrik akışları seçin",
|
||||
"selectMetrics": "Metrik Akışları Seçin"
|
||||
"selectMetrics": "Metrik Akışları Seçin",
|
||||
"correlatedLogs": "İlişkili Günlükler",
|
||||
"correlatedMetrics": "İlişkili Metrikler",
|
||||
"correlatedTraces": "İlişkili İzler",
|
||||
"clickToLoadLogs": "İlişkili günlükleri yüklemek için tıklayın",
|
||||
"clickToLoadMetrics": "İlişkili metrikleri yüklemek için tıklayın",
|
||||
"clickToLoadTraces": "İlişkili izleri yüklemek için tıklayın",
|
||||
"loadingLogs": "Günlükler yükleniyor...",
|
||||
"noLogsFound": "İlişkili günlükler bulunamadı",
|
||||
"service": "Hizmet: {servis}",
|
||||
"tracesPlaceholder": "İzler burada uygulanacak",
|
||||
"correlatedTracesFor": "Hizmet için ilişkili izler: {service}",
|
||||
"metricsSelector": "{toplam} metrik (ler) den {seçilmiş}",
|
||||
"dimensions": "Filtreler",
|
||||
"all": "Hepsi",
|
||||
"loadingTraces": "İlişkili izler yükleniyor...",
|
||||
"tracesError": "İzler yüklenemedi",
|
||||
"directTraceMatch": "Doğrudan İzleme Eşleşmesi",
|
||||
"spans": "aralık (lar)",
|
||||
"dimensionBasedCorrelation": "Boyut Tabanlı Korelasyon",
|
||||
"tracesFromService": "Hizmetten gelen izler: {service}",
|
||||
"noTracesFound": "İlişkili iz bulunamadı",
|
||||
"noTracesDescription": "Seçilen zaman aralığında {service} hizmeti için iz bulunamadı"
|
||||
}
|
||||
}
|
||||
@@ -1437,7 +1437,106 @@
|
||||
"errorSavingDomainSettings": "保存域名设置时出错",
|
||||
"noDomainMessage": "未配置任何域。在上方添加域名以限制访问权限。",
|
||||
"license": "执照",
|
||||
"alertCorrelation": "重复数据删除警报"
|
||||
"alertCorrelation": "重复数据删除警报",
|
||||
"correlationSettings": "关联设置",
|
||||
"correlation": {
|
||||
"title": "关联设置",
|
||||
"subtitle": "发现和管理日志、跟踪和指标之间的服务相关性",
|
||||
"serviceIdentityTab": "服务身份",
|
||||
"discoveredServicesTab": "已发现的服务",
|
||||
"alertCorrelationTab": "警报关联",
|
||||
"discoveredServicesTitle": "已发现的服务",
|
||||
"discoveredServicesDescription": "服务会自动从您的遥测数据中发现,并按其完全限定名称 (FQN) 进行分组以进行关联。共享相同 FQN 的服务在日志、跟踪和指标中相互关联。",
|
||||
"noServicesYet": "尚未发现任何服务",
|
||||
"noServicesDescription": "当遥测数据中包含服务标识属性(如 k8s-部署、服务名称等)时,服务将显示在此处",
|
||||
"fqns": "FQN",
|
||||
"services": "服务",
|
||||
"fullyCorrelated": "完全相关",
|
||||
"suggestions": "建议",
|
||||
"clickToViewSuggestions": "点击查看关联度改善建议",
|
||||
"byFqn": "作者:FQN",
|
||||
"byService": "按服务分类",
|
||||
"byStream": "按直播",
|
||||
"searchFqnOrService": "搜索 FQN 或服务名称...",
|
||||
"searchServiceName": "搜索服务名称...",
|
||||
"searchStreamName": "搜索直播名称...",
|
||||
"allServices": "所有服务",
|
||||
"missingTelemetry": "缺少遥测",
|
||||
"fqn": "粉丝",
|
||||
"correlationKey": "关联密钥",
|
||||
"telemetryCoverage": "遥测覆盖范围",
|
||||
"serviceName": "服务名称",
|
||||
"streams": "直播",
|
||||
"fqnSource": "FQN 来源",
|
||||
"viewDimensions": "查看尺寸",
|
||||
"viewServices": "点击查看服务",
|
||||
"dimensionsFor": "的尺寸",
|
||||
"correlatedViaSharedDimensions": "通过共享维度关联的服务",
|
||||
"correlationDimensions": "关联维度",
|
||||
"noSharedDimensions": "未找到跨服务的共享维度",
|
||||
"suggestionsDialogTitle": "关联建议",
|
||||
"suggestionsDialogSubtitle": "本可以关联但缺少遥测类型的服务",
|
||||
"suggestionsInfoText": "要实现完全关联,请确保您的遥测数据在日志、跟踪和指标中包含一致的服务识别属性(如 k8s-deployment、服务名称)。",
|
||||
"addTracingSuggestion": "添加 OpenTelemetry 跟踪工具以启用完全关联",
|
||||
"addLogsSuggestion": "使用匹配的服务属性配置日志收集",
|
||||
"addMetricsSuggestion": "添加具有匹配服务维度的指标集合",
|
||||
"missing": "失踪",
|
||||
"deployment": "部署",
|
||||
"statefulSet": "StatefulSet",
|
||||
"daemonSet": "DaemonSet",
|
||||
"ecsTask": "ECS 任务",
|
||||
"faas": "常见问题解答",
|
||||
"service": "服务",
|
||||
"serviceIdentityTitle": "服务身份配置",
|
||||
"serviceIdentityDescription": "配置如何在日志、跟踪和指标中识别和关联服务。服务 FQN(完全限定名称)按优先级顺序从工作负载维度派生。",
|
||||
"howItWorksTitle": "服务标识的工作原理",
|
||||
"howItWorksDescription": "(完全限定名称)是根据您的遥测维度计算得出的,用于唯一地识别和关联不同数据源中的服务。",
|
||||
"priorityOrderLabel": "优先顺序:",
|
||||
"priorityOrderDescription": "尺寸按下面列出的顺序进行检查。第一个带有值的维度成为服务 FQN。",
|
||||
"exampleLabel": "示例:",
|
||||
"exampleText1": "如果你两者都有",
|
||||
"exampleText2": "和",
|
||||
"exampleText3": "FQN 将是",
|
||||
"exampleText4": "因为",
|
||||
"exampleText5": "具有更高的优先级。",
|
||||
"correlationDescription": "具有相同 FQN 的服务是相互关联的,即使它们具有不同的",
|
||||
"correlationDescriptionField": "服务",
|
||||
"correlationDescriptionEnd": "名字。",
|
||||
"fqnPriorityTitle": "FQN 优先级维度",
|
||||
"fqnPriorityTooltip": "定义使用哪些维度来推导服务 FQN 以进行关联。第一个有值的维度获胜。重新排序以匹配您的基础架构。",
|
||||
"fqnPriorityDescription": "拖动以重新排序或使用箭头按钮。第一个匹配维度决定服务 FQN。",
|
||||
"resetToDefaults": "重置为默认值",
|
||||
"noDimensionsConfigured": "未配置任何维度。单击 “重置为默认值” 或在下方添加尺寸。",
|
||||
"addDimensionLabel": "将维度添加到优先级列表",
|
||||
"moveUp": "向上移动(更高的优先级)",
|
||||
"moveDown": "向下移动(较低的优先级)",
|
||||
"removeFromList": "从优先级列表中移除",
|
||||
"semanticFieldTitle": "语义字段映射",
|
||||
"semanticFieldTooltip": "将不同的字段名称映射到语义维度。例如,“部署”、“k8s.deployment.name” 和 “kube_deployment” 都映射到 “k8s-deployment”。",
|
||||
"semanticFieldDescription": "配置如何将遥测中的字段名称映射到语义维度。",
|
||||
"saveConfiguration": "保存配置",
|
||||
"configSavedSuccess": "服务身份配置已成功保存",
|
||||
"configSaveFailed": "保存设置失败",
|
||||
"correlationSettingsSaved": "关联设置已成功保存。",
|
||||
"logs": "日志",
|
||||
"traces": "痕迹",
|
||||
"metrics": "指标",
|
||||
"retry": "重试",
|
||||
"k8sDeployment": "Kubernetes 部署名称",
|
||||
"k8sStatefulSet": "Kubernetes StatefulSet 名称",
|
||||
"k8sDaemonSet": "Kubernetes DaemonSet 名称",
|
||||
"k8sJob": "Kubernetes 任务名称",
|
||||
"k8sReplicaSet": "Kubernetes ReplicaSet 名称",
|
||||
"k8sNamespace": "Kubernetes 命名空间",
|
||||
"awsEcsTask": "AWS ECS 任务系列",
|
||||
"faasName": "Lambda、云函数等",
|
||||
"gcpCloudRun": "谷歌云端运行服务",
|
||||
"azureCloudRole": "Azure 云角色名称",
|
||||
"processName": "裸机进程名称",
|
||||
"serviceDim": "openTelemetry 服务名称",
|
||||
"host": "主机/节点名称",
|
||||
"environment": "部署环境"
|
||||
}
|
||||
},
|
||||
"iam": {
|
||||
"users": "用户",
|
||||
@@ -1836,6 +1935,28 @@
|
||||
"matchedDimensions": "匹配的尺寸",
|
||||
"metricsButton": "{count} 指标",
|
||||
"metricsTooltip": "选择要显示的指标流",
|
||||
"selectMetrics": "选择指标流"
|
||||
"selectMetrics": "选择指标流",
|
||||
"correlatedLogs": "关联日志",
|
||||
"correlatedMetrics": "相关指标",
|
||||
"correlatedTraces": "相关痕迹",
|
||||
"clickToLoadLogs": "点击加载相关日志",
|
||||
"clickToLoadMetrics": "点击加载相关指标",
|
||||
"clickToLoadTraces": "点击加载相关跟踪",
|
||||
"loadingLogs": "正在加载日志...",
|
||||
"noLogsFound": "未找到相关的日志",
|
||||
"service": "服务:{服务}",
|
||||
"tracesPlaceholder": "跟踪将在此处实现",
|
||||
"correlatedTracesFor": "服务的相关跟踪:{service}",
|
||||
"metricsSelector": "{总计} 个指标中的 {已选择}",
|
||||
"dimensions": "过滤器",
|
||||
"all": "全部",
|
||||
"loadingTraces": "正在加载相关的跟踪...",
|
||||
"tracesError": "加载追踪失败",
|
||||
"directTraceMatch": "直接追踪匹配",
|
||||
"spans": "跨度 (s)",
|
||||
"dimensionBasedCorrelation": "基于维度的关联",
|
||||
"tracesFromService": "来自服务的跟踪:{service}",
|
||||
"noTracesFound": "未找到相关的痕迹",
|
||||
"noTracesDescription": "在所选时间范围内未找到服务 {service} 的跟踪"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -794,7 +794,8 @@ describe("DetailTable Component", () => {
|
||||
"view-trace",
|
||||
"sendToAiChat",
|
||||
"closeTable",
|
||||
"show-correlation"
|
||||
"show-correlation",
|
||||
"load-correlation"
|
||||
];
|
||||
expect(componentOptions.emits).toEqual(expectedEmits);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-separator />
|
||||
<!-- Single Tab Row -->
|
||||
<div class="row justify-between q-pt-sm">
|
||||
<div class="col-10">
|
||||
<q-tabs v-model="tab" shrink align="left">
|
||||
@@ -52,9 +53,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
name="table"
|
||||
:label="t('common.table')"
|
||||
/>
|
||||
<!-- Correlation Tabs (only visible when service streams enabled) -->
|
||||
<q-tab
|
||||
v-if="serviceStreamsEnabled"
|
||||
name="correlated-logs"
|
||||
:label="t('correlation.correlatedLogs')"
|
||||
/>
|
||||
<q-tab
|
||||
v-if="serviceStreamsEnabled"
|
||||
name="correlated-metrics"
|
||||
:label="t('correlation.correlatedMetrics')"
|
||||
/>
|
||||
<q-tab
|
||||
v-if="serviceStreamsEnabled"
|
||||
name="correlated-traces"
|
||||
:label="t('correlation.correlatedTraces')"
|
||||
/>
|
||||
<!-- o2 ai context add button in the detail table -->
|
||||
<O2AIContextAddBtn
|
||||
class="tw-px-2 tw-py-2"
|
||||
<O2AIContextAddBtn
|
||||
class="tw-px-2 tw-py-2"
|
||||
@sendToAiChat="sendToAiChat(JSON.stringify(rowData))"
|
||||
/>
|
||||
</q-tabs>
|
||||
@@ -84,7 +101,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
<q-tab-panels
|
||||
data-test="log-detail-tab-container"
|
||||
class="tab-panels-container"
|
||||
:class="['tab-panels-container', tab.startsWith('correlated-') ? 'full-height-panels' : '']"
|
||||
v-model="tab"
|
||||
animated
|
||||
>
|
||||
@@ -97,6 +114,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
:value="rowData"
|
||||
show-copy-button
|
||||
mode="sidebar"
|
||||
hide-view-related
|
||||
:highlight-query="highlightQuery"
|
||||
@copy="copyContentToClipboard"
|
||||
@add-field-to-table="addFieldToTable"
|
||||
@@ -277,10 +295,91 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
<!-- Correlated Logs Tab Panel -->
|
||||
<q-tab-panel name="correlated-logs" class="q-pa-none full-height">
|
||||
<TelemetryCorrelationDashboard
|
||||
v-if="correlationProps"
|
||||
mode="embedded-tabs"
|
||||
external-active-tab="logs"
|
||||
:service-name="correlationProps.serviceName"
|
||||
:matched-dimensions="correlationProps.matchedDimensions"
|
||||
:metric-streams="correlationProps.metricStreams"
|
||||
:log-streams="correlationProps.logStreams"
|
||||
:trace-streams="correlationProps.traceStreams"
|
||||
:source-stream="correlationProps.sourceStream"
|
||||
:source-type="correlationProps.sourceType"
|
||||
:available-dimensions="correlationProps.availableDimensions"
|
||||
:fts-fields="correlationProps.ftsFields"
|
||||
:time-range="correlationProps.timeRange"
|
||||
@close="tab = 'json'"
|
||||
/>
|
||||
<!-- Loading/Empty state when no data -->
|
||||
<div v-else class="tw-flex tw-items-center tw-justify-center tw-h-full tw-py-20">
|
||||
<div class="tw-text-center">
|
||||
<q-spinner-hourglass v-if="correlationLoading" color="primary" size="3rem" class="tw-mb-4" />
|
||||
<div v-else class="tw-text-base tw-text-gray-500">{{ t('correlation.clickToLoadLogs') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Correlated Metrics Tab Panel -->
|
||||
<q-tab-panel name="correlated-metrics" class="q-pa-none full-height">
|
||||
<TelemetryCorrelationDashboard
|
||||
v-if="correlationProps"
|
||||
mode="embedded-tabs"
|
||||
external-active-tab="metrics"
|
||||
:service-name="correlationProps.serviceName"
|
||||
:matched-dimensions="correlationProps.matchedDimensions"
|
||||
:metric-streams="correlationProps.metricStreams"
|
||||
:log-streams="correlationProps.logStreams"
|
||||
:trace-streams="correlationProps.traceStreams"
|
||||
:source-stream="correlationProps.sourceStream"
|
||||
:source-type="correlationProps.sourceType"
|
||||
:available-dimensions="correlationProps.availableDimensions"
|
||||
:fts-fields="correlationProps.ftsFields"
|
||||
:time-range="correlationProps.timeRange"
|
||||
@close="tab = 'json'"
|
||||
/>
|
||||
<!-- Loading/Empty state when no data -->
|
||||
<div v-else class="tw-flex tw-items-center tw-justify-center tw-h-full tw-py-20">
|
||||
<div class="tw-text-center">
|
||||
<q-spinner-hourglass v-if="correlationLoading" color="primary" size="3rem" class="tw-mb-4" />
|
||||
<div v-else class="tw-text-base tw-text-gray-500">{{ t('correlation.clickToLoadMetrics') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
|
||||
<!-- Correlated Traces Tab Panel -->
|
||||
<q-tab-panel name="correlated-traces" class="q-pa-none full-height">
|
||||
<TelemetryCorrelationDashboard
|
||||
v-if="correlationProps"
|
||||
mode="embedded-tabs"
|
||||
external-active-tab="traces"
|
||||
:service-name="correlationProps.serviceName"
|
||||
:matched-dimensions="correlationProps.matchedDimensions"
|
||||
:metric-streams="correlationProps.metricStreams"
|
||||
:log-streams="correlationProps.logStreams"
|
||||
:trace-streams="correlationProps.traceStreams"
|
||||
:source-stream="correlationProps.sourceStream"
|
||||
:source-type="correlationProps.sourceType"
|
||||
:available-dimensions="correlationProps.availableDimensions"
|
||||
:fts-fields="correlationProps.ftsFields"
|
||||
:time-range="correlationProps.timeRange"
|
||||
@close="tab = 'json'"
|
||||
/>
|
||||
<!-- Loading/Empty state when no data -->
|
||||
<div v-else class="tw-flex tw-items-center tw-justify-center tw-h-full tw-py-20">
|
||||
<div class="tw-text-center">
|
||||
<q-spinner-hourglass v-if="correlationLoading" color="primary" size="3rem" class="tw-mb-4" />
|
||||
<div v-else class="tw-text-base tw-text-gray-500">{{ t('correlation.clickToLoadTraces') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-tab-panel>
|
||||
</q-tab-panels>
|
||||
|
||||
<q-separator />
|
||||
<q-card-section class="q-pa-md q-pb-md">
|
||||
<!-- Navigation buttons for log details (show only on JSON/Table tabs) -->
|
||||
<q-separator v-if="tab === 'json' || tab === 'table'" />
|
||||
<q-card-section v-if="tab === 'json' || tab === 'table'" class="q-pa-md q-pb-md">
|
||||
<div class="row items-center no-wrap justify-between">
|
||||
<div class="col-2">
|
||||
<q-btn
|
||||
@@ -342,7 +441,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onBeforeMount, computed } from "vue";
|
||||
import { defineComponent, ref, onBeforeMount, computed, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useStore } from "vuex";
|
||||
@@ -356,6 +455,7 @@ import LogsHighLighting from "@/components/logs/LogsHighLighting.vue";
|
||||
import { extractStatusFromLog } from "@/utils/logs/statusParser";
|
||||
import { logsUtils } from "@/composables/useLogs/logsUtils";
|
||||
import { searchState } from "@/composables/useLogs/searchState";
|
||||
import TelemetryCorrelationDashboard from "@/plugins/correlation/TelemetryCorrelationDashboard.vue";
|
||||
|
||||
const defaultValue: any = () => {
|
||||
return {
|
||||
@@ -365,7 +465,7 @@ const defaultValue: any = () => {
|
||||
|
||||
export default defineComponent({
|
||||
name: "SearchDetail",
|
||||
components: { EqualIcon, NotEqualIcon, JsonPreview, O2AIContextAddBtn, LogsHighLighting },
|
||||
components: { EqualIcon, NotEqualIcon, JsonPreview, O2AIContextAddBtn, LogsHighLighting, TelemetryCorrelationDashboard },
|
||||
emits: [
|
||||
"showPrevDetail",
|
||||
"showNextDetail",
|
||||
@@ -376,7 +476,8 @@ export default defineComponent({
|
||||
"view-trace",
|
||||
"sendToAiChat",
|
||||
"closeTable",
|
||||
"show-correlation"
|
||||
"show-correlation",
|
||||
"load-correlation" // New event for lazy loading correlation data
|
||||
],
|
||||
props: {
|
||||
modelValue: {
|
||||
@@ -399,6 +500,14 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
correlationProps: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
correlationLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleIncludeSearchTerm(
|
||||
@@ -437,6 +546,21 @@ export default defineComponent({
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
// Watch for tab changes - load correlation data when user clicks a correlation tab
|
||||
watch(tab, (newTab, oldTab) => {
|
||||
const isCorrelationTab = newTab.startsWith('correlated-');
|
||||
const wasCorrelationTab = oldTab?.startsWith('correlated-');
|
||||
|
||||
// Only emit if switching TO a correlation tab AND we don't have data yet
|
||||
if (isCorrelationTab && !props.correlationProps) {
|
||||
console.log("[DetailTable] User clicked correlation tab, emitting load-correlation");
|
||||
emit("load-correlation", rowData.value);
|
||||
}
|
||||
|
||||
// If switching FROM correlation tab back to JSON/Table, we keep the data loaded
|
||||
// (tabs persist, data persists until navigation)
|
||||
});
|
||||
|
||||
// Table columns for q-table
|
||||
const tableColumns = [
|
||||
{
|
||||
@@ -476,6 +600,11 @@ export default defineComponent({
|
||||
return extractStatusFromLog(rowData.value).color;
|
||||
});
|
||||
|
||||
// Check if service streams feature is enabled
|
||||
const serviceStreamsEnabled = computed(() => {
|
||||
return store.state.zoConfig.service_streams_enabled !== false;
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (window.localStorage.getItem("wrap-log-details") === null) {
|
||||
window.localStorage.setItem("wrap-log-details", "true");
|
||||
@@ -573,6 +702,7 @@ export default defineComponent({
|
||||
tableColumns,
|
||||
tableRows,
|
||||
tablePagination,
|
||||
serviceStreamsEnabled,
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
@@ -584,4 +714,19 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/logs/detail-table.scss";
|
||||
|
||||
// Make correlation tab panels use full remaining height (no footer space)
|
||||
.full-height-panels {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.q-tab-panel) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -434,6 +434,10 @@ export default {
|
||||
default: "",
|
||||
required: false,
|
||||
},
|
||||
hideViewRelated: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
NotEqualIcon,
|
||||
@@ -534,12 +538,13 @@ export default {
|
||||
|
||||
// Show button if correlation is available AND we're in detail view (sidebar or expanded)
|
||||
// AND service_streams is enabled in config
|
||||
// AND hideViewRelated prop is not set (used by DetailTable drawer to hide the button)
|
||||
// Mode can be 'sidebar' (when opened from sidebar) or 'expanded' (when log row is expanded in table)
|
||||
const isDetailView = props.mode === 'sidebar' || props.mode === 'expanded';
|
||||
const serviceStreamsEnabled = store.state.zoConfig.service_streams_enabled !== false; // Default to true if not set
|
||||
showViewRelatedBtn.value = available && isDetailView && serviceStreamsEnabled;
|
||||
showViewRelatedBtn.value = available && isDetailView && serviceStreamsEnabled && !props.hideViewRelated;
|
||||
|
||||
console.log("[JsonPreview] showViewRelatedBtn set to:", showViewRelatedBtn.value, "isDetailView:", isDetailView, "serviceStreamsEnabled:", serviceStreamsEnabled);
|
||||
console.log("[JsonPreview] showViewRelatedBtn set to:", showViewRelatedBtn.value, "isDetailView:", isDetailView, "serviceStreamsEnabled:", serviceStreamsEnabled, "hideViewRelated:", props.hideViewRelated);
|
||||
} catch (err) {
|
||||
console.error("[JsonPreview] Error checking correlation availability:", err);
|
||||
showViewRelatedBtn.value = false;
|
||||
|
||||
@@ -354,6 +354,8 @@ color="warning" size="xs"></q-icon> Error while
|
||||
]
|
||||
"
|
||||
:stream-type="searchObj.data.stream.streamType"
|
||||
:correlation-props="correlationDashboardProps"
|
||||
:correlation-loading="correlationLoading"
|
||||
class="detail-table-dialog"
|
||||
:currentIndex="searchObj.meta.resultGrid.navigation.currentRowIndex"
|
||||
:totalLength="parseInt(searchObj.data.queryResults.hits.length)"
|
||||
@@ -377,7 +379,7 @@ color="warning" size="xs"></q-icon> Error while
|
||||
"
|
||||
@sendToAiChat="sendToAiChat"
|
||||
@closeTable="closeTable"
|
||||
@show-correlation="openCorrelationFromLog"
|
||||
@load-correlation="openCorrelationFromLog"
|
||||
/>
|
||||
</q-dialog>
|
||||
|
||||
@@ -409,9 +411,10 @@ color="warning" size="xs"></q-icon> Error while
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Correlation Dashboard -->
|
||||
<!-- Correlation Dashboard (for inline expanded logs, opens as separate dialog) -->
|
||||
<TelemetryCorrelationDashboard
|
||||
v-if="showCorrelation && correlationDashboardProps"
|
||||
v-if="shouldShowInlineDialog"
|
||||
mode="dialog"
|
||||
:service-name="correlationDashboardProps.serviceName"
|
||||
:matched-dimensions="correlationDashboardProps.matchedDimensions"
|
||||
:metric-streams="correlationDashboardProps.metricStreams"
|
||||
@@ -420,6 +423,7 @@ color="warning" size="xs"></q-icon> Error while
|
||||
:source-stream="correlationDashboardProps.sourceStream"
|
||||
:source-type="correlationDashboardProps.sourceType"
|
||||
:available-dimensions="correlationDashboardProps.availableDimensions"
|
||||
:fts-fields="correlationDashboardProps.ftsFields"
|
||||
:time-range="correlationDashboardProps.timeRange"
|
||||
@close="showCorrelation = false"
|
||||
/>
|
||||
@@ -726,8 +730,21 @@ export default defineComponent({
|
||||
const showCorrelation = ref(false);
|
||||
const correlationContext = ref<TelemetryContext | null>(null);
|
||||
const correlationDashboardProps = ref<any>(null);
|
||||
const correlationLoading = ref(false);
|
||||
const { findRelatedTelemetry } = useServiceCorrelation();
|
||||
|
||||
// Debug: computed to check why dialog isn't showing
|
||||
const shouldShowInlineDialog = computed(() => {
|
||||
const result = showCorrelation.value && correlationDashboardProps.value && !searchObj.meta.showDetailTab;
|
||||
console.log("[SearchResult] shouldShowInlineDialog:", {
|
||||
showCorrelation: showCorrelation.value,
|
||||
hasProps: !!correlationDashboardProps.value,
|
||||
showDetailTab: searchObj.meta.showDetailTab,
|
||||
result
|
||||
});
|
||||
return result;
|
||||
});
|
||||
|
||||
const patternsColumns = [
|
||||
{
|
||||
accessorKey: "pattern_id",
|
||||
@@ -873,6 +890,8 @@ export default defineComponent({
|
||||
console.log("[SearchResult] Current stream:", searchObj.data.stream.selectedStream[0]);
|
||||
|
||||
try {
|
||||
correlationLoading.value = true;
|
||||
|
||||
// Set the correlation context from the log data
|
||||
const context: TelemetryContext = {
|
||||
timestamp: logData._timestamp || Date.now() * 1000,
|
||||
@@ -953,6 +972,11 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract FTS fields from stream settings
|
||||
const ftsFields = searchObj.data.stream.selectedStreamFields
|
||||
?.filter((field: any) => field.ftsKey === true)
|
||||
.map((field: any) => field.name) || [];
|
||||
|
||||
correlationDashboardProps.value = {
|
||||
serviceName: result.correlationData.service_name,
|
||||
matchedDimensions: result.correlationData.matched_dimensions,
|
||||
@@ -962,14 +986,22 @@ export default defineComponent({
|
||||
sourceStream: searchObj.data.stream.selectedStream[0],
|
||||
sourceType: "logs",
|
||||
availableDimensions: context.fields, // Actual field names from the log record
|
||||
ftsFields: ftsFields, // Full text search fields for trace_id extraction from log body
|
||||
timeRange: {
|
||||
startTime: startTimeMicros,
|
||||
endTime: endTimeMicros,
|
||||
},
|
||||
};
|
||||
|
||||
// Open the correlation dashboard
|
||||
showCorrelation.value = true;
|
||||
// For inline expanded logs, open the correlation dashboard as a dialog
|
||||
// For DetailTable drawer, the data is passed via props (tabs are already visible)
|
||||
console.log("[SearchResult] showDetailTab:", searchObj.meta.showDetailTab);
|
||||
if (!searchObj.meta.showDetailTab) {
|
||||
console.log("[SearchResult] Opening correlation dialog for inline expansion");
|
||||
showCorrelation.value = true;
|
||||
} else {
|
||||
console.log("[SearchResult] DetailTable drawer is open, passing props to drawer tabs");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("[SearchResult] Error in openCorrelationFromLog:", err);
|
||||
$q.notify({
|
||||
@@ -977,6 +1009,9 @@ export default defineComponent({
|
||||
message: `Correlation error: ${err.message || err}`,
|
||||
timeout: 3000,
|
||||
});
|
||||
correlationDashboardProps.value = null;
|
||||
} finally {
|
||||
correlationLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1091,6 +1126,11 @@ export default defineComponent({
|
||||
Number(searchObj.meta.resultGrid.navigation.currentRowIndex),
|
||||
);
|
||||
searchObj.meta.resultGrid.navigation.currentRowIndex = newIndex;
|
||||
|
||||
// Clear correlation data when navigating to a different log
|
||||
// User will need to click a correlation tab again for the new log
|
||||
correlationDashboardProps.value = null;
|
||||
correlationLoading.value = false;
|
||||
};
|
||||
|
||||
const addSearchTerm = (
|
||||
@@ -1370,6 +1410,8 @@ export default defineComponent({
|
||||
showCorrelation,
|
||||
correlationContext,
|
||||
correlationDashboardProps,
|
||||
correlationLoading,
|
||||
shouldShowInlineDialog,
|
||||
openCorrelationPanel,
|
||||
openCorrelationFromLog,
|
||||
};
|
||||
|
||||
@@ -89,6 +89,39 @@ export interface DimensionAnalyticsSummary {
|
||||
generated_at: number;
|
||||
}
|
||||
|
||||
// Types for grouped services API
|
||||
export interface ServiceStreamsInfo {
|
||||
logs: string[];
|
||||
traces: string[];
|
||||
metrics: string[];
|
||||
}
|
||||
|
||||
export interface ServiceInGroup {
|
||||
service_name: string;
|
||||
derived_from: string;
|
||||
streams: ServiceStreamsInfo;
|
||||
dimensions: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface StreamSummary {
|
||||
logs_count: number;
|
||||
traces_count: number;
|
||||
metrics_count: number;
|
||||
has_full_correlation: boolean;
|
||||
}
|
||||
|
||||
export interface ServiceFqnGroup {
|
||||
fqn: string;
|
||||
services: ServiceInGroup[];
|
||||
stream_summary: StreamSummary;
|
||||
}
|
||||
|
||||
export interface GroupedServicesResponse {
|
||||
groups: ServiceFqnGroup[];
|
||||
total_fqns: number;
|
||||
total_services: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get semantic field groups for field name translation
|
||||
* Uses the existing alerts/deduplication/semantic-groups API
|
||||
@@ -133,8 +166,26 @@ export const getDimensionAnalytics = (
|
||||
return http().get(`/api/${org_identifier}/service_streams/_analytics`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get services grouped by their FQN (Fully Qualified Name)
|
||||
*
|
||||
* Returns services organized by their correlation FQN, showing:
|
||||
* - Which services share the same FQN (correlated together)
|
||||
* - Stream counts per group (logs/traces/metrics)
|
||||
* - Whether each group has full telemetry coverage
|
||||
*
|
||||
* @param org_identifier Organization ID
|
||||
* @returns Grouped services response
|
||||
*/
|
||||
export const getGroupedServices = (
|
||||
org_identifier: string
|
||||
): Promise<{ data: GroupedServicesResponse }> => {
|
||||
return http().get(`/api/${org_identifier}/service_streams/_grouped`);
|
||||
};
|
||||
|
||||
export default {
|
||||
getSemanticGroups,
|
||||
correlate,
|
||||
getDimensionAnalytics,
|
||||
getGroupedServices,
|
||||
};
|
||||
|
||||
@@ -29,6 +29,71 @@ const settings = {
|
||||
updateCustomText: (org_identifier: string, key: string, value: string) => {
|
||||
return http().post(`/api/${org_identifier}/settings/logo/text`, value);
|
||||
},
|
||||
|
||||
// Settings v2 API - multi-level settings (system -> org -> user)
|
||||
|
||||
/**
|
||||
* Get a resolved setting (checks user -> org -> system levels)
|
||||
*/
|
||||
getSetting: (org_identifier: string, key: string, user_id?: string) => {
|
||||
let url = `/api/${org_identifier}/settings/v2/${key}`;
|
||||
if (user_id) {
|
||||
url += `?user_id=${encodeURIComponent(user_id)}`;
|
||||
}
|
||||
return http().get(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* List all resolved settings for an org/user
|
||||
*/
|
||||
listSettings: (org_identifier: string, user_id?: string, category?: string) => {
|
||||
const params = new URLSearchParams();
|
||||
if (user_id) params.append('user_id', user_id);
|
||||
if (category) params.append('category', category);
|
||||
const queryString = params.toString();
|
||||
const url = `/api/${org_identifier}/settings/v2${queryString ? '?' + queryString : ''}`;
|
||||
return http().get(url);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set an org-level setting
|
||||
*/
|
||||
setOrgSetting: (org_identifier: string, setting_key: string, setting_value: any, setting_category?: string, description?: string) => {
|
||||
const payload: any = {
|
||||
setting_key,
|
||||
setting_value,
|
||||
};
|
||||
if (setting_category) payload.setting_category = setting_category;
|
||||
if (description) payload.description = description;
|
||||
return http().post(`/api/${org_identifier}/settings/v2`, payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a user-level setting
|
||||
*/
|
||||
setUserSetting: (org_identifier: string, user_id: string, setting_key: string, setting_value: any, setting_category?: string, description?: string) => {
|
||||
const payload: any = {
|
||||
setting_key,
|
||||
setting_value,
|
||||
};
|
||||
if (setting_category) payload.setting_category = setting_category;
|
||||
if (description) payload.description = description;
|
||||
return http().post(`/api/${org_identifier}/settings/v2/user/${encodeURIComponent(user_id)}`, payload);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an org-level setting
|
||||
*/
|
||||
deleteOrgSetting: (org_identifier: string, key: string) => {
|
||||
return http().delete(`/api/${org_identifier}/settings/v2/${key}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a user-level setting
|
||||
*/
|
||||
deleteUserSetting: (org_identifier: string, user_id: string, key: string) => {
|
||||
return http().delete(`/api/${org_identifier}/settings/v2/user/${encodeURIComponent(user_id)}/${key}`);
|
||||
},
|
||||
};
|
||||
|
||||
export default settings;
|
||||
|
||||
Reference in New Issue
Block a user