Boolli
L'interprete di espressioni booleane
Boolli è un interprete di espressioni booleane open-source scritto in C#
e disponibile come libreria su NuGet.
Cosa può fare Boolli? Rispondere true
o false
data un’espressione booleana!
Per Boolli, un’espressione booleana è una stringa generata secondo questa grammatica EBNF:
expr: factor((and | or) factor)*
factor: (not)* factor | boolean | LPAR expr RPAR
boolean: true | false | 0 | 1
Come software Boolli è composto da tre moduli:
- Un lexer
- Un parser
- Un interprete
…e la struttura dati alla base del suo funzionamento è l’Abstract Syntax Tree.
Perché Boolli può esserti utile?
- A scopo didattico: per capire come funziona un semplice interprete. Noi informatici li diamo per scontati, ma sono molto interessanti!
- Se sostituisci, via codice, i token booleani con delle
Func<bool>
(o anche delleFunc<Task<bool>>
). Uno use case è quello di definire un semplice sistema di regole.
Valutare un’espressione booleana
var boolli = new Evaluator();
string booleanExpression = "not true and false or (true and false)";
bool result = boolli.EvaluateBooleanExpression(booleanExpression);
…puoi anche usare 0
al posto di false
e 1
al posto di true
var boolli = new Evaluator();
string booleanExpression = "not 1 and 0 or (true and false)";
bool result = boolli.EvaluateBooleanExpression(booleanExpression);
Valutare un’espressione di Func<bool>
var boolli = new Evaluator();
bool result = boolli.EvaluateFuncOfBoolExpression(
"f1 and f2",
new NamedBooleanFunction[]
{
new NamedBooleanFunction("f1", () => true),
new NamedBooleanFunction("f2", () => false),
});
…anche in versione Async
, con un’espressione di Func<Task<bool>
var boolli = new Evaluator();
bool result = await boolli.EvaluateFuncOfBoolExpressionAsync(
"f3 and f4",
new NamedAsyncBooleanFunction[]
{
new NamedAsyncBooleanFunction(
"f3",
async () => { await Task.Delay(100); return true; }),
new NamedAsyncBooleanFunction(
"f4",
async () => { await Task.Delay(100); return true; })
});
Scenario reale
Ho sviluppato un semplicissimo scenario a scopo dimostrativo (se vuoi lo puoi migliorare tramite GitHub, fa sempre piacere!): un sistema di creazione di alert basato su regole configurabili interpretate da Boolli.
Qui su GitHub puoi trovare il codice sorgente dell’esempio specifico che sto per approfondire.
L’idea è quella di generare degli alert testuali basati su dati di monitoraggio hardware, come ad esempio la percentuali di CPU utilizzata in un dato momento. Ogni alert è generato da una regola che può essere scritta come una stringa - e quindi configurabile anche da una GUI - come ad esempio: CPUPercentage and UsedRAMGigaBytes
.
In questo caso CPUPercentage
è una funzione booleana che valuta la percentuale di CPU di un dato record secondo una soglia. Ad esempio può ritornare true
se il valore supera il 90%.
Per completare l’esempio, ho cercato di immaginare uno scenario il più possibile reale. Abbiamo bisogno di questi elementi per far funzionare tutto:
- Una lista di regole (
Rules
) - Dei dati di monitoraggio (
MonitoringData
) che in questo caso ho generato casualmente conFakeMonitoringDataRepository
- Un generatore di alert che utilizzi un interprete di espressioni booleane (Boolli)
L’anatomia di una regola
new Rule()
{
RuleName = "RAM full and CPU busy last minute",
BooleanExpression = $"{Metrics.CPUPercentage} and {Metrics.UsedRAMGigaBytes}",
ResultingAlertGravity = AlertGravity.Critical,
MessageFormat = "Attention! Last minute CPU ({0}%) and RAM ({1} GB) are very critical! (Last value collection time: {2})",
TimeFrameMinutes = 1,
DataEvaluationFunctionDescription = new DataEvaluationFunctionDescription[]
{
new DataEvaluationFunctionDescription()
{
Metric = Metrics.CPUPercentage,
Threshold = 90
},
new DataEvaluationFunctionDescription()
{
Metric = Metrics.UsedRAMGigaBytes,
Threshold = 7
}
}
}
Le property salienti sono:
BooleanExpression
: con questa property specifichi un’espressione booleana che deve essere parsata ed eseguita da Boolli utilizzando i nomi di funzione specificati inDataEvaluationFunctionDescription
MessageFormat
: per comporre il messaggio testuale di alert utilizzando come parametri l’output delle funzioni di valutazione dei record
In questo esempio specifico ho definito la classe DataEvaluationFunctionDescription
con lo scopo di parametrizzare ogni tipo di metrica da valutare e la sua soglia specifica.
Il generatore di alert
Questa è la parte più complessa di questo esempio, perché qui si mette tutto insieme: le regole, Boolli e i dati di monitoraggio.
public List<Alert> GenerateAlerts(MonitoringData[] monitoringData)
{
var alerts = new List<Alert>();
var boolli = new Evaluator();
foreach (var rule in _alertGenerationRules)
{
// I need object for String.Format
var lastValues = new Dictionary<Metrics, MonitoringData>();
var namedBooleanFunctions = rule.DataEvaluationFunctionDescription.Select(d => new NamedBooleanFunction(
d.Metric.ToString(),
() =>
{
(bool IsCritical, MonitoringData data) = ValueCritical(
monitoringData,
DateTime.Now.AddMinutes(-rule.TimeFrameMinutes),
d.Metric,
d.Threshold);
if (IsCritical && data != null)
{
lastValues[d.Metric] = data;
}
return IsCritical;
})).ToArray();
bool alertShouldBeGenerated = boolli.EvaluateFuncOfBoolExpression(
rule.BooleanExpression,
namedBooleanFunctions);
if (alertShouldBeGenerated)
{
var stringFormatParameters = new List<object>();
foreach (var lastValue in lastValues)
{
stringFormatParameters.Add((int)lastValue.Value.MetricValue);
}
if (stringFormatParameters.Count < Enum.GetNames(typeof(Metrics)).Length)
{
for (int i = 0; i < Enum.GetNames(typeof(Metrics)).Length; ++i)
stringFormatParameters.Add("-");
}
stringFormatParameters.Add(lastValues.Values.OrderByDescending(v => v.CollectionTime).FirstOrDefault().CollectionTime);
alerts.Add(new Alert()
{
GeneratingRuleName = rule.RuleName,
AlertGravity = rule.ResultingAlertGravity,
GenerationDate = DateTime.Now,
Message = string.Format(rule.MessageFormat, stringFormatParameters.ToArray())
});
}
}
return alerts;
}
In questo frangente c’è un ciclo che valuta ogni regola ed il punto di giunzione con Boolli è proprio qui: le DataEvaluationFunctionDescription
diventano NamedBooleanFunction
per poter essere utilizzate dalla libreria.
bool alertShouldBeGenerated = boolli.EvaluateFuncOfBoolExpression(
rule.BooleanExpression,
namedBooleanFunctions);
La funzione concreta dietro le quinte
Dopo aver letto tutto questo potrete pensare: ok, ma chi è che materialmente valuta quando i dati di monitoraggio devono generare un alert oppure no?
private (bool IsCritical, MonitoringData Data) ValueCritical(
MonitoringData[] monitoringData,
DateTime startingDate,
Metrics metric,
double threshold)
{
foreach (var data in monitoringData
.Where(d => d.CollectionTime > startingDate)
.OrderByDescending(d => d.CollectionTime))
{
if (data.Metric == metric && data.MetricValue > threshold)
{
return (true, data);
}
}
return (false, null);
}
Il codice sopra è l’If
sostanziale che sta alla base di tutto.
Contribuire
Il mondo dell’open source è meraviglioso proprio perché ognuno può dare il proprio contributo ed aiutare chissà chi nel mondo!
Il codice di Boolli è interamente open source su GitHub ed è aperta alla collaborazione da parte di tutti!