///|
/// The source location retained when an input record becomes an incident event.
pub(all) struct EventEvidence {
evidence_id : String
line_number : Int
} derive(Debug, Eq, ToJson)
///|
/// A normalized event with the identity needed for cross-source analysis.
///
/// `resource_id` is optional because records without an explicit resource remain
/// useful in a timeline but must not be correlated automatically.
pub(all) struct IncidentEvent {
event_id : String
normalized : NormalizedEvent
resource_id : String?
kind : CorrelationKind
attributes : Json
evidence : EventEvidence
} derive(Debug, Eq, ToJson)
///|
/// Explicit field mapping for JSONL records from one source shape.
pub(all) struct JsonlEventMapping {
system : String
source_field : String
event_id_field : String
timestamp_field : String
severity_field : String
resource_field : String?
resource_override : String?
kind : CorrelationKind
evidence_id : String
} derive(Debug, Eq)
///|
/// Mapping for the fixed ` ` text format.
pub(all) struct PlainTextEventMapping {
system : String
component : String
resource_id : String?
kind : CorrelationKind
evidence_id : String
} derive(Debug, Eq)
///|
/// A record cannot enter the common event contract until its required fields are valid.
pub(all) suberror EventAdaptError {
MissingField(line_number~ : Int, field~ : String)
InvalidField(line_number~ : Int, field~ : String, value~ : String)
InvalidText(line_number~ : Int, reason~ : String)
} derive(Debug, Eq)
///|
fn json_field(value : Json, field : String) -> Json? {
if value is Object(object) {
object.get(field)
} else {
None
}
}
///|
fn required_json_string(
value : Json,
line_number : Int,
field : String,
) -> String raise EventAdaptError {
match json_field(value, field) {
Some(String(text)) => text
Some(other) =>
raise EventAdaptError::InvalidField(
line_number~,
field~,
value=other.stringify(),
)
None => raise EventAdaptError::MissingField(line_number~, field~)
}
}
///|
fn mapped_resource(
value : Json,
line_number : Int,
mapping : JsonlEventMapping,
) -> String? raise EventAdaptError {
match mapping.resource_override {
Some(resource) =>
if resource.trim() == "" {
raise EventAdaptError::InvalidField(
line_number~,
field="resource_override",
value=resource,
)
} else {
Some(resource.trim().to_owned())
}
None =>
match mapping.resource_field {
Some(field) => {
let resource = required_json_string(value, line_number, field)
if resource.trim() == "" {
raise EventAdaptError::InvalidField(
line_number~,
field~,
value=resource,
)
}
Some(resource.trim().to_owned())
}
None => None
}
}
}
///|
fn normalized_from_fields(
line_number : Int,
timestamp : String,
severity : String,
system : String,
component : String,
raw_content : String,
) -> NormalizedEvent raise EventAdaptError {
normalize_event(
Some(timestamp),
Some(severity),
system,
component,
None,
raw_content,
) catch {
_ =>
raise EventAdaptError::InvalidField(
line_number~,
field="timestamp_or_severity",
value=raw_content,
)
}
}
///|
/// Converts JSONL objects to events using an explicit field and resource mapping.
/// Unknown JSON attributes are retained as `attributes`; no field names are guessed.
pub fn adapt_jsonl_records(
records : Array[JsonlRecord],
mapping : JsonlEventMapping,
) -> Array[IncidentEvent] raise EventAdaptError {
let events : Array[IncidentEvent] = []
for record in records {
let event_id = required_json_string(
record.value,
record.line_number,
mapping.event_id_field,
)
let timestamp = required_json_string(
record.value,
record.line_number,
mapping.timestamp_field,
)
let severity = required_json_string(
record.value,
record.line_number,
mapping.severity_field,
)
let component = required_json_string(
record.value,
record.line_number,
mapping.source_field,
)
let normalized = normalized_from_fields(
record.line_number,
timestamp,
severity,
mapping.system,
component,
record.value.stringify(),
)
events.push({
event_id,
normalized,
resource_id: mapped_resource(record.value, record.line_number, mapping),
kind: mapping.kind,
attributes: record.value,
evidence: {
evidence_id: mapping.evidence_id,
line_number: record.line_number,
},
})
}
events
}
///|
/// Adapts JSONL records through the canonical event contract.
///
/// This is the migration entry point for callers that still use
/// JsonlEventMapping. The legacy mapping remains explicit, while the result
/// is ready for processors, declarative rules, and incident graphs.
pub fn adapt_jsonl_records_as_canonical(
records : Array[JsonlRecord],
mapping : JsonlEventMapping,
) -> Array[CanonicalEvent] raise EventAdaptError {
adapt_jsonl_records(records, mapping).map(event => event.to_canonical_event())
}
///|
/// Converts fixed-format text records to events with an explicit source mapping.
pub fn adapt_plain_text_records(
records : Array[PlainTextRecord],
mapping : PlainTextEventMapping,
) -> Array[IncidentEvent] raise EventAdaptError {
let events : Array[IncidentEvent] = []
for record in records {
let fields : Array[String] = []
for field in record.text.split(" ") {
let field = field.trim()
if field != "" {
fields.push(field.to_owned())
}
}
guard fields.length() >= 3 else {
raise EventAdaptError::InvalidText(
line_number=record.line_number,
reason="expected timestamp, severity, and message",
)
}
let timestamp = fields[0]
let severity = fields[1]
let normalized = normalized_from_fields(
record.line_number,
timestamp,
severity,
mapping.system,
mapping.component,
record.text,
)
let resource_id = match mapping.resource_id {
Some(resource) =>
if resource.trim() == "" {
raise EventAdaptError::InvalidText(
line_number=record.line_number,
reason="resource_id must not be empty",
)
} else {
Some(resource.trim().to_owned())
}
None => None
}
events.push({
event_id: "\{mapping.evidence_id}:\{record.line_number}",
normalized,
resource_id,
kind: mapping.kind,
attributes: {},
evidence: {
evidence_id: mapping.evidence_id,
line_number: record.line_number,
},
})
}
events
}
///|
/// Adapts fixed-format text through the canonical event contract.
pub fn adapt_plain_text_records_as_canonical(
records : Array[PlainTextRecord],
mapping : PlainTextEventMapping,
) -> Array[CanonicalEvent] raise EventAdaptError {
adapt_plain_text_records(records, mapping).map(event => {
event.to_canonical_event()
})
}
///|
/// Builds the existing timeline view from adapted events without losing event identity.
pub fn incident_timeline(events : Array[IncidentEvent]) -> Timeline {
build_timeline(
events.map(event => event.to_canonical_event().to_normalized_event()),
)
}
///|
/// Correlates adapted events, excluding only events without an explicit resource.
pub fn correlate_incident_events(
events : Array[IncidentEvent],
window_seconds : Int,
) -> Array[CorrelationGroup] raise CorrelationError {
let correlation_events : Array[CorrelationEvent] = []
for event in events {
let canonical = event.to_canonical_event()
match canonical.resource {
Some(resource) =>
correlation_events.push({
event: canonical.to_normalized_event(),
resource_id: resource.id,
kind: event.kind,
})
None => ()
}
}
correlate_events(correlation_events, window_seconds)
}