Skip to content

Commit

Permalink
Merge pull request #213 from brharrington/v1.4.x-issue-175
Browse files Browse the repository at this point in the history
fixes #175, backport validation rules
  • Loading branch information
brharrington committed Aug 8, 2015
2 parents 2e35fc7 + f1a53a9 commit 03584fb
Show file tree
Hide file tree
Showing 28 changed files with 1,141 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ object SmallHashMap {
}

def apply[K <: AnyRef, V <: AnyRef](ts: Iterable[(K, V)]): SmallHashMap[K, V] = {
apply(ts.toSeq)
val seq = ts.toSeq
apply(seq.size, seq.iterator)
}

def apply[K <: AnyRef, V <: AnyRef](length: Int, iter: Iterator[(K, V)]): SmallHashMap[K, V] = {
Expand Down Expand Up @@ -95,6 +96,35 @@ object SmallHashMap {
}
}
}

class EntryIterator[K <: AnyRef, V <: AnyRef](map: SmallHashMap[K, V]) extends Iterator[(K, V)] {
private final val len = map.data.length
var pos = 0
skipEmptyEntries()

def hasNext: Boolean = pos < len

def next(): (K, V) = {
val t = map.data(pos).asInstanceOf[K] -> map.data(pos + 1).asInstanceOf[V]
nextEntry()
t
}

def nextEntry(): Unit = {
pos += 2
skipEmptyEntries()
}

private def skipEmptyEntries(): Unit = {
while (pos < len && map.data(pos) == null) {
pos += 2
}
}

def key: K = map.data(pos).asInstanceOf[K]

def value: V = map.data(pos + 1).asInstanceOf[V]
}
}

/**
Expand All @@ -110,8 +140,7 @@ object SmallHashMap {
* it may be a good fit.
*
* @param data array with the items
* @param dataLength number of pairs contained within the array starting at index 0. Everything
* in the array after 2 * dataLength will be ignored.
* @param dataLength number of pairs contained within the array starting at index 0.
*/
class SmallHashMap[K <: AnyRef, V <: AnyRef](val data: Array[AnyRef], dataLength: Int)
extends scala.collection.immutable.Map[K, V] {
Expand Down Expand Up @@ -157,48 +186,40 @@ class SmallHashMap[K <: AnyRef, V <: AnyRef](val data: Array[AnyRef], dataLength
}
}

