Event Ruler (appelé Ruler dans le reste de la documentation par souci de concision) est une bibliothèque Java qui permet de faire correspondre Rules to Events . Un événement est une liste de champs, qui peuvent être donnés sous forme de paires nom/valeur ou sous forme d'objet JSON. Une règle associe les noms de champs d'événements à des listes de valeurs possibles. Il y a deux raisons d'utiliser Ruler :
Contenu:
C'est plus simple à expliquer par un exemple.
Un événement est un objet JSON. Voici un exemple :
{
"version" : "0" ,
"id" : "ddddd4-aaaa-7777-4444-345dd43cc333" ,
"detail-type" : "EC2 Instance State-change Notification" ,
"source" : "aws.ec2" ,
"account" : "012345679012" ,
"time" : "2017-10-02T16:24:49Z" ,
"region" : "us-east-1" ,
"resources" : [
"arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000"
] ,
"detail" : {
"c-count" : 5 ,
"d-count" : 3 ,
"x-limit" : 301.8 ,
"source-ip" : "10.0.0.33" ,
"instance-id" : "i-000000aaaaaa00000" ,
"state" : "running"
}
}
Vous pouvez également voir cela comme un ensemble de paires nom/valeur. Par souci de concision, nous ne présentons qu’un échantillon. Ruler dispose d'API pour fournir des événements à la fois sous forme JSON et sous forme de paires nom/valeur :
+--------------+------------------------------------------+
| name | value |
|--------------|------------------------------------------|
| source | "aws.ec2" |
| detail-type | "EC2 Instance State-change Notification" |
| detail.state | "running" |
+--------------+------------------------------------------+
Les événements au format JSON peuvent être fournis sous la forme d'une chaîne JSON brute ou d'un Jackson JsonNode analysé.
Les règles de cette section correspondent toutes à l'exemple d'événement ci-dessus :
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
}
}
Cela fera correspondre n'importe quel événement avec les valeurs fournies pour les valeurs resource
, detail-type
et detail.state
, en ignorant tous les autres champs de l'événement. Cela correspondrait également si la valeur de detail.state
avait été "initializing"
.
Les valeurs des règles sont toujours fournies sous forme de tableaux et correspondent si la valeur de l'événement est l'une des valeurs fournies dans le tableau. La référence aux resources
montre que si la valeur de l'événement est également un tableau, la règle correspond si l'intersection entre le tableau d'événements et le tableau de règles n'est pas vide.
{
"time" : [ { "prefix" : "2017-10-02" } ]
}
Les correspondances de préfixe ne fonctionnent que sur les champs de valeur chaîne.
{
"source" : [ { "prefix" : { "equals-ignore-case" : "EC2" } } ]
}
Les correspondances de préfixe égal à la casse ne fonctionnent que sur les champs de valeur chaîne.
{
"source" : [ { "suffix" : "ec2" } ]
}
Les correspondances de suffixe ne fonctionnent que sur les champs de valeur chaîne.
{
"source" : [ { "suffix" : { "equals-ignore-case" : "EC2" } } ]
}
Les correspondances de suffixe égal à la casse ne fonctionnent que sur les champs de valeur chaîne.
{
"source" : [ { "equals-ignore-case" : "EC2" } ]
}
Les correspondances égales à la casse ne fonctionnent que sur les champs de valeur chaîne.
{
"source" : [ { "wildcard" : "Simple*Service" } ]
}
Les correspondances génériques ne fonctionnent que sur les champs de valeur chaîne. Une valeur unique peut contenir de zéro à plusieurs caractères génériques, mais les caractères génériques consécutifs ne sont pas autorisés. Pour faire correspondre spécifiquement le caractère astérisque, un caractère générique peut être échappé avec une barre oblique inverse. Deux barres obliques inverses consécutives (c'est-à-dire une barre oblique inverse suivie d'une barre oblique inverse) représentent le caractère barre oblique inverse réelle. Une barre oblique inverse échappant à tout caractère autre qu'un astérisque ou une barre oblique inverse n'est pas autorisée.
Tout sauf la correspondance fait ce que son nom indique : correspond à tout sauf à ce qui est fourni dans la règle.
Tout sauf fonctionne avec une chaîne unique et des valeurs ou des listes numériques, qui doivent contenir entièrement des chaînes ou entièrement des valeurs numériques. Il peut également être appliqué à un préfixe, un suffixe ou une correspondance égale à la casse d'une chaîne ou d'une liste de chaînes.
Unique tout sauf (chaîne, puis numérique) :
{
"detail" : {
"state" : [ { "anything-but" : "initializing" } ]
}
}
{
"detail" : {
"x-limit" : [ { "anything-but" : 123 } ]
}
}
Tout sauf liste (chaînes) :
{
"detail" : {
"state" : [ { "anything-but" : [ "stopped" , "overloaded" ] } ]
}
}
Tout sauf une liste (chiffres) :
{
"detail" : {
"x-limit" : [ { "anything-but" : [ 100 , 200 , 300 ] } ]
}
}
Tout sauf le préfixe :
{
"detail" : {
"state" : [ { "anything-but" : { "prefix" : "init" } } ]
}
}
Liste tout sauf des préfixes (chaînes) :
{
"detail" : {
"state" : [ { "anything-but" : { "prefix" : [ "init" , "error" ] } } ]
}
}
Tout sauf le suffixe :
{
"detail" : {
"instance-id" : [ { "anything-but" : { "suffix" : "1234" } } ]
}
}
Liste tout sauf suffixes (chaînes) :
{
"detail" : {
"instance-id" : [ { "anything-but" : { "suffix" : [ "1234" , "6789" ] } } ]
}
}
Tout sauf ignorer la casse :
{
"detail" : {
"state" : [ { "anything-but" : { "equals-ignore-case" : "Stopped" } } ]
}
}
Liste tout sauf ignorer la casse (chaînes) :
{
"detail" : {
"state" : [ { "anything-but" : { "equals-ignore-case" : [ "Stopped" , "OverLoaded" ] } } ]
}
}
Tout sauf un caractère générique :
{
"detail" : {
"state" : [ { "anything-but" : { "wildcard" : "*/bin/*.jar" } } ]
}
}
Liste tout sauf des caractères génériques (chaînes) :
{
"detail" : {
"state" : [ { "anything-but" : { "wildcard" : [ "*/bin/*.jar" , "*/bin/*.class" ] } } ]
}
}
{
"detail" : {
"c-count" : [ { "numeric" : [ ">" , 0 , "<=" , 5 ] } ] ,
"d-count" : [ { "numeric" : [ "<" , 10 ] } ] ,
"x-limit" : [ { "numeric" : [ "=" , 3.018e2 ] } ]
}
}
Ci-dessus, les références à c-count
, d-count
et x-limit
illustrent la correspondance numérique et fonctionnent uniquement avec des valeurs qui sont des nombres JSON. La correspondance numérique prend en charge la même précision et la même plage que double
primitive de Java qui implémente la norme binary64
IEEE 754.
{
"detail" : {
"source-ip" : [ { "cidr" : "10.0.0.0/24" } ]
}
}
Cela fonctionne également avec les adresses IPv6.
Existe correspondant fonctionne sur la présence ou l'absence d'un champ dans l'événement JSON.
La règle ci-dessous correspondra à tout événement comportant un champ detail.c-count.
{
"detail" : {
"c-count" : [ { "exists" : true } ]
}
}
La règle ci-dessous correspondra à tout événement qui n'a pas de champ detail.c-count.
{
"detail" : {
"c-count" : [ { "exists" : false } ]
}
}
Remarque La correspondance Exists
ne fonctionne que sur les nœuds feuilles. Cela ne fonctionne pas sur les nœuds intermédiaires.
À titre d'exemple, l'exemple ci-dessus pour exists : false
correspondrait à l'événement ci-dessous :
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
}
}
mais correspondrait également à l'événement ci-dessous car c-count
n'est pas un nœud feuille :
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
"c-count" : {
"c1" : 100
}
}
}
{
"time" : [ { "prefix" : "2017-10-02" } ] ,
"detail" : {
"state" : [ { "anything-but" : "initializing" } ] ,
"c-count" : [ { "numeric" : [ ">" , 0 , "<=" , 5 ] } ] ,
"d-count" : [ { "numeric" : [ "<" , 10 ] } ] ,
"x-limit" : [ { "anything-but" : [ 100 , 200 , 300 ] } ] ,
"source-ip" : [ { "cidr" : "10.0.0.0/8" } ]
}
}
Comme le montrent les exemples ci-dessus, Ruler considère qu'une règle correspond si tous les champs nommés dans la règle correspondent, et il considère qu'un champ correspond si l'une des valeurs de champ fournies correspond, c'est-à-dire que Ruler a appliqué la logique "Et". à tous les champs par défaut sans la primitive "Et" est requise .
Il existe deux manières d'accéder aux effets « Ou » :
La primitive « $ou » pour permettre au client de décrire directement la relation « Ou » entre les champs de la règle.
La règle reconnaît la relation « Ou » uniquement lorsque la règle remplit toutes les conditions ci-dessous :
/src/main/software/amazon/event/ruler/Constants.java#L38
par exemple, la règle ci-dessous ne sera pas analysée comme " Ou" relation car "numérique" et "préfixe" sont des mots-clés réservés par la règle. {
"$or": [ {"numeric" : 123}, {"prefix": "abc"} ]
}
Sinon, Ruler traite simplement le "$or" comme un nom de fichier normal de la même manière que les autres chaînes de la règle.
"Ou" normal :
// Effect of "source" && ("metricName" || "namespace")
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{ "namespace" : [ "AWS/EC2" , "AWS/ES" ] }
]
}
"Ou" parallèle :
// Effect of ("metricName" || "namespace") && ("detail.source" || "detail.detail-type")
{
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{ "namespace" : [ "AWS/EC2" , "AWS/ES" ] }
] ,
"detail" : {
"$or" : [
{ "source" : [ "aws.cloudwatch" ] } ,
{ "detail-type" : [ "CloudWatch Alarm State Change" ] }
]
}
}
"Ou" contient un "Et" à l'intérieur
// Effect of ("source" && ("metricName" || ("metricType && "namespace") || "scope"))
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ]
} ,
{ "scope" : [ "Service" ] }
]
}
"Ou" et "Et" imbriqués
// Effect of ("source" && ("metricName" || ("metricType && "namespace" && ("metricId" || "spaceId")) || "scope"))
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ] ,
"$or" : [
{ "metricId" : [ 1234 ] } ,
{ "spaceId" : [ 1000 ] }
]
} ,
{ "scope" : [ "Service" ] }
]
}
"$or" est peut-être déjà utilisé comme clé normale dans certaines applications (bien que ce soit probablement rare). Dans ces cas, Ruler fait de son mieux pour maintenir la compatibilité ascendante. Ce n'est que lorsque les 3 conditions mentionnées ci-dessus que la règle changera de comportement, car elle suppose que votre règle voulait vraiment un OU et qu'elle a été mal configurée jusqu'à aujourd'hui. Par exemple, la règle ci-dessous continuera à fonctionner comme une règle normale en traitant « $or » comme nom de champ normal dans la règle et l'événement :
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : {
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ]
}
}
Reportez-vous à /src/test/data/normalRulesWithOrWording.json
pour plus d'exemples montrant que « $or » est analysé comme nom de champ normal par Ruler.
Le mot-clé « $or » en tant que primitive de relation « Ou » ne doit pas être conçu comme un champ normal à la fois dans les événements et dans les règles. Ruler prend en charge les règles héritées où « $or » est analysé comme nom de champ normal pour maintenir la compatibilité ascendante et donner le temps à l'équipe de migrer son utilisation héritée de « $or » loin de ses événements et règles en tant que nom de fichier normal. L'utilisation mixte de "$or" comme primitive "Or" et de "$or" comme nom de champ normal n'est pas prise en charge intentionnellement par Ruler pour éviter que des ambiguïtés très gênantes sur "$or" ne se produisent.
Il existe deux manières d'utiliser Ruler. Vous pouvez compiler plusieurs règles dans une « Machine », puis utiliser l'une de ses méthodes rulesForEvent()
ou rulesForJSONEvent()
pour vérifier laquelle des règles correspond à un événement. La différence entre ces deux méthodes est discutée ci-dessous. Cette discussion utilisera rulesForEvent()
de manière générique, sauf lorsque la différence compte.
Vous pouvez également utiliser une seule méthode booléenne statique pour déterminer si un événement individuel correspond à une règle particulière.
Il existe une seule méthode booléenne statique Ruler.matchesRule(event, rule)
- les deux arguments sont fournis sous forme de chaînes JSON.
REMARQUE : Il existe une autre méthode obsolète appelée Ruler.matches(event, rule)
qui ne doit pas être utilisée car ses résultats sont incohérents avec rulesForJSONEvent()
et rulesForEvent()
. Consultez la documentation sur Ruler.matches(event, rule)
pour plus de détails.
Le temps de correspondance ne dépend pas du nombre de règles. C'est le meilleur choix si vous souhaitez sélectionner plusieurs règles possibles, et surtout si vous disposez d'un moyen de stocker la machine compilée.
Le temps de correspondance est affecté par le degré de non-déterminisme provoqué par les règles génériques et tout sauf génériques. Les performances se détériorent à mesure qu'un nombre croissant de préfixes de règles génériques correspondent à un événement théorique du pire des cas. Pour éviter cela, les règles génériques relatives au même champ d'événement doivent éviter les préfixes courants menant à leur premier caractère générique. Si un préfixe commun est requis, utilisez le nombre minimum de caractères génériques et limitez les séquences de caractères répétitives qui se produisent après un caractère générique. MachineComplexityEvaluator peut être utilisé pour évaluer une machine et déterminer le degré de non-déterminisme, ou de « complexité » (c'est-à-dire combien de préfixes de règles génériques correspondent à un événement théorique du pire des cas). Voici quelques points de données montrant une diminution typique des performances pour des scores de complexité croissants.
Il est important de limiter la complexité des machines pour protéger votre application. Il existe au moins deux stratégies différentes pour limiter la complexité des machines. Celui qui a le plus de sens peut dépendre de votre application.
La stratégie n°1 est plus idéale dans la mesure où elle mesure la complexité réelle de la machine contenant toutes les règles. Lorsque cela est possible, cette stratégie doit être utilisée. L'inconvénient est que, disons que vous disposez d'un plan de contrôle qui permet la création d'une règle à la fois, jusqu'à un très grand nombre. Ensuite pour chacune de ces opérations du plan de contrôle, vous devez charger toutes les règles existantes pour effectuer la validation. Cela pourrait coûter très cher. Il est également sujet aux conditions de concurrence. La stratégie n°2 est un compromis. Le seuil utilisé par la stratégie n°2 sera inférieur à celui de la stratégie n°1 puisqu'il s'agit d'un seuil par règle. Supposons que vous souhaitiez que la complexité d'une machine, avec toutes les règles ajoutées, ne dépasse pas 300. Ensuite, avec la stratégie n°2, par exemple, vous pourriez limiter chaque machine à règle unique à une complexité de 10 et autoriser 30 règles contenant des modèles génériques. . Dans le pire des cas où la complexité est parfaitement additive (peu probable), cela conduirait à une machine d'une complexité de 300. L'inconvénient est qu'il est peu probable que la complexité soit parfaitement additive, et donc le nombre de règles contenant des caractères génériques augmentera. sera probablement limitée inutilement.
Pour la stratégie n°2, selon la manière dont les règles sont stockées, un attribut supplémentaire peut devoir être ajouté aux règles pour indiquer lesquelles sont non déterministes (c'est-à-dire contiennent des modèles de caractères génériques) afin de limiter le nombre de règles contenant des caractères génériques.
Ce qui suit est un extrait de code illustrant comment limiter la complexité d'un modèle donné, comme pour la stratégie n°2.
public class Validate {
private void validate ( String pattern , MachineComplexityEvaluator machineComplexityEvaluator ) {
// If we cannot compile, then return exception.
List < Map < String , List < Patterns >>> compilationResult = Lists . newArrayList ();
try {
compilationResult . addAll ( JsonRuleCompiler . compile ( pattern ));
} catch ( Exception e ) {
InvalidPatternException internalException =
EXCEPTION_FACTORY . invalidPatternException ( e . getLocalizedMessage ());
throw ExceptionMapper . mapToModeledException ( internalException );
}
// Validate wildcard patterns. Look for wildcard patterns out of all patterns that have been used.
Machine machine = new Machine ();
int i = 0 ;
for ( Map < String , List < Patterns >> rule : compilationResult ) {
if ( containsWildcard ( rule )) {
// Add rule to machine for complexity evaluation.
machine . addPatternRule ( Integer . toString (++ i ), rule );
}
}
// Machine has all rules containing wildcard match types. See if the complexity is under the limit.
int complexity = machine . evaluateComplexity ( machineComplexityEvaluator );
if ( complexity > MAX_MACHINE_COMPLEXITY ) {
InvalidPatternException internalException = EXCEPTION_FACTORY . invalidPatternException ( "Rule is too complex" );
throw ExceptionMapper . mapToModeledException ( internalException );
}
}
private boolean containsWildcard ( Map < String , List < Patterns >> rule ) {
for ( List < Patterns > fieldPatterns : rule . values ()) {
for ( Patterns fieldPattern : fieldPatterns ) {
if ( fieldPattern . type () == WILDCARD || fieldPattern . type () == ANYTHING_BUT_WILDCARD ) {
return true ;
}
}
}
return false ;
}
}
La classe principale avec laquelle vous interagirez implémente la correspondance de règles basée sur la machine à états. Les méthodes intéressantes sont :
addRule()
- ajoute une nouvelle règle à la machinedeleteRule()
- supprime une règle de la machinerulesForEvent()
/ rulesForJSONEvent()
- recherche les règles de la machine qui correspondent à un événement Il existe deux versions : Machine
et GenericMachine<T>
. Machine est simplement GenericMachine<String>
. L'API fait référence au type générique sous le nom de « nom », ce qui reflète l'historique : la version String a été construite en premier et les chaînes qu'elle stockait et renvoyait étaient considérées comme des noms de règles.
Pour des raisons de sécurité, le type utilisé pour « nommer » les règles doit être immuable. Si vous modifiez le contenu d'un objet alors qu'il est utilisé comme nom de règle, cela peut interrompre le fonctionnement de Ruler.
Les constructeurs GenericMachine et Machine acceptent éventuellement un objet GenericMachineConfiguration, qui expose les options de configuration suivantes.
Par défaut : false Normalement, les NameStates sont réutilisés pour une sous-séquence de clé et un modèle donnés si cette sous-séquence de clé et ce modèle ont été précédemment ajoutés, ou si un modèle a déjà été ajouté pour la sous-séquence de clé donnée. Par conséquent, par défaut, la réutilisation de NameState est opportuniste. Mais en définissant cet indicateur sur true, la réutilisation de NameState sera forcée pour une sous-séquence de clé. Cela signifie que le premier modèle ajouté pour une sous-séquence de clé réutilisera un NameState si cette sous-séquence de clé a déjà été ajoutée. Cela signifie que chaque sous-séquence de clé a un seul NameState. Cela améliore l'utilisation de la mémoire de manière exponentielle dans certains cas, mais conduit à stocker davantage de sous-règles dans des NameStates individuels, sur lesquels Ruler itère parfois, ce qui peut entraîner une modeste régression des performances d'exécution. La valeur par défaut est false pour des raisons de compatibilité ascendante, mais il est probable que toutes les applications, à l'exception des applications les plus sensibles à la latence, gagneraient à définir cette valeur sur true.
Voici un exemple simple. Considérer:
machine . addRule ( "0" , "{"key1": ["a", "b", "c"]}" ) ;
Le modèle "a" crée un NameState, puis, même avec additionNameStateReuse=false, le deuxième modèle ("b") et le troisième modèle ("c") réutilisent ce même NameState. Mais considérez plutôt ce qui suit :
machine . addRule ( "0" , "{"key1": ["a"]}" ) ;
machine . addRule ( "1" , "{"key1": ["b"]}" ) ;
machine . addRule ( "2" , "{"key1": ["c"]}" ) ;
Maintenant, avec additionnelNameStateReuse=false, nous nous retrouvons avec trois NameStates, car le premier modèle rencontré pour une sous-séquence de clé à chaque ajout de règle créera un nouveau NameState. Ainsi, "a", "b" et "c" ont tous leurs propres NameStates. Cependant, avec additionnelNameStateReuse=true, "a" créera un nouveau NameState, puis "b" et "c" réutiliseront ce même NameState. Ceci est accompli en stockant le fait que nous avons déjà un NameState pour la sous-séquence de clé « key1 ».
Notez que peu importe si chaque addRule utilise un nom de règle différent ou le même nom de règle.
Toutes les formes de cette méthode ont le même premier argument, une chaîne qui fournit le nom de la règle et est renvoyée par rulesForEvent()
. Le reste des arguments fournit les paires nom/valeur. Ils peuvent être fournis en JSON comme dans les exemples ci-dessus (via un String, un Reader, un InputStream ou byte[]
), ou sous forme de Map<String, List<String>>
, où les clés sont les noms de champs et les les valeurs sont la liste des correspondances possibles ; en utilisant l'exemple ci-dessus, il y aurait une clé nommée detail.state
dont la valeur serait la liste contenant "initializing"
et "running"
.
Remarque : Cette méthode (et également deleteRule()
) est synchronisée, donc un seul thread peut mettre à jour la machine à tout moment.