Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-log

Structured event logging for Pudu: message templates, enrichment, filtering, formatting, and sinks

0.1.0Apache-2.01

InstallClose

Logger.pudu

Pudu224 lines10.1 KB

GitHub ↗
1/** @Log.Logger.Facade — writes events and derives contextual loggers */2module PuduLangLog.Logger34import Std.Sync as Sync5import PuduLangLog.Constants.Names as Names6import PuduLangLog.Domain.Capture as Capture7import PuduLangLog.Domain.Parser as Parser8import PuduLangLog.Enricher as Enricher9import PuduLangLog as Log10import PuduLangLog.Pipeline as Pipeline11import PuduLangLog.SelfLog as SelfLog12import PuduLangLog.Sink as Sink1314/** @Log.Logger.Handle — a pipeline seen through one context */15export type Logger = { root: Root, context: Array[Enricher.Enricher], source: Option[Str], trace: Option[(Str, Str)] }1617/** @Log.Logger.Root — a pipeline fixed at creation or replaceable later */18export type Root = Fixed(Pipeline.Pipeline) | Reloadable(Sync.Cell[Pipeline.Pipeline])1920/// A logger over a pipeline.21export fn fromPipeline(pipeline: Pipeline.Pipeline) -> Logger { Logger{root: Fixed(pipeline), context: [], source: None, trace: None} }2223/// A logger whose pipeline can be replaced with `reload`; every logger derived from it follows.24export fn reloadable(pipeline: Pipeline.Pipeline) -> Logger { Logger{root: Reloadable(Sync.cell(pipeline)), context: [], source: None, trace: None} }2526/// Replaces the pipeline of a reloadable logger, flushing and closing the previous one. Answers27/// whether the logger was reloadable.28export fn reload(logger: &Logger, pipeline: Pipeline.Pipeline) -> Bool {29  match &logger.root {30    case Reloadable(cell) => {31      if let Ok(previous) = Sync.swap(&cell, pipeline) {32        (previous.sink.flush)()33        (previous.sink.close)()34      }35      true36    }37    case Fixed(_) => false38  }39}4041/// A logger that writes nothing.42export fn none() -> Logger { fromPipeline(Pipeline.silent()) }4344/// The pipeline the logger currently writes through.45export fn pipelineOf(logger: &Logger) -> Pipeline.Pipeline {46  match &logger.root {47    case Fixed(pipeline) => pipeline48    case Reloadable(cell) => {49      match Sync.get(&cell) {50        case Ok(pipeline) => pipeline51        case Err(_) => Pipeline.silent()52      }53    }54  }55}5657/// Flushes every sink.58export fn flush(logger: &Logger) -> () {59  let pipeline = pipelineOf(logger)60  (pipeline.sink.flush)()61  if let Some(audit) = pipeline.audit { (audit.flush)() }62}6364/// Flushes and closes every sink; call it once before the program ends.65export fn close(logger: &Logger) -> () {66  flush(logger)67  let pipeline = pipelineOf(logger)68  (pipeline.sink.close)()69  if let Some(audit) = pipeline.audit { (audit.close)() }70}7172/// The logger as a sink, so one pipeline can feed another. Events pass the logger's level check,73/// enrichers, and filters.74export fn asSink(logger: &Logger) -> Sink.Sink {75  let shared = *logger76  Sink.Sink {77    emit: fn(event: &Log.Event) -> Result[(), Str] {78      let pipeline = pipelineOf(&shared)79      if !Pipeline.enabled(&pipeline, &shared.source, event.level) { return Ok(()) }80      Pipeline.process(&pipeline, *event, &shared.context)81    },82    flush: fn() -> () { flush(&shared) },83    close: fn() -> () { close(&shared) },84    attach: fn(listener: Sink.Listener) -> () { (pipelineOf(&shared).sink.attach)(listener) }85  }86}8788/** @Log.Logger.Logging — the operations of a logger */89export trait Logging {90  fn isEnabled(self: &Self, level: Log.Level) -> Bool91  fn write(self: &Self, level: Log.Level, template: Str, arguments: Array[Log.Value]) -> ()92  fn writeFailure(self: &Self, level: Log.Level, failure: Log.Failure, template: Str, arguments: Array[Log.Value]) -> ()93  fn tryWrite(self: &Self, level: Log.Level, failure: Option[Log.Failure], template: Str, arguments: Array[Log.Value]) -> Result[(), Str]94  fn emit(self: &Self, event: Log.Event) -> Result[(), Str]95  fn verbose(self: &Self, template: Str, arguments: Array[Log.Value]) -> ()96  fn debug(self: &Self, template: Str, arguments: Array[Log.Value]) -> ()97  fn information(self: &Self, template: Str, arguments: Array[Log.Value]) -> ()98  fn warning(self: &Self, template: Str, arguments: Array[Log.Value]) -> ()99  fn error(self: &Self, template: Str, arguments: Array[Log.Value]) -> ()100  fn fatal(self: &Self, template: Str, arguments: Array[Log.Value]) -> ()101  fn forContext(self: &Self, name: Str, held: Log.Value) -> Self102  fn forContextDestructured(self: &Self, name: Str, held: Log.Value) -> Self103  fn forSource(self: &Self, source: Str) -> Self104  fn forEnricher(self: &Self, enricher: Enricher.Enricher) -> Self105  fn withTrace(self: &Self, traceId: Str, spanId: Str) -> Self106  fn bindTemplate(self: &Self, template: Str, arguments: Array[Log.Value]) -> (Log.Template, Array[Log.Property])107  fn bindProperty(self: &Self, name: Str, held: Log.Value, destructure: Bool) -> Option[Log.Property]108}109110impl Logging for Logger {111  /// Whether an event at the level would be written.112  fn isEnabled(self: &Self, level: Log.Level) -> Bool { Pipeline.enabled(&pipelineOf(self), &self.source, level) }113114  /// Writes an event from a template and its arguments.115  fn write(self: &Self, level: Log.Level, template: Str, arguments: Array[Log.Value]) -> () {116    let _written = self.tryWrite(level, None, template, arguments)117  }118119  /// Writes an event carrying a failure.120  fn writeFailure(self: &Self, level: Log.Level, failure: Log.Failure, template: Str, arguments: Array[Log.Value]) -> () {121    let _written = self.tryWrite(level, Some(failure), template, arguments)122  }123124  /// Writes an event and answers the failure of an audit sink, if any; other sinks never fail a125  /// write.126  fn tryWrite(self: &Self, level: Log.Level, failure: Option[Log.Failure], template: Str, arguments: Array[Log.Value]) -> Result[(), Str] {127    let pipeline = pipelineOf(self)128    if !Pipeline.enabled(&pipeline, &self.source, level) { return Ok(()) }129    let parsed = Pipeline.templateOf(&pipeline, template)130    let bound = Capture.bind(&pipeline.policy, &parsed, &arguments)131    Pipeline.report(&pipeline, &bound.problems)132    let event = Log.Event {133      timestamp: (pipeline.clock)(),134      level: level,135      template: parsed,136      properties: bound.properties,137      failure: failure,138      traceId: traceOf(&self.trace, true),139      spanId: traceOf(&self.trace, false)140    }141    Pipeline.process(&pipeline, event, &self.context)142  }143144  /// Writes a complete event when its level is enabled.145  fn emit(self: &Self, event: Log.Event) -> Result[(), Str] {146    let pipeline = pipelineOf(self)147    if !Pipeline.enabled(&pipeline, &self.source, event.level) { return Ok(()) }148    Pipeline.process(&pipeline, event, &self.context)149  }150151  /// Writes a `Verbose` event.152  fn verbose(self: &Self, template: Str, arguments: Array[Log.Value]) -> () { self.write(Log.Verbose, template, arguments) }153154  /// Writes a `Debug` event.155  fn debug(self: &Self, template: Str, arguments: Array[Log.Value]) -> () { self.write(Log.Debug, template, arguments) }156157  /// Writes an `Information` event.158  fn information(self: &Self, template: Str, arguments: Array[Log.Value]) -> () { self.write(Log.Information, template, arguments) }159160  /// Writes a `Warning` event.161  fn warning(self: &Self, template: Str, arguments: Array[Log.Value]) -> () { self.write(Log.Warning, template, arguments) }162163  /// Writes an `Error` event.164  fn error(self: &Self, template: Str, arguments: Array[Log.Value]) -> () { self.write(Log.Error, template, arguments) }165166  /// Writes a `Fatal` event.167  fn fatal(self: &Self, template: Str, arguments: Array[Log.Value]) -> () { self.write(Log.Fatal, template, arguments) }168169  /// A logger adding a property to every event unless the event names it itself; structures170  /// render to text. A blank name is reported to the self-log and ignored.171  fn forContext(self: &Self, name: Str, held: Log.Value) -> Self { contextual(self, name, held, Log.Default) }172173  /// A logger adding a property to every event, keeping structures.174  fn forContextDestructured(self: &Self, name: Str, held: Log.Value) -> Self { contextual(self, name, held, Log.Destructure) }175176  /// A logger whose events carry the source context, which also selects level overrides.177  fn forSource(self: &Self, source: Str) -> Self { contextual(self, Names.SOURCE_CONTEXT, Log.Scalar(Log.Text(source)), Log.Default) }178179  /// A logger applying an enricher to every event before the pipeline's own.180  fn forEnricher(self: &Self, enricher: Enricher.Enricher) -> Self { Logger{..*self, context: self.context.push(enricher)} }181182  /// A logger whose events carry a trace and span identifier.183  fn withTrace(self: &Self, traceId: Str, spanId: Str) -> Self { Logger{..*self, trace: Some((traceId, spanId))} }184185  /// The parsed template and the properties the arguments bind to, as a write would produce.186  fn bindTemplate(self: &Self, template: Str, arguments: Array[Log.Value]) -> (Log.Template, Array[Log.Property]) {187    let pipeline = pipelineOf(self)188    let parsed = Pipeline.templateOf(&pipeline, template)189    let bound = Capture.bind(&pipeline.policy, &parsed, &arguments)190    Pipeline.report(&pipeline, &bound.problems)191    (parsed, bound.properties)192  }193194  /// A property captured as a write would capture it, or none for a blank name.195  fn bindProperty(self: &Self, name: Str, held: Log.Value, destructure: Bool) -> Option[Log.Property] {196    if !Parser.isValidName(name) { return None }197    let hint = if destructure { Log.Destructure } else { Log.Default }198    Some(Log.Property{name: name, value: Capture.capture(&pipelineOf(self).policy, &held, hint)})199  }200}201202/// A logger with a captured property added; the source context, as given and before any203/// truncation, also selects overrides.204fn contextual(logger: &Logger, name: Str, held: Log.Value, hint: Log.Hint) -> Logger {205  let pipeline = pipelineOf(logger)206  if !Parser.isValidName(name) {207    SelfLog.write(&pipeline.selfLog, "Attempt to add a context property with an invalid name `" + name + "`")208    return *logger209  }210  let captured = Capture.capture(&pipeline.policy, &held, hint)211  let enricher = Enricher.property(name, captured)212  let source = match (name == Names.SOURCE_CONTEXT, held) {213    case (true, Log.Scalar(Log.Text(text))) => Some(text)214    case _ => logger.source215  }216  Logger{..*logger, context: logger.context.push(enricher), source: source}217}218219/// The trace identifier, or the span identifier when `trace` is false.220fn traceOf(trace: &Option[(Str, Str)], wantTrace: Bool) -> Option[Str] {221  let pair = (*trace) ?222  Some(if wantTrace { pair[0] } else { pair[1] })223}224