///|
#derive(Eq, Debug)
pub(all) enum Stage {
Plan
Decode
Frontend
Parse
ProfileCheck
Resolve
Lower
Generate
Build
Invoke
Call
Evaluate
Compare
Cleanup
Execute
Legacy
}
///|
pub fn Stage::name(self : Stage) -> String {
match self {
Plan => "plan"
Decode => "decode"
Frontend => "frontend"
Parse => "parse"
ProfileCheck => "profile-check"
Resolve => "resolve"
Lower => "lower"
Generate => "generate"
Build => "build"
Invoke => "invoke"
Call => "call"
Evaluate => "evaluate"
Compare => "compare"
Cleanup => "cleanup"
Execute => "execute"
Legacy => "legacy"
}
}
///|
#derive(Eq, Debug)
pub(all) enum Signal {
Started
Succeeded
Failed
Cancelled
Notice
}
///|
#derive(Eq, Debug)
pub(all) enum Delivery {
Accepted
Filtered
Dropped
Unavailable
}
///|
pub(all) struct ObservationContext {
run_id : String
span_id : String
parent_id : String
trace_id : String
}
///|
/// Fixed fields exclude model values, paths, environment and unbounded labels.
pub(all) struct Details {
entry : String
code : String
}
///|
pub(all) struct Observation {
details : Details?
context : ObservationContext
correlation : Correlation?
sequence : Int32
stage : Stage
signal : Signal
wall_seconds : Double
duration_seconds : Double
}
///|
pub(all) struct Observer {
enabled : () -> Bool
try_record : (Observation) -> Delivery raise
monotonic : () -> Double
wall : () -> Double
next_id : () -> String
run_id : String
trace_id : String
sequence : Ref[Int32]
}
///|
pub fn Observer::noop() -> Observer {
{
enabled: fn() { false },
try_record: fn(_) { Filtered },
monotonic: fn() { 0.0 },
wall: fn() { 0.0 },
next_id: fn() { "" },
run_id: "",
trace_id: "",
sequence: Ref(0),
}
}
///|
pub struct Scope {
priv observer : Observer
priv context : ObservationContext
priv sequence : Ref[Int32]
}
///|
pub fn Observer::scope(self : Observer) -> Scope {
{
observer: self,
context: {
run_id: self.run_id,
trace_id: self.trace_id,
span_id: "",
parent_id: "",
},
sequence: self.sequence,
}
}
///|
/// Sink errors are contained by record; exporter health stays outside this scope.
pub fn[T] Scope::observe(
self : Scope,
stage : Stage,
body : (Scope) -> T raise,
details~ : Details? = None,
is_cancelled~ : (Error) -> Bool = fn(error) {
error is @execution.Invalid("Cancelled")
},
) -> T raise {
let o = self.observer
if !(o.enabled)() {
return body(self)
}
let context = ObservationContext::{
run_id: o.run_id,
trace_id: o.trace_id,
span_id: (o.next_id)(),
parent_id: self.context.span_id,
}
let child = Scope::{ observer: o, context, sequence: self.sequence, }
let start = (o.monotonic)()
fn emit(signal : Signal, duration : Double) -> Unit {
self.sequence.val += 1
ignore(
o.record({
context,
details: match signal {
Failed | Cancelled =>
Some({
entry: details.map(d => d.entry).unwrap_or(""),
code: if signal is Cancelled {
"execution.cancelled"
} else {
"execution.stage_failed"
},
})
_ => details
},
correlation: None,
sequence: self.sequence.val,
stage,
signal,
wall_seconds: (o.wall)(),
duration_seconds: duration,
}),
)
}
emit(Started, 0.0)
let mut terminal = Signal::Failed
errdefer emit(terminal, (o.monotonic)() - start)
let attempt = Ok(body(child)) catch { error => Err(error) }
let result = match attempt {
Ok(value) => value
Err(error) => {
if is_cancelled(error) {
terminal = Cancelled
}
raise error
}
}
emit(Succeeded, (o.monotonic)() - start)
result
}
///|
pub fn Observation::to_ion(self : Observation) -> @ion.IonValue {
@execution.record([
("profile", @ion.IonValue::string("morphir-observation-v1")),
(
"details",
match self.details {
Some(d) =>
@execution.record([
("entry", @ion.IonValue::string(d.entry)),
("code", @ion.IonValue::string(d.code)),
])
None => @ion.IonValue::untyped_null()
},
),
("run_id", @ion.IonValue::string(self.context.run_id)),
("trace_id", @ion.IonValue::string(self.context.trace_id)),
(
"correlation",
match self.correlation {
Some(c) => c.to_ion()
None => @ion.Ion(@ion.IonValueKind::Null(@ion.Null))
},
),
("span_id", @ion.IonValue::string(self.context.span_id)),
("parent_id", @ion.IonValue::string(self.context.parent_id)),
("sequence", @ion.IonValue::int(self.sequence)),
("stage", @ion.IonValue::string(self.stage.name())),
(
"signal",
@ion.IonValue::string(
match self.signal {
Started => "started"
Succeeded => "succeeded"
Failed => "failed"
Cancelled => "cancelled"
Notice => "notice"
},
),
),
("wall_seconds", @ion.IonValue::float(self.wall_seconds)),
("duration_seconds", @ion.IonValue::float(self.duration_seconds)),
])
}
///|
pub fn Observation::to_json(self : Observation) -> Json {
{
"profile": "morphir-observation-v1",
"details": match self.details {
Some(d) => { "entry": d.entry, "code": d.code }
None => Json::null()
},
"run_id": self.context.run_id,
"trace_id": self.context.trace_id,
"correlation": match self.correlation {
Some(c) => c.to_json()
None => Json::null()
},
"span_id": self.context.span_id,
"parent_id": self.context.parent_id,
"sequence": self.sequence,
"stage": self.stage.name(),
"signal": match self.signal {
Started => "started"
Succeeded => "succeeded"
Failed => "failed"
Cancelled => "cancelled"
Notice => "notice"
},
"wall_seconds": self.wall_seconds,
"duration_seconds": self.duration_seconds,
}
}
///|
pub extend Stage with Eq::{equal, not_equal}
///|
pub extend Stage with @debug.Debug::{to_repr}
///|
pub extend Signal with Eq::{equal, not_equal}
///|
pub extend Signal with @debug.Debug::{to_repr}
///|
pub extend Delivery with Eq::{equal, not_equal}
///|
pub extend Delivery with @debug.Debug::{to_repr}
///|
pub(all) struct Correlation {
target : String
lease_id : String
call_id : String
attempt_id : String
}
///|
pub fn Correlation::to_json(self : Correlation) -> Json {
{
"target": self.target,
"lease_id": self.lease_id,
"call_id": self.call_id,
"attempt_id": self.attempt_id,
}
}
///|
pub fn Correlation::to_ion(self : Correlation) -> @ion.IonValue {
@execution.record([
("target", @ion.IonValue::string(self.target)),
("lease_id", @ion.IonValue::string(self.lease_id)),
("call_id", @ion.IonValue::string(self.call_id)),
("attempt_id", @ion.IonValue::string(self.attempt_id)),
])
}
///|
/// W3C version 00 only. Baggage and tracestate are deliberately unsupported.
#derive(Eq)
pub(all) struct TraceContext {
trace_id : String
parent_id : String
flags : String
}
///|
fn hex_id(s : String, size : Int32) -> Bool {
s.length() == size &&
s.to_array().all(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) &&
s.to_array().any(c => c != '0')
}
///|
pub fn TraceContext::parse(value : String) -> TraceContext raise {
let parts = value.split("-").map(s => s.to_owned()).collect()
if parts.length() != 4 ||
parts[0] != "00" ||
!hex_id(parts[1], 32) ||
!hex_id(parts[2], 16) ||
!["00", "01"].contains(parts[3]) {
raise @execution.Invalid("observability.invalid_trace_context")
}
{ trace_id: parts[1], parent_id: parts[2], flags: parts[3], }
}
///|
pub fn TraceContext::header(self : TraceContext) -> String raise {
let value = "00-" + self.trace_id + "-" + self.parent_id + "-" + self.flags
ignore(TraceContext::parse(value))
value
}
///|
pub fn Scope::trace_context(self : Scope) -> TraceContext? raise {
if !(self.observer.enabled)() {
return None
}
let context = TraceContext::{
trace_id: self.context.trace_id,
parent_id: self.context.span_id,
flags: "01",
}
ignore(context.header())
Some(context)
}
///|
pub extend TraceContext with Eq::{equal, not_equal}