Event Ruler (llamado Ruler en el resto del documento por brevedad) es una biblioteca de Java que permite hacer coincidir reglas con eventos . Un evento es una lista de campos, que se pueden proporcionar como pares de nombre/valor o como un objeto JSON. Una regla asocia nombres de campos de eventos con listas de valores posibles. Hay dos razones para utilizar Ruler:
Contenido:
Es más fácil de explicar con el ejemplo.
Un evento es un objeto JSON. He aquí un ejemplo:
{
"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"
}
}
También puede ver esto como un conjunto de pares de nombre/valor. Por brevedad, presentamos sólo una muestra. Ruler tiene API para proporcionar eventos tanto en formato JSON como en pares de nombre/valor:
+--------------+------------------------------------------+
| name | value |
|--------------|------------------------------------------|
| source | "aws.ec2" |
| detail-type | "EC2 Instance State-change Notification" |
| detail.state | "running" |
+--------------+------------------------------------------+
Los eventos en formato JSON se pueden proporcionar en forma de una cadena JSON sin formato o un Jackson JsonNode analizado.
Todas las reglas de esta sección coinciden con el evento de ejemplo anterior:
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
}
}
Esto hará coincidir cualquier evento con los valores proporcionados para los valores de resource
, detail-type
y detail.state
, ignorando cualquier otro campo en el evento. También coincidiría si el valor de detail.state
hubiera sido "initializing"
.
Los valores de las reglas siempre se proporcionan como matrices y coinciden si el valor del evento es uno de los valores proporcionados en la matriz. La referencia a resources
muestra que si el valor del evento también es una matriz, la regla coincide si la intersección entre la matriz del evento y la matriz de reglas no está vacía.
{
"time" : [ { "prefix" : "2017-10-02" } ]
}
Las coincidencias de prefijo solo funcionan en campos con valores de cadena.
{
"source" : [ { "prefix" : { "equals-ignore-case" : "EC2" } } ]
}
Las coincidencias de prefijo igual a ignorar mayúsculas y minúsculas solo funcionan en campos con valores de cadena.
{
"source" : [ { "suffix" : "ec2" } ]
}
Las coincidencias de sufijos solo funcionan en campos con valores de cadena.
{
"source" : [ { "suffix" : { "equals-ignore-case" : "EC2" } } ]
}
Las coincidencias de sufijo igual a ignorar mayúsculas y minúsculas solo funcionan en campos con valores de cadena.
{
"source" : [ { "equals-ignore-case" : "EC2" } ]
}
Las coincidencias de igual a ignorar mayúsculas y minúsculas solo funcionan en campos con valores de cadena.
{
"source" : [ { "wildcard" : "Simple*Service" } ]
}
Las coincidencias con comodines solo funcionan en campos con valores de cadena. Un único valor puede contener de cero a muchos caracteres comodín, pero no se permiten caracteres comodín consecutivos. Para que coincida específicamente con el carácter de asterisco, se puede utilizar una barra invertida como carácter de escape para un carácter comodín. Dos barras invertidas consecutivas (es decir, una barra invertida con una barra invertida) representan el carácter de barra invertida real. No se permite una barra invertida que escape a cualquier carácter que no sea un asterisco o una barra invertida.
Cualquier cosa menos coincidencia hace lo que dice el nombre: coincide con cualquier cosa excepto lo proporcionado en la regla.
Anything-but funciona con listas o valores numéricos y de cadena única, que deben contener cadenas enteras o números enteramente numéricos. También se puede aplicar a un prefijo, sufijo o coincidencia de mayúsculas y minúsculas de una cadena o una lista de cadenas.
Cualquier cosa menos (cadena, luego numérica):
{
"detail" : {
"state" : [ { "anything-but" : "initializing" } ]
}
}
{
"detail" : {
"x-limit" : [ { "anything-but" : 123 } ]
}
}
Todo menos lista (cadenas):
{
"detail" : {
"state" : [ { "anything-but" : [ "stopped" , "overloaded" ] } ]
}
}
Todo menos lista (números):
{
"detail" : {
"x-limit" : [ { "anything-but" : [ 100 , 200 , 300 ] } ]
}
}
Todo menos prefijo:
{
"detail" : {
"state" : [ { "anything-but" : { "prefix" : "init" } } ]
}
}
Lista de todo menos prefijos (cadenas):
{
"detail" : {
"state" : [ { "anything-but" : { "prefix" : [ "init" , "error" ] } } ]
}
}
Todo menos sufijo:
{
"detail" : {
"instance-id" : [ { "anything-but" : { "suffix" : "1234" } } ]
}
}
Lista de todo menos sufijos (cadenas):
{
"detail" : {
"instance-id" : [ { "anything-but" : { "suffix" : [ "1234" , "6789" ] } } ]
}
}
Caso-todo-menos-ignorar:
{
"detail" : {
"state" : [ { "anything-but" : { "equals-ignore-case" : "Stopped" } } ]
}
}
Lista de cualquier caso menos ignorar (cadenas):
{
"detail" : {
"state" : [ { "anything-but" : { "equals-ignore-case" : [ "Stopped" , "OverLoaded" ] } } ]
}
}
Todo menos comodín:
{
"detail" : {
"state" : [ { "anything-but" : { "wildcard" : "*/bin/*.jar" } } ]
}
}
Lista de todo menos comodines (cadenas):
{
"detail" : {
"state" : [ { "anything-but" : { "wildcard" : [ "*/bin/*.jar" , "*/bin/*.class" ] } } ]
}
}
{
"detail" : {
"c-count" : [ { "numeric" : [ ">" , 0 , "<=" , 5 ] } ] ,
"d-count" : [ { "numeric" : [ "<" , 10 ] } ] ,
"x-limit" : [ { "numeric" : [ "=" , 3.018e2 ] } ]
}
}
Arriba, las referencias a c-count
, d-count
y x-limit
ilustran la coincidencia numérica y solo funcionan con valores que son números JSON. La coincidencia numérica admite la misma precisión y rango que la primitiva double
de Java que implementa el estándar IEEE 754 binary64
.
{
"detail" : {
"source-ip" : [ { "cidr" : "10.0.0.0/24" } ]
}
}
Esto también funciona con direcciones IPv6.
Existe coincidencia de trabajos sobre la presencia o ausencia de un campo en el evento JSON.
La siguiente regla coincidirá con cualquier evento que tenga presente un campo Detail.c-count.
{
"detail" : {
"c-count" : [ { "exists" : true } ]
}
}
La siguiente regla coincidirá con cualquier evento que no tenga el campo detalle.c-count.
{
"detail" : {
"c-count" : [ { "exists" : false } ]
}
}
Nota La coincidencia Exists
solo funciona en los nodos hoja. No funciona en nodos intermedios.
Como ejemplo, el ejemplo anterior exists : false
coincidiría con el siguiente evento:
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
}
}
pero también coincidiría con el evento siguiente porque c-count
no es un nodo hoja:
{
"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" } ]
}
}
Como muestran los ejemplos anteriores, Ruler considera que una regla coincide si todos los campos nombrados en la regla coinciden, y considera que un campo coincide si alguno de los valores de campo proporcionados coincide, es decir, Ruler ha aplicado la lógica "Y". a todos los campos de forma predeterminada sin que se requiera la primitiva "Y" .
Hay dos formas de alcanzar los efectos "O":
La primitiva "$o" para permitir al cliente describir directamente la relación "O" entre los campos de la regla.
La regla reconoce la relación "O" solo cuando la regla cumple todas las condiciones siguientes:
/src/main/software/amazon/event/ruler/Constants.java#L38
, por ejemplo, la siguiente regla no se analizará como " O" relación porque "numérico" y "prefijo" son palabras clave reservadas por la regla. {
"$or": [ {"numeric" : 123}, {"prefix": "abc"} ]
}
De lo contrario, Ruler simplemente trata el "$o" como un nombre de archivo normal, al igual que otras cadenas de la regla.
Normal "O":
// Effect of "source" && ("metricName" || "namespace")
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{ "namespace" : [ "AWS/EC2" , "AWS/ES" ] }
]
}
Paralelo "O":
// 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" ] }
]
}
}
"O" tiene una "Y" dentro
// Effect of ("source" && ("metricName" || ("metricType && "namespace") || "scope"))
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ]
} ,
{ "scope" : [ "Service" ] }
]
}
"O" y "Y" anidados
// 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" ] }
]
}
Es posible que "$or" ya se utilice como clave normal en algunas aplicaciones (aunque probablemente sea poco común). Para estos casos, Ruler hace todo lo posible para mantener la compatibilidad con versiones anteriores. Solo cuando se cumplan las 3 condiciones mencionadas anteriormente, la regla cambiará el comportamiento porque supone que su regla realmente quería un OR y estaba mal configurada hasta hoy. Por ejemplo, la siguiente regla seguirá funcionando como regla normal y tratará "$o" como nombre de campo normal en la regla y el evento:
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : {
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ]
}
}
Consulte /src/test/data/normalRulesWithOrWording.json
para ver más ejemplos en los que Ruler analiza "$or" como un nombre de campo normal.
La palabra clave "$o" como primitiva de relación "O" no debe diseñarse como un campo normal tanto en Eventos como en Reglas. Ruler admite las reglas heredadas donde "$or" se analiza como nombre de campo normal para mantener la compatibilidad con versiones anteriores y darle tiempo al equipo para migrar su uso heredado de "$or" fuera de sus eventos y reglas como nombre de archivo normal. Ruler no admite intencionalmente el uso combinado de "$or" como primitivo "O" y "$o" como nombre de campo normal para evitar que se produzcan ambigüedades muy incómodas sobre "$o".
Hay dos formas de utilizar Ruler. Puede compilar varias reglas en una "Máquina" y luego usar cualquiera de sus métodos rulesForEvent()
o rulesForJSONEvent()
para verificar cuál de las reglas coincide con cualquier Evento. La diferencia entre estos dos métodos se analiza a continuación. Esta discusión usará rulesForEvent()
genéricamente excepto cuando la diferencia sea importante.
Alternativamente, puede utilizar un único método booleano estático para determinar si un evento individual coincide con una regla particular.
Hay un único método booleano estático Ruler.matchesRule(event, rule)
; ambos argumentos se proporcionan como cadenas JSON.
NOTA: Existe otro método obsoleto llamado Ruler.matches(event, rule)
que no debe usarse ya que sus resultados son inconsistentes con rulesForJSONEvent()
y rulesForEvent()
. Consulte la documentación en Ruler.matches(event, rule)
para obtener más detalles.
El tiempo de coincidencia no depende de la cantidad de reglas. Esta es la mejor opción si tiene varias reglas posibles entre las que desea seleccionar y, especialmente, si tiene una forma de almacenar la máquina compilada.
El tiempo de coincidencia se ve afectado por el grado de no determinismo causado por las reglas de comodines y todo menos comodines. El rendimiento se deteriora a medida que un número cada vez mayor de prefijos de reglas comodín coinciden con el peor de los casos teóricos. Para evitar esto, las reglas de comodines pertenecientes al mismo campo de evento deben evitar prefijos comunes que conduzcan al primer carácter comodín. Si se requiere un prefijo común, utilice la cantidad mínima de caracteres comodín y limite las secuencias de caracteres repetidas que ocurren después de un carácter comodín. MachineComplexityEvaluator se puede utilizar para evaluar una máquina y determinar el grado de no determinismo o "complejidad" (es decir, cuántos prefijos de reglas comodín coinciden con un evento teórico del peor de los casos). A continuación se muestran algunos puntos de datos que muestran una disminución típica en el rendimiento al aumentar las puntuaciones de complejidad.
Es importante limitar la complejidad de la máquina para proteger su aplicación. Existen al menos dos estrategias diferentes para limitar la complejidad de la máquina. Cuál tiene más sentido puede depender de su aplicación.
La estrategia n.° 1 es más ideal porque mide la complejidad real de la máquina que contiene todas las reglas. Cuando sea posible, se debe utilizar esta estrategia. La desventaja es que, digamos, tiene un plano de control que permite la creación de una regla a la vez, hasta un número muy grande. Luego, para cada una de estas operaciones del plano de control, debes cargar todas las reglas existentes para realizar la validación. Esto podría resultar muy caro. También es propenso a las condiciones de carrera. La estrategia número 2 es un compromiso. El umbral utilizado por la estrategia n.º 2 será inferior al de la estrategia n.º 1, ya que es un umbral por regla. Digamos que desea que la complejidad de una máquina, con todas las reglas agregadas, no sea superior a 300. Luego, con la estrategia n.° 2, por ejemplo, podría limitar cada máquina de una sola regla a una complejidad de 10 y permitir 30 reglas que contengan patrones comodín. . En el peor de los casos, donde la complejidad es perfectamente aditiva (poco probable), esto llevaría a una máquina con una complejidad de 300. La desventaja es que es poco probable que la complejidad sea perfectamente aditiva, por lo que el número de reglas que contienen comodines será probablemente se limite innecesariamente.
Para la estrategia n.º 2, dependiendo de cómo se almacenen las reglas, es posible que sea necesario agregar un atributo adicional a las reglas para indicar cuáles son no deterministas (es decir, contienen patrones de comodines) para limitar el número de reglas que contienen comodines.
El siguiente es un fragmento de código que ilustra cómo limitar la complejidad de un patrón determinado, como para la estrategia 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 clase principal con la que interactuará implementa la coincidencia de reglas basada en la máquina de estados. Los métodos interesantes son:
addRule()
: agrega una nueva regla a la máquinadeleteRule()
: elimina una regla de la máquinarulesForEvent()
/ rulesForJSONEvent()
- busca las reglas en la máquina que coinciden con un evento Hay dos tipos: Machine
y GenericMachine<T>
. La máquina es simplemente GenericMachine<String>
. La API se refiere al tipo genérico como "nombre", que refleja la historia: la versión String se creó primero y las cadenas que almacenó y devolvió se consideraron nombres de reglas.
Por seguridad, el tipo utilizado para "nombrar" las reglas debe ser inmutable. Si cambia el contenido de un objeto mientras se utiliza como nombre de regla, esto puede interrumpir el funcionamiento de Ruler.
Los constructores GenericMachine y Machine aceptan opcionalmente un objeto GenericMachineConfiguration, que expone las siguientes opciones de configuración.
Valor predeterminado: falso Normalmente, los NameStates se reutilizan para una subsecuencia y patrón de clave determinados si esta subsecuencia y patrón de clave se han agregado previamente, o si ya se ha agregado un patrón para la subsecuencia de clave determinada. Por lo tanto, de forma predeterminada, la reutilización de NameState es oportunista. Pero al establecer este indicador en verdadero, se forzará la reutilización de NameState para una subsecuencia clave. Esto significa que el primer patrón que se agregue para una subsecuencia clave reutilizará un NameState si esa subsecuencia clave se agregó antes. Lo que significa que cada subsecuencia clave tiene un único NameState. Esto mejora exponencialmente la utilización de la memoria en algunos casos, pero conduce a que se almacenen más subreglas en NameStates individuales, sobre los cuales Ruler a veces itera, lo que puede provocar una modesta regresión del rendimiento en tiempo de ejecución. El valor predeterminado es falso para compatibilidad con versiones anteriores, pero probablemente todas las aplicaciones, excepto las más sensibles a la latencia, se beneficiarían al establecerlo en verdadero.
He aquí un ejemplo sencillo. Considerar:
machine . addRule ( "0" , "{"key1": ["a", "b", "c"]}" ) ;
El patrón "a" crea un NameState y luego, incluso con adicionalNameStateReuse=false, el segundo patrón ("b") y el tercer patrón ("c") reutilizan ese mismo NameState. Pero considere lo siguiente en su lugar:
machine . addRule ( "0" , "{"key1": ["a"]}" ) ;
machine . addRule ( "1" , "{"key1": ["b"]}" ) ;
machine . addRule ( "2" , "{"key1": ["c"]}" ) ;
Ahora, con adicionalNameStateReuse=false, terminamos con tres NameStates, porque el primer patrón encontrado para una subsecuencia clave en cada adición de regla creará un nuevo NameState. Entonces, "a", "b" y "c" obtienen sus propios NameStates. Sin embargo, con adicionalNameStateReuse=true, "a" creará un nuevo NameState, luego "b" y "c" reutilizarán este mismo NameState. Esto se logra almacenando que ya tenemos un NameState para la subsecuencia clave "key1".
Tenga en cuenta que no importa si cada addRule usa un nombre de regla diferente o el mismo nombre de regla.
Todas las formas de este método tienen el mismo primer argumento, una Cadena que proporciona el nombre de la Regla y es devuelta por rulesForEvent()
. El resto de los argumentos proporcionan los pares nombre/valor. Se pueden proporcionar en JSON como en los ejemplos anteriores (a través de String, Reader, InputStream o byte[]
), o como Map<String, List<String>>
, donde las claves son los nombres de los campos y el los valores son la lista de posibles coincidencias; Usando el ejemplo anterior, habría una clave llamada detail.state
cuyo valor sería la lista que contiene "initializing"
y "running"
.
Nota: Este método (y también deleteRule()
) está sincronizado, por lo que solo un subproceso puede actualizar la máquina en cualquier momento.