use std::collections::BTreeMap;
use std::time::SystemTime;
use crate::contracts::UniversalEvent;
use crate::contracts::descriptor::{
ContractDescriptor, EncodedPayload, Interaction, PayloadDescriptor, Version,
};
use crate::contracts::envelope::MessageEnvelope;
use crate::contracts::metadata::{ContractMetadata, Participants};
use crate::events::EventName;
use crate::identity::{CapabilityId, ContractId, EngineId, EventId, MessageId};
use crate::status::{ArtifactReference, ErrorReference, Status};
use super::{LogContext, LogScope, LogSource, LogValidationError};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum LogLevel {
Debug,
Info,
Warning,
Error,
Audit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum LogEventType {
RequestReceived,
RequestSent,
CapabilityStarted,
CapabilityCompleted,
DependencyWaiting,
ResponseReceived,
Error,
Warning,
LifecycleChange,
Diagnostic,
}
impl LogEventType {
fn as_str(self) -> &'static str {
match self {
Self::RequestReceived => "request.received",
Self::RequestSent => "request.sent",
Self::CapabilityStarted => "capability.started",
Self::CapabilityCompleted => "capability.completed",
Self::DependencyWaiting => "dependency.waiting",
Self::ResponseReceived => "response.received",
Self::Error => "error",
Self::Warning => "warning",
Self::LifecycleChange => "lifecycle.change",
Self::Diagnostic => "diagnostic",
}
}
}
pub type LogMetadata = BTreeMap<String, String>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LogEvent {
pub event: UniversalEvent,
pub timestamp: SystemTime,
pub level: LogLevel,
pub source: LogSource,
pub scope: LogScope,
pub component: String,
pub context: LogContext,
pub message: String,
pub status: Option<Status>,
pub error_reference: Option<ErrorReference>,
pub metadata: LogMetadata,
pub artifact_reference: Option<ArtifactReference>,
}
impl LogEvent {
#[allow(clippy::too_many_arguments)]
pub fn new(
event_name: EventName,
level: LogLevel,
source: LogSource,
scope: LogScope,
component: impl Into<String>,
context: LogContext,
message: impl Into<String>,
event_type: LogEventType,
) -> Result<Self, LogValidationError> {
let component = component.into();
let message = message.into();
validate_log_inputs(&source, scope, &context, &component, &message)?;
let message_id = context
.message_id
.clone()
.unwrap_or_else(MessageId::generate);
let sender = match &source {
LogSource::Core => EngineId::new("core").expect("static core engine id is valid"),
LogSource::ControlPlane => {
EngineId::new("control-plane").expect("static control-plane engine id is valid")
}
LogSource::Runtime => {
EngineId::new("runtime").expect("static runtime engine id is valid")
}
LogSource::Engine(engine_id) => engine_id.clone(),
};
let descriptor = ContractDescriptor::new(
ContractId::new("logging.event").expect("static logging contract id is valid"),
CapabilityId::new("logging.emit").expect("static logging capability id is valid"),
Version::new(1, 0, 0),
Interaction::Event,
PayloadDescriptor::new("text/plain", Version::new(1, 0, 0))
.expect("static logging payload descriptor is valid"),
);
let payload_descriptor = descriptor.payload.clone();
let metadata = ContractMetadata::new(
descriptor,
Participants::new(
sender,
EngineId::new("logging-system").expect("static logging-system engine id is valid"),
),
);
let envelope = MessageEnvelope::new(
message_id,
context.operation.clone(),
metadata,
EncodedPayload::new(payload_descriptor, message.as_bytes().to_vec()),
);
let event_scope = match scope {
LogScope::Global => "global".to_owned(),
LogScope::Local => format!(
"engine:{}",
context
.engine_id
.as_ref()
.expect("validated local logging events have an engine context")
.as_str()
),
};
let event = UniversalEvent::new(
envelope,
event_name.to_string(),
event_type.as_str(),
event_scope,
)
.expect("validated logging metadata must produce a valid universal Event");
let event = Self {
event,
timestamp: SystemTime::now(),
level,
source,
scope,
component,
context,
message,
status: None,
error_reference: None,
metadata: BTreeMap::new(),
artifact_reference: None,
};
event.validate()?;
Ok(event)
}
pub fn event_id(&self) -> &EventId {
self.event.event_id()
}
pub fn message_id(&self) -> &MessageId {
self.event.message_id()
}
pub fn event_name(&self) -> &EventName {
self.event.event_name()
}
pub fn event_type(&self) -> &str {
self.event.event_type()
}
pub fn event_scope(&self) -> &str {
self.event.scope()
}
pub fn universal_event(&self) -> &UniversalEvent {
&self.event
}
pub fn status(&self) -> Option<&Status> {
self.status.as_ref()
}
pub fn error_reference(&self) -> Option<&ErrorReference> {
self.error_reference.as_ref()
}
pub fn metadata(&self) -> &LogMetadata {
&self.metadata
}
pub fn artifact_reference(&self) -> Option<&ArtifactReference> {
self.artifact_reference.as_ref()
}
pub fn with_status(mut self, status: Status) -> Self {
self.status = Some(status);
self
}
pub fn with_error(mut self, error_reference: ErrorReference) -> Self {
self.error_reference = Some(error_reference);
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
pub fn with_artifact(mut self, artifact_reference: ArtifactReference) -> Self {
self.artifact_reference = Some(artifact_reference);
self
}
pub fn validate(&self) -> Result<(), LogValidationError> {
if self.component.trim().is_empty() {
return Err(LogValidationError::EmptyComponent);
}
if self.message.trim().is_empty() {
return Err(LogValidationError::EmptyMessage);
}
if self.scope == LogScope::Global && self.context.engine_id.is_some() {
return Err(LogValidationError::GlobalEventHasEngineContext);
}
if self.scope == LogScope::Local && self.context.engine_id.is_none() {
return Err(LogValidationError::LocalEventNeedsEngineContext);
}
if self.scope == LogScope::Global && matches!(self.source, LogSource::Engine(_)) {
return Err(LogValidationError::GlobalEventHasEngineSource);
}
if let LogSource::Engine(source_engine_id) = &self.source
&& self.context.engine_id.as_ref() != Some(source_engine_id)
{
return Err(LogValidationError::SourceContextMismatch);
}
if self
.metadata
.iter()
.any(|(key, value)| key.trim().is_empty() || value.trim().is_empty())
{
return Err(LogValidationError::EmptyMetadataField);
}
Ok(())
}
}
fn validate_log_inputs(
source: &LogSource,
scope: LogScope,
context: &LogContext,
component: &str,
message: &str,
) -> Result<(), LogValidationError> {
if component.trim().is_empty() {
return Err(LogValidationError::EmptyComponent);
}
if message.trim().is_empty() {
return Err(LogValidationError::EmptyMessage);
}
if scope == LogScope::Global && context.engine_id.is_some() {
return Err(LogValidationError::GlobalEventHasEngineContext);
}
if scope == LogScope::Local && context.engine_id.is_none() {
return Err(LogValidationError::LocalEventNeedsEngineContext);
}
if scope == LogScope::Global && matches!(source, LogSource::Engine(_)) {
return Err(LogValidationError::GlobalEventHasEngineSource);
}
if let LogSource::Engine(source_engine_id) = source
&& context.engine_id.as_ref() != Some(source_engine_id)
{
return Err(LogValidationError::SourceContextMismatch);
}
Ok(())
}