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

Settings.pudu

Pudu144 lines7.4 KB

GitHub ↗
1/** @Log.Settings.Loader — a configuration read from settings */2module PuduLangLog.Settings34import Std.Env as Env5import Std.Io as Io6import Std.List as List7import PuduLangLog.Configuration as Configuration8import PuduLangLog.Domain.Settings as Plan9import PuduLangLog.Expressions as Expressions10import PuduLangLog.LevelSwitch as LevelSwitch11import PuduLangLog as Log12import PuduLangLog.Settings.Registry as Registry13import PuduLangLog.Sink as Sink1415/** @Log.Settings.Loaded — a configuration and the level switches it declared */16export type Loaded = { configuration: Configuration.Configuration, switches: Array[(Str, LevelSwitch.LevelSwitch)] }1718/// The sink argument restricting it to a minimum level.19const MINIMUM_ARGUMENT: Str = "restrictedToMinimumLevel"2021/// The base configuration changed by the settings, or every setting that could not be used. Keys22/// are colon-separated, section names ignore case, and sinks and enrichers are found by name in23/// the registry:24///25/// - `MinimumLevel` or `MinimumLevel:Default`: the minimum level.26/// - `MinimumLevel:ControlledBy`: the level switch, `$name`, controlling the minimum.27/// - `MinimumLevel:Override:Source`: the level or level switch of a source prefix.28/// - `LevelSwitches:$name`: a level switch and its initial level.29/// - `Properties:Name`: a text property added to every event.30/// - `Enrich:i` (or `Enrich:i:Name`) and `Enrich:i:Args:arg`: a named enricher.31/// - `WriteTo:i` (or `WriteTo:i:Name`) and `WriteTo:i:Args:arg`: a named sink; the arguments32///   `restrictedToMinimumLevel` and `levelSwitch` apply to any sink. `AuditTo` is the same for33///   audit sinks.34/// - `Filter:i:ByExcluding` and `Filter:i:ByIncludingOnly`: a filter expression.35/// - `Destructure:MaximumDepth`, `Destructure:MaximumStringLength`,36///   `Destructure:MaximumCollectionCount`, and `Destructure:AsScalar:i`: capture limits and tags37///   captured as text.38export fn apply(base: &Configuration.Configuration, pairs: &Array[(Str, Str)], registry: &Registry.Registry) -> Result[Loaded, Array[Str]] {39  let read = Plan.read(pairs)40  let plan = read.plan41  var problems = read.problems42  let switches = plan.switches.map(|entry: (Str, Log.Level)| (entry[0], LevelSwitch.create(entry[1])))43  var configuration = *base44  if let Some(level) = plan.minimum { configuration = configuration.minimumLevel(level) }45  if let Some(name) = plan.controlledBy {46    if let Some(control) = switchOf(&switches, name) { configuration = configuration.controlledBy(&control) }47  }48  for (source, chosen) in plan.overrides {49    match (Plan.levelOf(chosen), switchOf(&switches, Plan.switchName(chosen))) {50      case (Some(level), _) => { configuration = configuration.overrideLevel(source, level) }51      case (None, Some(control)) => { configuration = configuration.overrideSwitch(source, &control) }52      case _ => {}53    }54  }55  for (name, text) in plan.properties { configuration = configuration.enrichWithProperty(name, Log.Scalar(Log.Text(text))) }56  for target in plan.enrichers {57    match Registry.enricherNamed(registry, target.name) {58      case Some(factory) => match factory(&target.arguments) {59        case Ok(enricher) => { configuration = configuration.enrichWith(enricher) }60        case Err(problem) => { problems = problems.push("The enricher `" + target.name + "`: " + problem) }61      }62      case None => { problems = problems.push("No enricher is registered as `" + target.name + "`") }63    }64  }65  for filter in plan.filters {66    match Expressions.predicate(filter.expression) {67      case Ok(test) => { configuration = if filter.including { configuration.filterIncludingOnly(test) } else { configuration.filterExcluding(test) } }68      case Err(problem) => { problems = problems.push("The filter `" + filter.expression + "`: " + problem) }69    }70  }71  if let Some(depth) = plan.maximumDepth { configuration = configuration.maximumDepth(depth) }72  if let Some(length) = plan.maximumStringLength { configuration = configuration.maximumStringLength(length) }73  if let Some(size) = plan.maximumCollectionCount { configuration = configuration.maximumCollectionCount(size) }74  for tag in plan.scalars { configuration = configuration.destructureAsScalar(tag) }75  for (audit, targets) in [(false, plan.sinks), (true, plan.audits)] {76    for target in targets {77      match sinkOf(&target, registry, &switches) {78        case Ok(sink) => { configuration = if audit { configuration.auditTo(sink) } else { configuration.writeTo(sink) } }79        case Err(problem) => { problems = problems.push(problem) }80      }81    }82  }83  if !problems.isEmpty() { return Err(problems) }84  Ok(Loaded{configuration: configuration, switches: switches})85}8687/// The sink a target names, restricted by its minimum level and level switch arguments.88fn sinkOf(target: &Plan.Target, registry: &Registry.Registry, switches: &Array[(Str, LevelSwitch.LevelSwitch)]) -> Result[Sink.Sink, Str] {89  let factory = match Registry.sinkNamed(registry, target.name) {90    case Some(found) => found91    case None => { return Err("No sink is registered as `" + target.name + "`") }92  }93  let own = target.arguments.filter(|entry: (Str, Str)| entry[0].toLower() != MINIMUM_ARGUMENT.toLower() && entry[0].toLower() != Plan.LEVEL_SWITCH_ARGUMENT.toLower())94  var sink = match factory(&own) {95    case Ok(built) => built96    case Err(problem) => { return Err("The sink `" + target.name + "`: " + problem) }97  }98  if let Some(text) = Plan.argument(target, MINIMUM_ARGUMENT) {99    match Plan.levelOf(text) {100      case Some(level) => { sink = Sink.restricted(sink, level) }101      case None => { return Err("The sink `" + target.name + "`: `" + text + "` is not a level") }102    }103  }104  if let Some(name) = Plan.argument(target, Plan.LEVEL_SWITCH_ARGUMENT) {105    if let Some(control) = switchOf(switches, Plan.switchName(name)) { sink = Sink.controlled(sink, &control) }106  }107  Ok(sink)108}109110/// The declared level switch of a name.111fn switchOf(switches: &Array[(Str, LevelSwitch.LevelSwitch)], name: Str) -> Option[LevelSwitch.LevelSwitch] {112  let found = List.find(switches, |entry: (Str, LevelSwitch.LevelSwitch)| entry[0] == name) ?113  Some(found[1])114}115116/// The level switch the settings declared under a name, with or without its `$`, to change the117/// level while the program runs.118export fn switchNamed(loaded: &Loaded, name: Str) -> Option[LevelSwitch.LevelSwitch] { switchOf(&loaded.switches, Plan.switchName(name)) }119120/// The settings of a JSON document under a top-level section, such as `Log`; the whole document121/// when the section is empty.122export fn fromJson(text: Str, section: Str) -> Result[Array[(Str, Str)], Str] { Plan.fromJson(text, section) }123124/// The settings of a JSON file under a top-level section.125export fn fromJsonFile(path: Str, section: Str) -> Result[Array[(Str, Str)], Str] {126  match Io.read(path) {127    case Ok(text) => fromJson(text, section)128    case Err(problem) => Err("Cannot read `" + path + "`: " + show(problem))129  }130}131132/// The settings of environment variables named `PREFIX__Section__Key`, the prefix ignoring case:133/// `APP__MinimumLevel__Default=Debug` sets `MinimumLevel:Default`.134export fn fromEnvironment(prefix: Str) -> Array[(Str, Str)] {135  var found: Array[(Str, Str)] = []136  for (name, value) in Env.variables() {137    if let Some(key) = Plan.environmentKey(name, prefix) { found = found.push((key, value)) }138  }139  found140}141142/// An argument of a sink or enricher factory, its name ignoring case.143export fn argument(arguments: &Array[(Str, Str)], name: Str) -> Option[Str] { Plan.argument(&Plan.Target{name: "", arguments: *arguments}, name) }144