Event Ruler (chamado Ruler no restante do documento por questões de brevidade) é uma biblioteca Java que permite combinar Rules com Events . Um evento é uma lista de campos, que pode ser fornecida como pares nome/valor ou como um objeto JSON. Uma regra associa nomes de campos de eventos a listas de valores possíveis. Existem dois motivos para usar a Régua:
Conteúdo:
É mais fácil explicar com exemplos.
Um Evento é um objeto JSON. Aqui está um exemplo:
{
"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"
}
}
Você também pode ver isso como um conjunto de pares nome/valor. Por uma questão de brevidade, apresentamos apenas uma amostragem. O Ruler possui APIs para fornecer eventos no formato JSON e como pares nome/valor:
+--------------+------------------------------------------+
| name | value |
|--------------|------------------------------------------|
| source | "aws.ec2" |
| detail-type | "EC2 Instance State-change Notification" |
| detail.state | "running" |
+--------------+------------------------------------------+
Os eventos no formato JSON podem ser fornecidos na forma de uma string JSON bruta ou de um Jackson JsonNode analisado.
Todas as regras nesta seção correspondem ao evento de exemplo acima:
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
}
}
Isso corresponderá a qualquer evento com os valores fornecidos para os valores resource
, detail-type
e detail.state
, ignorando quaisquer outros campos no evento. Também corresponderia se o valor de detail.state
tivesse sido "initializing"
.
Os valores nas regras são sempre fornecidos como matrizes e correspondem se o valor no evento for um dos valores fornecidos na matriz. A referência aos resources
mostra que se o valor no evento também for uma matriz, a regra corresponderá se a interseção entre a matriz de eventos e a matriz de regras não estiver vazia.
{
"time" : [ { "prefix" : "2017-10-02" } ]
}
As correspondências de prefixo funcionam apenas em campos com valor de string.
{
"source" : [ { "prefix" : { "equals-ignore-case" : "EC2" } } ]
}
As correspondências de prefixo equals-ignore-case funcionam apenas em campos com valor de string.
{
"source" : [ { "suffix" : "ec2" } ]
}
As correspondências de sufixos funcionam apenas em campos com valor de string.
{
"source" : [ { "suffix" : { "equals-ignore-case" : "EC2" } } ]
}
As correspondências de sufixo equals-ignore-case funcionam apenas em campos com valor de string.
{
"source" : [ { "equals-ignore-case" : "EC2" } ]
}
As correspondências Equals-ignore-case funcionam apenas em campos com valor de string.
{
"source" : [ { "wildcard" : "Simple*Service" } ]
}
As correspondências de curinga funcionam apenas em campos com valor de string. Um único valor pode conter de zero a muitos caracteres curinga, mas caracteres curinga consecutivos não são permitidos. Para corresponder especificamente ao caractere asterisco, um caractere curinga pode ser escapado com uma barra invertida. Duas barras invertidas consecutivas (ou seja, uma barra invertida escapada por uma barra invertida) representam o caractere de barra invertida real. Uma barra invertida escapando de qualquer caractere diferente de asterisco ou barra invertida não é permitida.
A correspondência de qualquer coisa, menos faz o que o nome diz: corresponde a qualquer coisa, exceto ao que é fornecido na regra.
Qualquer coisa, menos funciona com string única e valores ou listas numéricas, que devem conter inteiramente strings ou inteiramente numéricos. Também pode ser aplicado a uma correspondência de prefixo, sufixo ou igual a ignorar maiúsculas e minúsculas de uma string ou lista de strings.
Único, tudo menos (string e depois numérico):
{
"detail" : {
"state" : [ { "anything-but" : "initializing" } ]
}
}
{
"detail" : {
"x-limit" : [ { "anything-but" : 123 } ]
}
}
Qualquer coisa, menos lista (strings):
{
"detail" : {
"state" : [ { "anything-but" : [ "stopped" , "overloaded" ] } ]
}
}
Qualquer coisa, menos lista (números):
{
"detail" : {
"x-limit" : [ { "anything-but" : [ 100 , 200 , 300 ] } ]
}
}
Qualquer coisa, menos prefixo:
{
"detail" : {
"state" : [ { "anything-but" : { "prefix" : "init" } } ]
}
}
Lista de tudo menos prefixos (strings):
{
"detail" : {
"state" : [ { "anything-but" : { "prefix" : [ "init" , "error" ] } } ]
}
}
Qualquer coisa, menos sufixo:
{
"detail" : {
"instance-id" : [ { "anything-but" : { "suffix" : "1234" } } ]
}
}
Lista de qualquer coisa, menos sufixos (strings):
{
"detail" : {
"instance-id" : [ { "anything-but" : { "suffix" : [ "1234" , "6789" ] } } ]
}
}
Qualquer coisa, menos ignorar:
{
"detail" : {
"state" : [ { "anything-but" : { "equals-ignore-case" : "Stopped" } } ]
}
}
Lista de casos de qualquer coisa, menos ignorar (strings):
{
"detail" : {
"state" : [ { "anything-but" : { "equals-ignore-case" : [ "Stopped" , "OverLoaded" ] } } ]
}
}
Qualquer coisa, menos curinga:
{
"detail" : {
"state" : [ { "anything-but" : { "wildcard" : "*/bin/*.jar" } } ]
}
}
Qualquer coisa, menos lista de curingas (strings):
{
"detail" : {
"state" : [ { "anything-but" : { "wildcard" : [ "*/bin/*.jar" , "*/bin/*.class" ] } } ]
}
}
{
"detail" : {
"c-count" : [ { "numeric" : [ ">" , 0 , "<=" , 5 ] } ] ,
"d-count" : [ { "numeric" : [ "<" , 10 ] } ] ,
"x-limit" : [ { "numeric" : [ "=" , 3.018e2 ] } ]
}
}
Acima, as referências a c-count
, d-count
e x-limit
ilustram a correspondência numérica e funcionam apenas com valores que são números JSON. A correspondência numérica suporta a mesma precisão e intervalo que a primitiva double
do Java que implementa o padrão IEEE 754 binary64
.
{
"detail" : {
"source-ip" : [ { "cidr" : "10.0.0.0/24" } ]
}
}
Isso também funciona com endereços IPv6.
A correspondência de existências funciona na presença ou ausência de um campo no evento JSON.
A regra abaixo corresponderá a qualquer evento que tenha um campo detail.c-count presente.
{
"detail" : {
"c-count" : [ { "exists" : true } ]
}
}
A regra abaixo corresponderá a qualquer evento que não possua o campo detail.c-count.
{
"detail" : {
"c-count" : [ { "exists" : false } ]
}
}
Nota A correspondência Exists
funciona apenas nos nós folha. Não funciona em nós intermediários.
Por exemplo, o exemplo acima para exists : false
corresponderia ao evento abaixo:
{
"detail-type" : [ "EC2 Instance State-change Notification" ] ,
"resources" : [ "arn:aws:ec2:us-east-1:123456789012:instance/i-000000aaaaaa00000" ] ,
"detail" : {
"state" : [ "initializing" , "running" ]
}
}
mas também corresponderia ao evento abaixo porque c-count
não é um nó folha:
{
"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 mostram os exemplos acima, o Ruler considera uma regra correspondente se todos os campos nomeados na regra corresponderem e considera um campo correspondente se algum dos valores de campo fornecidos corresponder, ou seja, o Ruler aplicou a lógica "E" para todos os campos por padrão sem a primitiva "E" é obrigatória .
Existem duas maneiras de alcançar os efeitos "Or":
A primitiva "$or" para permitir que o cliente descreva diretamente o relacionamento "Or" entre os campos da regra.
A régua reconhece o relacionamento "Ou" somente quando a regra atende a todas as condições abaixo:
/src/main/software/amazon/event/ruler/Constants.java#L38
por exemplo, a regra abaixo não será analisada como " Ou" relacionamento porque "numérico" e "prefixo" são palavras-chave reservadas da régua. {
"$or": [ {"numeric" : 123}, {"prefix": "abc"} ]
}
Caso contrário, o Ruler apenas trata o "$or" como um nome de campo normal, da mesma forma que outra string na regra.
Normal "Ou":
// Effect of "source" && ("metricName" || "namespace")
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{ "namespace" : [ "AWS/EC2" , "AWS/ES" ] }
]
}
Paralelo "Ou":
// 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" tem um "E" dentro
// Effect of ("source" && ("metricName" || ("metricType && "namespace") || "scope"))
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : [
{ "metricName" : [ "CPUUtilization" , "ReadLatency" ] } ,
{
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ]
} ,
{ "scope" : [ "Service" ] }
]
}
"Ou" e "E" aninhados
// 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" possivelmente já é usado como chave normal em alguns aplicativos (embora seja provavelmente raro). Para esses casos, o Ruler faz o possível para manter a compatibilidade com versões anteriores. Somente quando as 3 condições mencionadas acima forem atendidas, a régua mudará o comportamento, pois assume que sua regra realmente queria um OR e estava mal configurada até hoje. Por exemplo, a regra abaixo continuará funcionando normalmente, tratando "$or" como nome de campo normal na regra e no evento:
{
"source" : [ "aws.cloudwatch" ] ,
"$or" : {
"metricType" : [ "MetricType" ] ,
"namespace" : [ "AWS/EC2" , "AWS/ES" ]
}
}
Consulte /src/test/data/normalRulesWithOrWording.json
para obter mais exemplos de que "$or" é analisado como nome de campo normal pela Régua.
A palavra-chave "$or" como primitiva de relacionamento "Or" não deve ser projetada como campo normal em Eventos e Regras. O Ruler oferece suporte às regras legadas em que "$or" é analisado como nome de campo normal para manter a compatibilidade com versões anteriores e dar tempo para a equipe migrar seu uso "$or" herdado de seus eventos e regras como nome arquivado normal. O uso misto de "$or" como "Or" primitivo e "$or" como nome de campo normal não é suportado intencionalmente pelo Ruler para evitar a ocorrência de ambiguidades super estranhas em "$or".
Existem duas maneiras de usar a Régua. Você pode compilar várias regras em uma "Máquina" e, em seguida, usar um de seus métodos rulesForEvent()
ou métodos rulesForJSONEvent()
para verificar quais das regras correspondem a qualquer evento. A diferença entre esses dois métodos é discutida abaixo. Esta discussão usará rulesForEvent()
genericamente, exceto onde a diferença for importante.
Como alternativa, você pode usar um único método booleano estático para determinar se um evento individual corresponde a uma regra específica.
Existe um único método booleano estático Ruler.matchesRule(event, rule)
- ambos os argumentos são fornecidos como strings JSON.
NOTA: Existe outro método obsoleto chamado Ruler.matches(event, rule)
que não deve ser usado porque seus resultados são inconsistentes com rulesForJSONEvent()
e rulesForEvent()
. Consulte a documentação em Ruler.matches(event, rule)
para obter detalhes.
O tempo de correspondência não depende do número de regras. Esta é a melhor escolha se você tiver várias regras possíveis para selecionar e, especialmente, se tiver uma maneira de armazenar a máquina compilada.
O tempo de correspondência é afetado pelo grau de não determinismo causado pelas regras curinga e tudo menos curinga. O desempenho se deteriora à medida que um número crescente de prefixos de regras curinga corresponde a um evento teórico de pior caso. Para evitar isso, as regras curinga pertencentes ao mesmo campo de evento devem evitar prefixos comuns que levam ao seu primeiro caractere curinga. Se um prefixo comum for necessário, use o número mínimo de caracteres curinga e limite as sequências de caracteres repetidas que ocorrem após um caractere curinga. MachineComplexityEvaluator pode ser usado para avaliar uma máquina e determinar o grau de não determinismo ou "complexidade" (ou seja, quantos prefixos de regras curinga correspondem a um evento teórico de pior caso). Aqui estão alguns pontos de dados que mostram uma diminuição típica no desempenho para pontuações de complexidade crescentes.
É importante limitar a complexidade da máquina para proteger sua aplicação. Existem pelo menos duas estratégias diferentes para limitar a complexidade da máquina. Qual deles faz mais sentido pode depender da sua aplicação.
A estratégia nº 1 é mais ideal porque mede a complexidade real da máquina que contém todas as regras. Sempre que possível, esta estratégia deve ser utilizada. A desvantagem é que digamos que você tenha um plano de controle que permite a criação de uma regra por vez, até um número muito grande. Então, para cada uma dessas operações do plano de controle, você deverá carregar todas as regras existentes para realizar a validação. Isto pode ser muito caro. Também está sujeito a condições de corrida. A estratégia nº 2 é um compromisso. O limite usado pela estratégia nº 2 será menor que a estratégia nº 1, pois é um limite por regra. Digamos que você queira que a complexidade de uma máquina, com todas as regras adicionadas, não seja superior a 300. Então, com a estratégia nº 2, por exemplo, você poderia limitar cada máquina de regra única a uma complexidade de 10 e permitir 30 regras contendo padrões curinga . No pior caso, onde a complexidade é perfeitamente aditiva (improvável), isso levaria a uma máquina com complexidade de 300. A desvantagem é que é improvável que a complexidade seja perfeitamente aditiva e, portanto, o número de regras contendo caracteres curinga será provavelmente será limitado desnecessariamente.
Para a estratégia nº 2, dependendo de como as regras são armazenadas, pode ser necessário adicionar um atributo adicional às regras para indicar quais são não determinísticas (ou seja, contêm padrões curinga), a fim de limitar o número de regras que contêm curinga.
A seguir está um trecho de código que ilustra como limitar a complexidade para um determinado padrão, como para a estratégia 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 ;
}
}
A classe principal com a qual você interagirá implementa correspondência de regras baseada em máquina de estado. Os métodos interessantes são:
addRule()
– adiciona uma nova regra à máquinadeleteRule()
- exclui uma regra da máquinarulesForEvent()
/ rulesForJSONEvent()
– encontra as regras na máquina que correspondem a um evento Existem dois sabores: Machine
e GenericMachine<T>
. Máquina é simplesmente GenericMachine<String>
. A API refere-se ao tipo genérico como "nome", que reflete a história: a versão String foi construída primeiro e as strings armazenadas e retornadas foram consideradas nomes de regras.
Por segurança, o tipo usado para "nomear" as regras deve ser imutável. Se você alterar o conteúdo de um objeto enquanto ele estiver sendo usado como nome de regra, isso poderá interromper a operação da Régua.
Os construtores GenericMachine e Machine aceitam opcionalmente um objeto GenericMachineConfiguration, que expõe as seguintes opções de configuração.
Padrão: falso Normalmente, NameStates são reutilizados para uma determinada subsequência e padrão de chave se essa subsequência e padrão de chave tiverem sido adicionados anteriormente ou se um padrão já tiver sido adicionado para a subsequência de chave especificada. Portanto, por padrão, a reutilização do NameState é oportunista. Mas ao definir este sinalizador como verdadeiro, a reutilização de NameState será forçada para uma subsequência de chave. Isso significa que o primeiro padrão adicionado para uma subsequência de chave reutilizará um NameState se essa subsequência de chave tiver sido adicionada antes. Significa que cada subsequência de chave possui um único NameState. Isso melhora exponencialmente a utilização da memória em alguns casos, mas faz com que mais sub-regras sejam armazenadas em NameStates individuais, sobre os quais o Ruler às vezes itera, o que pode causar uma regressão modesta no desempenho do tempo de execução. O padrão é falso para compatibilidade com versões anteriores, mas provavelmente todos os aplicativos, exceto os mais sensíveis à latência, se beneficiariam com a configuração como verdadeiro.
Aqui está um exemplo simples. Considerar:
machine . addRule ( "0" , "{"key1": ["a", "b", "c"]}" ) ;
O padrão "a" cria um NameState e, então, mesmo com adicionalNameStateReuse=false, o segundo padrão ("b") e o terceiro padrão ("c") reutilizam esse mesmo NameState. Mas considere o seguinte:
machine . addRule ( "0" , "{"key1": ["a"]}" ) ;
machine . addRule ( "1" , "{"key1": ["b"]}" ) ;
machine . addRule ( "2" , "{"key1": ["c"]}" ) ;
Agora, com adicionalNameStateReuse=false, terminamos com três NameStates, porque o primeiro padrão encontrado para uma subsequência de chave em cada adição de regra criará um novo NameState. Portanto, "a", "b" e "c" obtêm seus próprios NameStates. No entanto, com adicionalNameStateReuse=true, "a" criará um novo NameState, então "b" e "c" reutilizarão esse mesmo NameState. Isso é feito armazenando que já temos um NameState para a subsequência de chave "key1".
Observe que não importa se cada addRule usa um nome de regra diferente ou o mesmo nome de regra.
Todas as formas deste método possuem o mesmo primeiro argumento, uma String que fornece o nome da Regra e é retornada por rulesForEvent()
. O restante dos argumentos fornece os pares nome/valor. Eles podem ser fornecidos em JSON como nos exemplos acima (por meio de String, Reader, InputStream ou byte[]
) ou como Map<String, List<String>>
, onde as chaves são os nomes dos campos e o os valores são a lista de possíveis correspondências; usando o exemplo acima, haveria uma chave chamada detail.state
cujo valor seria a lista contendo "initializing"
e "running"
.
Nota: Este método (e também deleteRule()
) é sincronizado, portanto, apenas um thread pode atualizar a máquina a qualquer momento.