def iterator: Iterator[(K, V)] = new Iterator[(K, V)] {
def find(f: (K, V) => Boolean): Option[(K, V)] = {
var i = 0
var pos = 0
def hasNext: Boolean = i < dataLength
def next(): (K, V) = {
while (data(pos) == null) {
pos += 2
while (i < data.length) {
if (data(i) != null && f(data(i).asInstanceOf[K], data(i + 1).asInstanceOf[V])) {
return Some(data(i).asInstanceOf[K] -> data(i + 1).asInstanceOf[V])
}
val t = data(pos).asInstanceOf[K] -> data(pos + 1).asInstanceOf[V]
pos += 2
i += 1
t
i += 2
}
None
}

def entriesIterator: SmallHashMap.EntryIterator[K, V] = {
new SmallHashMap.EntryIterator[K, V](this)
}

def iterator: Iterator[(K, V)] = entriesIterator

override def keysIterator: Iterator[K] = new Iterator[K] {
var i = 0
var pos = 0
def hasNext: Boolean = i < dataLength
val iter = entriesIterator
def hasNext: Boolean = iter.hasNext
def next(): K = {
while (data(pos) == null) {
pos += 2
}
val t = data(pos).asInstanceOf[K]
pos += 2
i += 1
t
val k = iter.key
iter.nextEntry()
k
}
}

override def valuesIterator: Iterator[V] = new Iterator[V] {
var i = 0
var pos = 0
def hasNext: Boolean = i < dataLength
val iter = entriesIterator
def hasNext: Boolean = iter.hasNext
def next(): V = {
while (data(pos) == null) {
pos += 2
}
val t = data(pos + 1).asInstanceOf[V]
pos += 2
i += 1
t
val v = iter.value
iter.nextEntry()
v
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.core.validation

import com.netflix.atlas.core.util.SmallHashMap
import com.typesafe.config.Config

/**
* Verifies that the tags contain a specified key. Sample config:
*
* ```
* key = name
* ```
*/
class HasKeyRule(config: Config) extends Rule {
private val key = config.getString("key")

def validate(tags: SmallHashMap[String, String]): ValidationResult = {
if (tags.contains(key)) ValidationResult.Pass else failure(s"missing '$key': ${tags.keys}")
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.core.validation

import com.typesafe.config.Config

/**
* Verifies that the keys are within the specified length bounds. Sample config:
*
* ```
* min-length = 2
* max-length = 60
* ```
*/
class KeyLengthRule(config: Config) extends TagRule {
private val minLength = config.getInt("min-length")
private val maxLength = config.getInt("max-length")

def validate(k: String, v: String): ValidationResult = {
k.length match {
case len if len > maxLength =>
failure(s"key too long: [$k] ($len > $maxLength)")
case len if len < minLength =>
failure(s"key too short: [$k] ($len < $minLength)")
case _ =>
ValidationResult.Pass
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.core.validation

import java.util.regex.Pattern

import com.typesafe.config.Config

/**
* Verifies that the keys match a specified pattern. Sample config:
*
* ```
* pattern = "^[-_.a-zA-Z0-9]{4,60}$"
* ```
*/
class KeyPatternRule(config: Config) extends TagRule {

private val pattern = Pattern.compile(config.getString("pattern"))

def validate(k: String, v: String): ValidationResult = {
if (pattern.matcher(k).matches()) ValidationResult.Pass else {
failure(s"key doesn't match pattern '$pattern': [$k]")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.core.validation

import com.netflix.atlas.core.model.TagKey
import com.netflix.atlas.core.util.SmallHashMap
import com.typesafe.config.Config

/**
* Verifies that the number of custom user tags are within a specified limit. Sample config:
*
* ```
* limit = 10
* ```
*/
class MaxUserTagsRule(config: Config) extends Rule {

private val limit = config.getInt("limit")

override def validate(tags: SmallHashMap[String, String]): ValidationResult = {
var count = 0
val iter = tags.entriesIterator
while (iter.hasNext) {
if (!TagKey.isRestricted(iter.key)) count += 1
iter.nextEntry()
}
if (count <= limit) ValidationResult.Pass else {
failure(s"too many user tags: $count > $limit")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.core.validation

import com.netflix.atlas.core.model.TagKey
import com.typesafe.config.Config

/**
* Verifies that no non-standard keys are used with a standardized prefix ("nf." and "atlas.").
*/
class NonStandardKeyRule(config: Config) extends TagRule {

override def validate(k: String, v: String): ValidationResult = {
if (TagKey.isValid(k)) ValidationResult.Pass else failure(s"non-standard key: $k")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.atlas.core.validation

import com.netflix.atlas.core.util.SmallHashMap
import com.typesafe.config.Config

/**
* Base type for validation rules.
*/
trait Rule {

private val ruleName = getClass.getSimpleName

/**
* Validates that the tag map matches the rule.
*/
def validate(tags: Map[String, String]): ValidationResult = {
tags match {
case m: SmallHashMap[String, String] => validate(m)
case _ => validate(SmallHashMap(tags))
}
}

/**
* Validates that the tag map matches the rule.
*/
def validate(tags: SmallHashMap[String, String]): ValidationResult

/**
* Helper for generating the failure response.
*/
protected def failure(reason: String): ValidationResult = {
ValidationResult.Fail(ruleName, reason)
}
}

object Rule {
def load(ruleConfigs: java.util.List[_ <: Config]): List[Rule] = {
import scala.collection.JavaConverters._
load(ruleConfigs.asScala.toList)
}

def load(ruleConfigs: List[_ <: Config]): List[Rule] = {
ruleConfigs.map { cfg =>
val cls = Class.forName(cfg.getString("class"))
cls.getConstructor(classOf[Config]).newInstance(cfg).asInstanceOf[Rule]
}
}

@scala.annotation.tailrec
def validate(tags: Map[String, String], rules: List[Rule]): ValidationResult = {
if (rules.isEmpty) ValidationResult.Pass else {
val res = rules.head.validate(tags)
if (res.isFailure) res else validate(tags, rules.tail)
}
}
}

Loading

0 comments on commit 03584fb

Please sign in to comment.