
Settings.pudu
Pudu297 lines13.0 KB
1/** @Log.Domain.Settings.Module — key-value settings read into a logging plan */2module PuduLangLog.Domain.Settings34import Std.Json as Json5import Std.List as List6import Std.Map as Map7import Std.Option as Option8import PuduLangLog.Domain.Levels as Levels9import PuduLangLog as Log1011/** @Log.Domain.Settings.Target — a named sink or enricher and its arguments */12export type Target = { name: Str, arguments: Array[(Str, Str)] }1314/** @Log.Domain.Settings.Filter — an expression that keeps or drops events */15export type Filter = { including: Bool, expression: Str }1617/** @Log.Domain.Settings.Plan — every decision the settings make */18export type Plan = {19 minimum: Option[Log.Level],20 controlledBy: Option[Str],21 switches: Array[(Str, Log.Level)],22 overrides: Array[(Str, Str)],23 properties: Array[(Str, Str)],24 enrichers: Array[Target],25 sinks: Array[Target],26 audits: Array[Target],27 filters: Array[Filter],28 maximumDepth: Option[Int],29 maximumStringLength: Option[Int],30 maximumCollectionCount: Option[Int],31 scalars: Array[Str]32}3334/** @Log.Domain.Settings.Read — a plan and the settings it could not use */35export type Read = { plan: Plan, problems: Array[Str] }3637/** @Log.Domain.Settings.Group — one numbered or named entry of a section */38type Group = { key: Str, target: Target }3940/** @Log.Domain.Settings.State — the plan so far and the entries of each section */41type State = { plan: Plan, problems: Array[Str], enrichers: Array[Group], sinks: Array[Group], audits: Array[Group] }424344const INTERVALS: Map[Str, Int] = mapOf([("infinite", 0), ("year", 1), ("month", 2), ("day", 3), ("hour", 4), ("minute", 5)])454647const ENVIRONMENT_SEPARATOR: Str = "__"484950const SWITCH_MARK: Str = "$"515253export const LEVEL_SWITCH_ARGUMENT: Str = "levelSwitch"545556export fn empty() -> Plan {57 Plan {58 minimum: None,59 controlledBy: None,60 switches: [],61 overrides: [],62 properties: [],63 enrichers: [],64 sinks: [],65 audits: [],66 filters: [],67 maximumDepth: None,68 maximumStringLength: None,69 maximumCollectionCount: None,70 scalars: []71 }72}7374757677export fn read(pairs: &Array[(Str, Str)]) -> Read {78 var state = State{plan: empty(), problems: [], enrichers: [], sinks: [], audits: []}79 for pair in *pairs { state = setting(&state, pair[0], pair[1]) }80 let plan = Plan{..state.plan, enrichers: targetsOf(&state.enrichers), sinks: targetsOf(&state.sinks), audits: targetsOf(&state.audits)}81 Read{plan: plan, problems: state.problems.concat(unresolved(&plan))}82}838485fn setting(state: &State, key: Str, value: Str) -> State {86 let parts = key.split(":").map(|part: Str| part.trim())87 let section = Option.unwrapOr(List.first(&parts), "").toLower()88 let rest = List.drop(&parts, 1)89 let plan = state.plan90 if section == "minimumlevel" { return minimumLevel(state, key, &rest, value) }91 if section == "levelswitches" && rest.length() == 1 {92 let level = match levelOf(value) {93 case Some(found) => found94 case None => { return refused(state, key) }95 }96 return State{..*state, plan: Plan{..plan, switches: put(&plan.switches, switchName(rest[0]), level)}}97 }98 if section == "properties" && rest.length() == 1 && !rest[0].isEmpty() { return State{..*state, plan: Plan{..plan, properties: put(&plan.properties, rest[0], value)}} }99 if section == "enrich" { return grouped(state, key, &rest, value, 0) }100 if section == "writeto" { return grouped(state, key, &rest, value, 1) }101 if section == "auditto" { return grouped(state, key, &rest, value, 2) }102 if section == "filter" && rest.length() == 2 {103 let kind = rest[1].toLower()104 if kind == "byexcluding" || kind == "byincludingonly" { return State{..*state, plan: Plan{..plan, filters: plan.filters.push(Filter{including: kind == "byincludingonly", expression: value})}} }105 }106 if section == "destructure" { return destructure(state, key, &rest, value) }107 refused(state, key)108}109110111112fn minimumLevel(state: &State, key: Str, rest: &Array[Str], value: Str) -> State {113 let plan = state.plan114 let first = Option.unwrapOr(List.first(rest), "default").toLower()115 if rest.length() < 2 && first == "default" {116 let level = match levelOf(value) {117 case Some(found) => found118 case None => { return refused(state, key) }119 }120 return State{..*state, plan: Plan{..plan, minimum: Some(level)}}121 }122 if rest.length() == 1 && first == "controlledby" { return State{..*state, plan: Plan{..plan, controlledBy: Some(switchName(value))}} }123 if first == "override" {124 let source = List.drop(rest, 1).join(":")125 let chosen = value.trim()126 if source.isEmpty() || (!chosen.startsWith(SWITCH_MARK) && levelOf(chosen) == None) { return refused(state, key) }127 return State{..*state, plan: Plan{..plan, overrides: put(&plan.overrides, source, chosen)}}128 }129 refused(state, key)130}131132133134fn grouped(state: &State, key: Str, rest: &Array[Str], value: Str, which: Int) -> State {135 let groups = if which == 0 { state.enrichers } else if which == 1 { state.sinks } else { state.audits }136 let entry = Option.unwrapOr(List.first(rest), "").toLower()137 let field = Option.unwrapOr(List.get(rest, 1), "").toLower()138 if entry.isEmpty() { return refused(state, key) }139 let current = match List.find(&groups, |group: Group| group.key == entry) {140 case Some(found) => found.target141 case None => Target{name: "", arguments: []}142 }143 let updated = if rest.length() == 1 || (rest.length() == 2 && field == "name") {144 Target{..current, name: value.trim()}145 } else if rest.length() == 3 && field == "args" && !rest[2].isEmpty() {146 Target{..current, arguments: put(¤t.arguments, rest[2], value)}147 } else {148 return refused(state, key)149 }150 let others = groups.filter(|group: Group| group.key != entry)151 let changed = if others.length() == groups.length() { groups.push(Group{key: entry, target: updated}) } else { groups.map(|group: Group| if group.key == entry { Group{key: entry, target: updated} } else { group }) }152 if which == 0 { State{..*state, enrichers: changed} } else if which == 1 { State{..*state, sinks: changed} } else { State{..*state, audits: changed} }153}154155156fn destructure(state: &State, key: Str, rest: &Array[Str], value: Str) -> State {157 let plan = state.plan158 let first = Option.unwrapOr(List.first(rest), "").toLower()159 if rest.length() == 2 && first == "asscalar" { return State{..*state, plan: Plan{..plan, scalars: plan.scalars.push(value.trim())}} }160 if rest.length() != 1 { return refused(state, key) }161 let limit = match count(value) {162 case Some(found) => found163 case None => { return refused(state, key) }164 }165 if first == "maximumdepth" { return State{..*state, plan: Plan{..plan, maximumDepth: Some(limit)}} }166 if first == "maximumstringlength" { return State{..*state, plan: Plan{..plan, maximumStringLength: Some(limit)}} }167 if first == "maximumcollectioncount" { return State{..*state, plan: Plan{..plan, maximumCollectionCount: Some(limit)}} }168 refused(state, key)169}170171172fn refused(state: &State, key: Str) -> State { State{..*state, problems: state.problems.push("Unknown or malformed setting `" + key + "`")} }173174175fn targetsOf(groups: &Array[Group]) -> Array[Target] { groups.map(|group: Group| group.target) }176177178fn unresolved(plan: &Plan) -> Array[Str] {179 var problems: Array[Str] = []180 let declared = plan.switches.map(|entry: (Str, Log.Level)| entry[0])181 for (section, targets) in [("Enrich", plan.enrichers), ("WriteTo", plan.sinks), ("AuditTo", plan.audits)] {182 for target in targets {183 if target.name.isEmpty() { problems = problems.push("An entry of `" + section + "` names nothing") }184 }185 }186 var references: Array[Str] = []187 if let Some(name) = plan.controlledBy { references = references.push(name) }188 for entry in plan.overrides {189 if entry[1].startsWith(SWITCH_MARK) { references = references.push(switchName(entry[1])) }190 }191 for target in plan.sinks.concat(plan.audits) {192 if let Some(name) = argument(&target, LEVEL_SWITCH_ARGUMENT) { references = references.push(switchName(name)) }193 }194 for name in references {195 if !declared.contains(name) { problems = problems.push("The level switch `" + name + "` is not declared") }196 }197 problems198}199200201export fn switchName(text: Str) -> Str {202 let trimmed = text.trim()203 (if trimmed.startsWith(SWITCH_MARK) { trimmed.drop(1) } else { trimmed }).toLower()204}205206207export fn levelOf(text: Str) -> Option[Log.Level] { Levels.parse(text) }208209210export fn intervalOf(text: Str) -> Option[Log.RollingInterval] {211 let rank = Map.get(&INTERVALS, text.trim().toLower()) ?212 Some(Log.ROLLING_INTERVALS[rank])213}214215216export fn count(text: Str) -> Option[Int] {217 let number = text.trim().toInt() ?218 if number < 0 { None } else { Some(number) }219}220221222export fn flag(text: Str) -> Option[Bool] {223 let lowered = text.trim().toLower()224 if lowered == "true" { Some(true) } else if lowered == "false" { Some(false) } else { None }225}226227228export fn argument(target: &Target, name: Str) -> Option[Str] {229 let found = List.find(&target.arguments, |entry: (Str, Str)| entry[0].toLower() == name.toLower()) ?230 Some(found[1])231}232233234fn put[T](pairs: &Array[(Str, T)], key: Str, value: T) -> Array[(Str, T)] {235 let others = pairs.filter(|entry: (Str, T)| entry[0].toLower() != key.toLower())236 if others.length() == pairs.length() { return pairs.push((key, value)) }237 pairs.map(|entry: (Str, T)| if entry[0].toLower() == key.toLower() { (key, value) } else { entry })238}239240241242export fn fromJson(text: Str, section: Str) -> Result[Array[(Str, Str)], Str] {243 match Json.decode(text) {244 case Ok(document) => Ok(flatten(&document, section))245 case Err(problem) => Err("The settings are not JSON: " + Json.explain(&problem))246 }247}248249250fn flatten(document: &Json.Json, section: Str) -> Array[(Str, Str)] {251 if section.isEmpty() { return entries(document, "") }252 let members = match Json.asObject(document) {253 case Some(found) => found254 case None => { return [] }255 }256 match List.find(&members, |member: (Str, Json.Json)| member[0].toLower() == section.toLower()) {257 case Some(found) => entries(&found[1], "")258 case None => []259 }260}261262263fn entries(held: &Json.Json, prefix: Str) -> Array[(Str, Str)] {264 match held {265 case Json.Object(members) => {266 var found: Array[(Str, Str)] = []267 for member in members { found = found.concat(entries(&member[1], joined(prefix, member[0]))) }268 found269 }270 case Json.List(items) => {271 var found: Array[(Str, Str)] = []272 var index = 0273 for item in items {274 found = found.concat(entries(&item, joined(prefix, show(index))))275 index = index + 1276 }277 found278 }279 case Json.Null => [(prefix, "")]280 case Json.Boolean(truth) => [(prefix, if truth { "true" } else { "false" })]281 case Json.Number(number) => [(prefix, show(number))]282 case Json.Fractional(digits) => [(prefix, digits)]283 case Json.Text(text) => [(prefix, text)]284 }285}286287288fn joined(prefix: Str, name: Str) -> Str { if prefix.isEmpty() { name } else { prefix + ":" + name } }289290291292export fn environmentKey(name: Str, prefix: Str) -> Option[Str] {293 let head = prefix + ENVIRONMENT_SEPARATOR294 if name.length() <= head.length() || name.take(head.length()).toLower() != head.toLower() { return None }295 Some(name.drop(head.length()).replace(ENVIRONMENT_SEPARATOR, ":"))296}297