Icons, docs formats
This commit is contained in:
@@ -67,6 +67,40 @@ pub fn extract_json_field(text: &str, field_path: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Flattens a JSON object into dot-separated field paths mapped to each leaf scalar's raw text form
|
||||
/// (string/number/bool), for [`crate::engine::auto_declare_params`] when a line is JSON
|
||||
/// (`crate::logfmt::detect_format`) rather than logfmt — the JSON equivalent of
|
||||
/// `crate::logfmt::extract_pairs`'s `key=value` scan. Nested objects recurse, dot-joining the path
|
||||
/// (`"foo": {"bar": 1}` yields `"foo.bar"`); arrays and null are skipped since there's no single leaf
|
||||
/// scalar to declare a param against, matching `extract_json_field`'s own restriction to scalars.
|
||||
/// Returns an empty vec for anything that isn't a JSON object, same as `extract_pairs` returns empty
|
||||
/// for a line with no `key=value` pairs.
|
||||
pub fn flatten_json_object(text: &str) -> Vec<(String, String)> {
|
||||
let Ok(serde_json::Value::Object(root)) = serde_json::from_str::<serde_json::Value>(text) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut results = Vec::new();
|
||||
flatten_json_object_into(&root, "", &mut results);
|
||||
results
|
||||
}
|
||||
|
||||
fn flatten_json_object_into(
|
||||
object: &serde_json::Map<String, serde_json::Value>,
|
||||
prefix: &str,
|
||||
results: &mut Vec<(String, String)>,
|
||||
) {
|
||||
for (key, value) in object {
|
||||
let path = if prefix.is_empty() { key.clone() } else { format!("{prefix}.{key}") };
|
||||
match value {
|
||||
serde_json::Value::String(s) => results.push((path, s.clone())),
|
||||
serde_json::Value::Number(n) => results.push((path, n.to_string())),
|
||||
serde_json::Value::Bool(b) => results.push((path, b.to_string())),
|
||||
serde_json::Value::Object(nested) => flatten_json_object_into(nested, &path, results),
|
||||
serde_json::Value::Null | serde_json::Value::Array(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `rename_all = "lowercase"` matches other `kind`-style enums, so JSON bodies use "string"/"number"/"timestamp".
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
+79
-16
@@ -356,37 +356,79 @@ fn auto_derived_param_count(index: &BTreeMap<IndexKey, DataValue>) -> usize {
|
||||
param_declarations_range(index).filter(|declaration| declaration.6).count()
|
||||
}
|
||||
|
||||
/// Scans `text` for `key=value`/`key="value"` pairs (`crate::logfmt::extract_pairs`) and declares a
|
||||
/// standalone param for any key not already declared — auto or manual. A manual declaration for a
|
||||
/// name always wins and is never touched here; this only fills in names nobody has declared yet.
|
||||
/// Run on every ingested line (`EngineMessage::Ingest`), same as `apply_all_metrics`, but note param
|
||||
/// *values* are still recomputed from raw text at read time (`derive_log_params`), not stored here —
|
||||
/// so a key discovered on line N also back-fills the column for lines before N on the next read.
|
||||
/// Bounded by `MAX_AUTO_DECLARED_PARAMS`/`MAX_NEW_AUTO_PARAMS_PER_LINE` (see their doc comments) so a
|
||||
/// single burst of unusual lines can't permanently inflate the declared-param set (ticket 050).
|
||||
/// One param worth auto-declaring, found on an ingested line by either extraction path
|
||||
/// `auto_declare_params` supports: a logfmt `key=value` pair (name, and a regex/capture-group built
|
||||
/// from it) or a JSON leaf field (name being its dot-separated path, extracted via `json_field_path`
|
||||
/// instead of a regex). Lets `auto_declare_params` share one insert-and-budget loop across both.
|
||||
struct AutoParamCandidate {
|
||||
name: String,
|
||||
value: String,
|
||||
message_match_regex: String,
|
||||
capture_group: String,
|
||||
json_field_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Scans `text` for auto-derivable params and declares a standalone param for any name not already
|
||||
/// declared — auto or manual. A manual declaration for a name always wins and is never touched here;
|
||||
/// this only fills in names nobody has declared yet. JSON lines (`crate::logfmt::detect_format`) are
|
||||
/// scanned via `crate::data_value::flatten_json_object` (dot-path leaf fields, extracted by
|
||||
/// `json_field_path` rather than regex, since a `key=value` scan finds nothing useful in JSON's
|
||||
/// `"key":value` shape); anything else falls back to the original `key=value` pair scan
|
||||
/// (`crate::logfmt::extract_pairs`). Run on every ingested line (`EngineMessage::Ingest`), same as
|
||||
/// `apply_all_metrics`, but note param *values* are still recomputed from raw text at read time
|
||||
/// (`derive_log_params`), not stored here — so a key discovered on line N also back-fills the column
|
||||
/// for lines before N on the next read. Bounded by `MAX_AUTO_DECLARED_PARAMS`/
|
||||
/// `MAX_NEW_AUTO_PARAMS_PER_LINE` (see their doc comments) so a single burst of unusual lines can't
|
||||
/// permanently inflate the declared-param set (ticket 050).
|
||||
fn auto_declare_params(index: &mut BTreeMap<IndexKey, DataValue>, text: &str) {
|
||||
let mut total = auto_derived_param_count(index);
|
||||
if total >= MAX_AUTO_DECLARED_PARAMS {
|
||||
return;
|
||||
}
|
||||
let candidates: Vec<AutoParamCandidate> = if crate::logfmt::detect_format(text) == "json" {
|
||||
crate::data_value::flatten_json_object(text)
|
||||
.into_iter()
|
||||
.map(|(path, value)| AutoParamCandidate {
|
||||
name: path.clone(),
|
||||
value,
|
||||
message_match_regex: String::new(),
|
||||
capture_group: String::new(),
|
||||
json_field_path: Some(path),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
crate::logfmt::extract_pairs(text)
|
||||
.into_iter()
|
||||
.map(|(key, value, quoted)| {
|
||||
let group = sanitize_group_name(key);
|
||||
AutoParamCandidate {
|
||||
name: key.to_string(),
|
||||
value: value.to_string(),
|
||||
message_match_regex: build_auto_param_pattern(key, &group, quoted),
|
||||
capture_group: group,
|
||||
json_field_path: None,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let mut new_this_line = 0;
|
||||
for (key, value, quoted) in crate::logfmt::extract_pairs(text) {
|
||||
for candidate in candidates {
|
||||
if new_this_line >= MAX_NEW_AUTO_PARAMS_PER_LINE || total >= MAX_AUTO_DECLARED_PARAMS {
|
||||
break;
|
||||
}
|
||||
let index_key = IndexKey::ParamDeclaration { name: key.to_string() };
|
||||
let index_key = IndexKey::ParamDeclaration { name: candidate.name.clone() };
|
||||
if index.contains_key(&index_key) {
|
||||
continue;
|
||||
}
|
||||
let group = sanitize_group_name(key);
|
||||
index.insert(
|
||||
index_key,
|
||||
DataValue::ParamDeclaration(ParamDeclaration {
|
||||
name: key.to_string(),
|
||||
message_match_regex: build_auto_param_pattern(key, &group, quoted),
|
||||
capture_group: group,
|
||||
json_field_path: None,
|
||||
kind: infer_kind(value),
|
||||
name: candidate.name,
|
||||
message_match_regex: candidate.message_match_regex,
|
||||
capture_group: candidate.capture_group,
|
||||
json_field_path: candidate.json_field_path,
|
||||
kind: infer_kind(&candidate.value),
|
||||
elapsed_since_last: false,
|
||||
auto_derived: true,
|
||||
}),
|
||||
@@ -2012,6 +2054,27 @@ mod tests {
|
||||
assert_eq!(params.get("msg"), Some(&MetricPrimitive::String("entity_moved".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_declare_params_discovers_json_leaf_fields_via_json_field_path() {
|
||||
let mut index = BTreeMap::new();
|
||||
auto_declare_params(&mut index, r#"{"level":"error","code":500,"request":{"path":"/x"}}"#);
|
||||
|
||||
let declarations: HashMap<&str, _> =
|
||||
param_declarations_range(&index).map(|d| (d.0, d)).collect();
|
||||
assert_eq!(declarations.len(), 3);
|
||||
assert_eq!(declarations["code"].4, MetricPrimitiveKind::Number);
|
||||
assert_eq!(declarations["code"].3, Some("code"));
|
||||
assert!(declarations["level"].6, "expected level to be flagged auto_derived");
|
||||
assert_eq!(declarations["request.path"].3, Some("request.path"));
|
||||
|
||||
let compiled = compiled_param_declarations(&index);
|
||||
let params =
|
||||
derive_log_params(r#"{"level":"error","code":500,"request":{"path":"/x"}}"#, &compiled);
|
||||
assert_eq!(params.get("level"), Some(&MetricPrimitive::String("error".to_string())));
|
||||
assert_eq!(params.get("code"), Some(&MetricPrimitive::Number(Number::Int(500))));
|
||||
assert_eq!(params.get("request.path"), Some(&MetricPrimitive::String("/x".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_declare_params_never_overwrites_an_existing_declaration() {
|
||||
let mut index = BTreeMap::new();
|
||||
|
||||
Reference in New Issue
Block a user