f) throws Exception;
+
+ /**
+ * Executes the runnable `f` and records the time taken.
+ *
+ * @param f
+ * Function to execute and measure the execution time.
+ */
+ void record(Runnable f);
+
+ /** The number of times that record has been called since this timer was created. */
+ long count();
+
+ /** The total time in nanoseconds of all recorded events since this timer was created. */
+ long totalTime();
+}
diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/ValueFunction.java b/spectator-api/src/main/java/com/netflix/spectator/api/ValueFunction.java
new file mode 100644
index 000000000..25caad0ab
--- /dev/null
+++ b/spectator-api/src/main/java/com/netflix/spectator/api/ValueFunction.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+/**
+ * Function to extract a double value from an object.
+ */
+public interface ValueFunction {
+ /**
+ * Returns a double value based on the object {@code ref}.
+ *
+ * @param ref
+ * An object to use for extracting the value.
+ * @return
+ * Double value based on the object.
+ */
+ double apply(Object ref);
+}
diff --git a/spectator-api/src/main/java/com/netflix/spectator/api/package-info.java b/spectator-api/src/main/java/com/netflix/spectator/api/package-info.java
new file mode 100644
index 000000000..e413c364d
--- /dev/null
+++ b/spectator-api/src/main/java/com/netflix/spectator/api/package-info.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2014 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.
+ */
+
+/**
+ * Primary interfaces for working with spectator. To get started, here is a small code sample:
+ *
+ *
+ * Server s = new Server(Spectator.registry());
+ *
+ * class Server {
+ * private final ExtendedRegistry registry;
+ * private final Id requestCountId;
+ * private final Timer requestLatency;
+ * private final DistributionSummary responseSizes;
+ *
+ * public Server(ExtendedRegistry registry) {
+ * this.registry = registry;
+ * requestCountId = registry.createId("server.requestCount");
+ * requestLatency = registry.timer("server.requestLatency");
+ * responseSizes = registry.distributionSummary("server.responseSizes");
+ * registry.methodValue("server.numConnections", this, "getNumConnections");
+ * }
+ *
+ * public Response handle(Request req) {
+ * final long s = System.nanoTime();
+ * try {
+ * Response res = doSomething(req);
+ *
+ * final Id cntId = requestCountId
+ * .withTag("country", req.country())
+ * .withTag("status", res.status());
+ * registry.counter(cntId).increment();
+ *
+ * responseSizes.record(res.body().size());
+ *
+ * return res;
+ * } catch (Exception e) {
+ * final Id cntId = requestCountId
+ * .withTag("country", req.country())
+ * .withTag("status", "exception")
+ * .withTag("error", e.getClass().getSimpleName());
+ * registry.counter(cntId).increment();
+ * throw e;
+ * } finally {
+ * requestLatency.record(System.nanoTime() - s, TimeUnit.NANOSECONDS);
+ * }
+ * }
+ *
+ * public int getNumConnections() {
+ * // however we determine the current number of connections on the server
+ * }
+ * }
+ *
+ *
+ * The main classes you will need to understand:
+ *
+ *
+ * - {@link com.netflix.spectator.api.Spectator}: static entrypoint to access the registry.
+ * - {@link com.netflix.spectator.api.ExtendedRegistry}: registry class used to create
+ * meters.
+ * - {@link com.netflix.spectator.api.Counter}: meter type for measuring a rate of change.
+ * - {@link com.netflix.spectator.api.Timer}: meter type for measuring the time for many short
+ * events.
+ * - {@link com.netflix.spectator.api.LongTaskTimer}: meter type for measuring the time for a
+ * few long events.
+ * - {@link com.netflix.spectator.api.DistributionSummary}: meter type for measuring the sample
+ * distribution of some type of events.
+ *
+ */
+package com.netflix.spectator.api;
diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/Preconditions.java b/spectator-api/src/main/java/com/netflix/spectator/impl/Preconditions.java
new file mode 100644
index 000000000..4d46a892b
--- /dev/null
+++ b/spectator-api/src/main/java/com/netflix/spectator/impl/Preconditions.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2014 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.spectator.impl;
+
+/**
+ * Internal convenience methods that help a method or constructor check whether it was invoked
+ * correctly. Please notice that this should be considered an internal implementation detail, and
+ * it is subject to change without notice.
+ */
+public final class Preconditions {
+ private Preconditions() {
+ }
+
+ /**
+ * Ensures the object reference is not null.
+ */
+ public static T checkNotNull(T obj, String name) {
+ if (obj == null) {
+ String msg = String.format("parameter '%s' cannot be null", name);
+ throw new NullPointerException(msg);
+ }
+ return obj;
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance.
+ */
+ public static void checkState(boolean expression, String errMsg) {
+ if (!expression) {
+ throw new IllegalStateException(errMsg);
+ }
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultCounterTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultCounterTest.java
new file mode 100644
index 000000000..b7344702a
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultCounterTest.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DefaultCounterTest {
+
+ private final ManualClock clock = new ManualClock();
+
+ @Test
+ public void testInit() {
+ Counter c = new DefaultCounter(clock, NoopId.INSTANCE);
+ Assert.assertEquals(c.count(), 0L);
+ }
+
+ @Test
+ public void testIncrement() {
+ Counter c = new DefaultCounter(clock, NoopId.INSTANCE);
+ c.increment();
+ Assert.assertEquals(c.count(), 1L);
+ c.increment();
+ c.increment();
+ Assert.assertEquals(c.count(), 3L);
+ }
+
+ @Test
+ public void testIncrementAmount() {
+ Counter c = new DefaultCounter(clock, NoopId.INSTANCE);
+ c.increment(42);
+ Assert.assertEquals(c.count(), 42L);
+ }
+
+ @Test
+ public void testMeasure() {
+ Counter c = new DefaultCounter(clock, NoopId.INSTANCE);
+ c.increment(42);
+ clock.setWallTime(3712345L);
+ for (Measurement m : c.measure()) {
+ Assert.assertEquals(m.id(), c.id());
+ Assert.assertEquals(m.timestamp(), 3712345L);
+ Assert.assertEquals(m.value(), 42.0, 0.1e-12);
+ }
+ }
+
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java
new file mode 100644
index 000000000..fa61feeb0
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultDistributionSummaryTest.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DefaultDistributionSummaryTest {
+
+ private final ManualClock clock = new ManualClock();
+
+ @Test
+ public void testInit() {
+ DistributionSummary t = new DefaultDistributionSummary(clock, NoopId.INSTANCE);
+ Assert.assertEquals(t.count(), 0L);
+ Assert.assertEquals(t.totalAmount(), 0L);
+ }
+
+ @Test
+ public void testRecord() {
+ DistributionSummary t = new DefaultDistributionSummary(clock, NoopId.INSTANCE);
+ t.record(42);
+ Assert.assertEquals(t.count(), 1L);
+ Assert.assertEquals(t.totalAmount(), 42L);
+ }
+
+ @Test
+ public void testMeasure() {
+ DistributionSummary t = new DefaultDistributionSummary(clock, new DefaultId("foo"));
+ t.record(42);
+ clock.setWallTime(3712345L);
+ for (Measurement m : t.measure()) {
+ Assert.assertEquals(m.timestamp(), 3712345L);
+ if (m.id().equals(t.id().withTag("statistic", "count"))) {
+ Assert.assertEquals(m.value(), 1.0, 0.1e-12);
+ } else if (m.id().equals(t.id().withTag("statistic", "totalAmount"))) {
+ Assert.assertEquals(m.value(), 42.0, 0.1e-12);
+ } else {
+ Assert.fail("unexpected id: " + m.id());
+ }
+ }
+ }
+
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultIdTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultIdTest.java
new file mode 100644
index 000000000..2daa8098f
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultIdTest.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import nl.jqno.equalsverifier.Warning;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(JUnit4.class)
+public class DefaultIdTest {
+
+ @Test(expected = NullPointerException.class)
+ public void testNullName() {
+ new DefaultId(null);
+ }
+
+ @Test
+ public void testName() {
+ Id id = new DefaultId("foo");
+ Assert.assertEquals(id.name(), "foo");
+ }
+
+ @Test
+ public void testTags() {
+ TagList ts = new TagList("k1", "v1");
+ Id id = new DefaultId("foo", ts);
+ Assert.assertEquals(id.name(), "foo");
+ Assert.assertEquals(id.tags(), ts);
+ }
+
+ @Test
+ public void testTagsEmpty() {
+ Id id = new DefaultId("foo");
+ Assert.assertTrue(!id.tags().iterator().hasNext());
+ }
+
+ @Test
+ public void equalsContractTest() {
+ TagList ts1 = new TagList("k1", "v1");
+ TagList ts2 = new TagList("k2", "v2", ts1);
+ EqualsVerifier
+ .forClass(DefaultId.class)
+ .withPrefabValues(TagList.class, ts1, ts2)
+ .suppress(Warning.NULL_FIELDS)
+ .verify();
+ }
+
+ @Test
+ public void testNormalize() {
+ DefaultId id12 = (new DefaultId("foo")).withTag("k1", "v1").withTag("k2", "v2");
+ DefaultId id21 = (new DefaultId("foo")).withTag("k2", "v2").withTag("k1", "v1");
+ Assert.assertTrue(!id12.equals(id21));
+ Assert.assertEquals(id12, id21.normalize());
+ }
+
+ @Test
+ public void testRollup() {
+ Set keys = new HashSet<>();
+ keys.add("k1");
+ keys.add("foo");
+ DefaultId id = (new DefaultId("foo")).withTag("k1", "v1").withTag("k2", "v2");
+ DefaultId keepId = (new DefaultId("foo")).withTag("k1", "v1");
+ DefaultId dropId = (new DefaultId("foo")).withTag("k2", "v2");
+ Assert.assertEquals(keepId, id.rollup(keys, true));
+ Assert.assertEquals(dropId, id.rollup(keys, false));
+ }
+
+ @Test
+ public void testToString() {
+ DefaultId id = (new DefaultId("foo")).withTag("k1", "v1").withTag("k2", "v2");
+ Assert.assertEquals(id.toString(), "foo:k2=v2:k1=v1");
+ }
+
+ @Test
+ public void testToStringNameOnly() {
+ DefaultId id = new DefaultId("foo");
+ Assert.assertEquals(id.toString(), "foo");
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java
new file mode 100644
index 000000000..897003438
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultLongTaskTimerTest.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class DefaultLongTaskTimerTest {
+ private final ManualClock clock = new ManualClock();
+
+ @Test
+ public void testInit() {
+ LongTaskTimer t = new DefaultLongTaskTimer(clock, NoopId.INSTANCE);
+ Assert.assertEquals(t.duration(), 0L);
+ Assert.assertEquals(t.activeTasks(), 0L);
+ }
+
+
+ @Test
+ public void testStart() {
+ LongTaskTimer t = new DefaultLongTaskTimer(clock, NoopId.INSTANCE);
+
+ long task1 = t.start();
+ long task2 = t.start();
+
+ Assert.assertFalse(task1 == task2);
+ Assert.assertEquals(t.activeTasks(), 2);
+ Assert.assertEquals(t.duration(), 0L);
+ }
+
+ @Test
+ public void testStop() {
+ LongTaskTimer t = new DefaultLongTaskTimer(clock, NoopId.INSTANCE);
+
+ long task1 = t.start();
+ long task2 = t.start();
+
+ Assert.assertEquals(t.activeTasks(), 2);
+ clock.setMonotonicTime(5L);
+ Assert.assertEquals(t.duration(), 10L);
+
+ long elapsed1 = t.stop(task1);
+ Assert.assertEquals(elapsed1, 5L);
+ Assert.assertEquals(t.duration(task2), 5L);
+ Assert.assertEquals(t.duration(task1), -1L); // task is gone, should return default
+ Assert.assertEquals(t.duration(), 5L);
+ }
+
+ static void assertLongTaskTimer(Meter t, long timestamp, int activeTasks, double duration) {
+ for (Measurement m : t.measure()) {
+ Assert.assertEquals(m.timestamp(), timestamp);
+ if (m.id().equals(t.id().withTag("statistic", "activeTasks"))) {
+ Assert.assertEquals(m.value(), activeTasks, 1.0e-12);
+ } else if (m.id().equals(t.id().withTag("statistic", "duration"))) {
+ Assert.assertEquals(m.value(), duration, 1.0e-12);
+ } else {
+ Assert.fail("unexpected id: " + m.id());
+ }
+ }
+ }
+
+ @Test
+ public void testMeasure() {
+ LongTaskTimer t = new DefaultLongTaskTimer(clock, new DefaultId("foo"));
+ long task1 = t.start();
+ clock.setMonotonicTime(1_000_000_000L);
+ clock.setWallTime(1L);
+ assertLongTaskTimer(t, 1L, 1, 1.0);
+
+ long task2 = t.start();
+ assertLongTaskTimer(t, 1L, 2, 1.0);
+
+ t.stop(task1);
+ assertLongTaskTimer(t, 1L, 1, 0.0);
+
+ t.stop(task2);
+ assertLongTaskTimer(t, 1L, 0, 0.0);
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultRegistryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultRegistryTest.java
new file mode 100644
index 000000000..b9a20f7e0
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultRegistryTest.java
@@ -0,0 +1,267 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(JUnit4.class)
+public class DefaultRegistryTest {
+
+ private final ManualClock clock = new ManualClock();
+
+ @Before
+ public void init() {
+ System.setProperty("spectator.api.propagateWarnings", "true");
+ System.setProperty("spectator.api.maxNumberOfMeters", "10000");
+ }
+
+ @Test
+ public void testCreateId() {
+ Registry r = new DefaultRegistry(clock);
+ Assert.assertEquals(r.createId("foo"), new DefaultId("foo"));
+ }
+
+ @Test
+ public void testCreateIdWithTags() {
+ Registry r = new DefaultRegistry(clock);
+ TagList ts = new TagList("k", "v");
+ Assert.assertEquals(r.createId("foo", ts), new DefaultId("foo", ts));
+ }
+
+ @Test
+ public void testRegister() {
+ Registry r = new DefaultRegistry(clock);
+ Counter c = new DefaultCounter(clock, r.createId("foo"));
+ r.register(c);
+ c.increment();
+ Assert.assertEquals(c.count(), 1L);
+ r.register(c);
+ Meter meter = r.get(c.id());
+ for (Measurement m : meter.measure()) {
+ Assert.assertEquals(m.value(), 2.0, 1e-12);
+ }
+ }
+
+ @Test
+ public void testCounter() {
+ Registry r = new DefaultRegistry(clock);
+ Counter c = r.counter(r.createId("foo"));
+ c.increment();
+ Assert.assertEquals(c.count(), 1L);
+
+ Counter c2 = r.counter(r.createId("foo"));
+ Assert.assertSame(c, c2);
+ }
+
+ @Test
+ public void testTimer() {
+ Registry r = new DefaultRegistry(clock);
+ Timer t = r.timer(r.createId("foo"));
+ t.record(42L, TimeUnit.MILLISECONDS);
+ Assert.assertEquals(t.count(), 1L);
+
+ Timer t2 = r.timer(r.createId("foo"));
+ Assert.assertSame(t, t2);
+ }
+
+ @Test
+ public void testDistributionSummary() {
+ Registry r = new DefaultRegistry(clock);
+ DistributionSummary t = r.distributionSummary(r.createId("foo"));
+ t.record(42L);
+ Assert.assertEquals(t.count(), 1L);
+
+ DistributionSummary t2 = r.distributionSummary(r.createId("foo"));
+ Assert.assertSame(t, t2);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testRegisterBadTypeAccess() {
+ Registry r = new DefaultRegistry(clock);
+ Counter c = new DefaultCounter(clock, r.createId("foo"));
+ r.register(c);
+ r.counter(c.id());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testCounterBadTypeAccess() {
+ Registry r = new DefaultRegistry(clock);
+ r.counter(r.createId("foo"));
+ r.distributionSummary(r.createId("foo"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testTimerBadTypeAccess() {
+ Registry r = new DefaultRegistry(clock);
+ r.timer(r.createId("foo"));
+ r.counter(r.createId("foo"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testDistributionSummaryBadTypeAccess() {
+ Registry r = new DefaultRegistry(clock);
+ r.distributionSummary(r.createId("foo"));
+ r.timer(r.createId("foo"));
+ }
+
+ @Test
+ public void testRegisterBadTypeAccessNoThrow() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ Registry r = new DefaultRegistry(clock);
+ Counter c = new DefaultCounter(clock, r.createId("foo"));
+ r.counter(c.id());
+ r.register(c);
+ Assert.assertNotSame(r.get(c.id()), c);
+ }
+
+ @Test
+ public void testCounterBadTypeAccessNoThrow() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ Registry r = new DefaultRegistry(clock);
+ r.counter(r.createId("foo"));
+ Assert.assertEquals(r.distributionSummary(r.createId("foo")), NoopDistributionSummary.INSTANCE);
+ }
+
+ @Test
+ public void testTimerBadTypeAccessNoThrow() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ Registry r = new DefaultRegistry(clock);
+ r.timer(r.createId("foo"));
+ Assert.assertEquals(r.counter(r.createId("foo")), NoopCounter.INSTANCE);
+ }
+
+ @Test
+ public void testDistributionSummaryBadTypeAccessNoThrow() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ Registry r = new DefaultRegistry(clock);
+ r.distributionSummary(r.createId("foo"));
+ Assert.assertEquals(r.timer(r.createId("foo")), NoopTimer.INSTANCE);
+ }
+
+ @Test
+ public void testMaxLimitExceededCounter() {
+ System.setProperty("spectator.api.maxNumberOfMeters", "1");
+ Registry r = new DefaultRegistry(clock);
+ Assert.assertNotSame(r.counter(r.createId("c1")), NoopCounter.INSTANCE);
+ Assert.assertSame(r.counter(r.createId("c2")), NoopCounter.INSTANCE);
+ Assert.assertNotSame(r.counter(r.createId("c1")), NoopCounter.INSTANCE);
+ }
+
+ @Test
+ public void testMaxLimitExceededTimer() {
+ System.setProperty("spectator.api.maxNumberOfMeters", "1");
+ Registry r = new DefaultRegistry(clock);
+ Assert.assertNotSame(r.timer(r.createId("c1")), NoopTimer.INSTANCE);
+ Assert.assertSame(r.timer(r.createId("c2")), NoopTimer.INSTANCE);
+ Assert.assertNotSame(r.timer(r.createId("c1")), NoopTimer.INSTANCE);
+ }
+
+ @Test
+ public void testMaxLimitExceededDistributionSummary() {
+ System.setProperty("spectator.api.maxNumberOfMeters", "1");
+ Registry r = new DefaultRegistry(clock);
+ Assert.assertNotSame(r.distributionSummary(r.createId("c1")), NoopDistributionSummary.INSTANCE);
+ Assert.assertSame(r.distributionSummary(r.createId("c2")), NoopDistributionSummary.INSTANCE);
+ Assert.assertNotSame(r.distributionSummary(r.createId("c1")), NoopDistributionSummary.INSTANCE);
+ }
+
+ @Test
+ public void testMaxLimitExceededRegister() {
+ final AtomicInteger count = new AtomicInteger(0);
+ RegistryListener listener = new RegistryListener() {
+ public void onAdd(Meter m) {
+ count.incrementAndGet();
+ }
+ };
+
+ System.setProperty("spectator.api.maxNumberOfMeters", "1");
+ Registry r = new DefaultRegistry(clock);
+ r.addListener(listener);
+ Assert.assertEquals(count.get(), 0);
+ r.register(new DefaultCounter(clock, r.createId("c1")));
+ Assert.assertEquals(count.get(), 1);
+ r.register(new DefaultCounter(clock, r.createId("c2")));
+ Assert.assertEquals(count.get(), 1);
+ r.register(new DefaultCounter(clock, r.createId("c1")));
+ Assert.assertEquals(count.get(), 2);
+ }
+
+ @Test
+ public void testGet() {
+ Registry r = new DefaultRegistry(clock);
+ Counter c = r.counter(r.createId("foo"));
+ Meter m = r.get(c.id());
+ Assert.assertSame(c, m);
+ }
+
+ @Test
+ public void testIteratorEmpty() {
+ Registry r = new DefaultRegistry(clock);
+ for (Meter m : r) {
+ Assert.fail("should be empty, but found " + m.id());
+ }
+ }
+
+ @Test
+ public void testIterator() {
+ Registry r = new DefaultRegistry(clock);
+ r.counter(r.createId("foo"));
+ r.counter(r.createId("bar"));
+ Set expected = new HashSet<>();
+ expected.add(r.createId("foo"));
+ expected.add(r.createId("bar"));
+ for (Meter m : r) {
+ expected.remove(m.id());
+ }
+ Assert.assertTrue(expected.isEmpty());
+ }
+
+ @Test
+ public void testListener() {
+ final Set seen = new HashSet<>();
+ RegistryListener listener = new RegistryListener() {
+ public void onAdd(Meter m) {
+ Assert.assertTrue(!seen.contains(m.id()));
+ seen.add(m.id());
+ }
+ };
+
+ Registry r = new DefaultRegistry(clock);
+ r.counter(r.createId("pre"));
+ r.addListener(listener);
+ r.counter(r.createId("foo"));
+ r.timer(r.createId("bar"));
+ r.distributionSummary(r.createId("baz"));
+ r.removeListener(listener);
+ r.counter(r.createId("post"));
+
+ Set expected = new HashSet<>();
+ expected.add(r.createId("foo"));
+ expected.add(r.createId("bar"));
+ expected.add(r.createId("baz"));
+
+ Assert.assertEquals(expected, seen);
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java
new file mode 100644
index 000000000..b7bf723b9
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/DefaultTimerTest.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class DefaultTimerTest {
+
+ private final ManualClock clock = new ManualClock();
+
+ @Test
+ public void testInit() {
+ Timer t = new DefaultTimer(clock, NoopId.INSTANCE);
+ Assert.assertEquals(t.count(), 0L);
+ Assert.assertEquals(t.totalTime(), 0L);
+ }
+
+ @Test
+ public void testRecord() {
+ Timer t = new DefaultTimer(clock, NoopId.INSTANCE);
+ t.record(42, TimeUnit.MILLISECONDS);
+ Assert.assertEquals(t.count(), 1L);
+ Assert.assertEquals(t.totalTime(), 42000000L);
+ }
+
+ @Test
+ public void testRecordCallable() throws Exception {
+ Timer t = new DefaultTimer(clock, NoopId.INSTANCE);
+ clock.setMonotonicTime(100L);
+ int v = t.record(new Callable() {
+ public Integer call() throws Exception {
+ clock.setMonotonicTime(500L);
+ return 42;
+ }
+ });
+ Assert.assertEquals(v, 42);
+ Assert.assertEquals(t.count(), 1L);
+ Assert.assertEquals(t.totalTime(), 400L);
+ }
+
+ @Test
+ public void testRecordCallableException() throws Exception {
+ Timer t = new DefaultTimer(clock, NoopId.INSTANCE);
+ clock.setMonotonicTime(100L);
+ boolean seen = false;
+ try {
+ t.record(new Callable() {
+ public Integer call() throws Exception {
+ clock.setMonotonicTime(500L);
+ throw new RuntimeException("foo");
+ }
+ });
+ } catch (Exception e) {
+ seen = true;
+ }
+ Assert.assertTrue(seen);
+ Assert.assertEquals(t.count(), 1L);
+ Assert.assertEquals(t.totalTime(), 400L);
+ }
+
+ @Test
+ public void testRecordRunnable() throws Exception {
+ Timer t = new DefaultTimer(clock, NoopId.INSTANCE);
+ clock.setMonotonicTime(100L);
+ t.record(new Runnable() {
+ public void run() {
+ clock.setMonotonicTime(500L);
+ }
+ });
+ Assert.assertEquals(t.count(), 1L);
+ Assert.assertEquals(t.totalTime(), 400L);
+ }
+
+ @Test
+ public void testRecordRunnableException() throws Exception {
+ Timer t = new DefaultTimer(clock, NoopId.INSTANCE);
+ clock.setMonotonicTime(100L);
+ boolean seen = false;
+ try {
+ t.record(new Runnable() {
+ public void run() {
+ clock.setMonotonicTime(500L);
+ throw new RuntimeException("foo");
+ }
+ });
+ } catch (Exception e) {
+ seen = true;
+ }
+ Assert.assertTrue(seen);
+ Assert.assertEquals(t.count(), 1L);
+ Assert.assertEquals(t.totalTime(), 400L);
+ }
+
+ @Test
+ public void testMeasure() {
+ Timer t = new DefaultTimer(clock, new DefaultId("foo"));
+ t.record(42, TimeUnit.MILLISECONDS);
+ clock.setWallTime(3712345L);
+ for (Measurement m : t.measure()) {
+ Assert.assertEquals(m.timestamp(), 3712345L);
+ if (m.id().equals(t.id().withTag("statistic", "count"))) {
+ Assert.assertEquals(m.value(), 1.0, 0.1e-12);
+ } else if (m.id().equals(t.id().withTag("statistic", "totalTime"))) {
+ Assert.assertEquals(m.value(), 42e6, 0.1e-12);
+ } else {
+ Assert.fail("unexpected id: " + m.id());
+ }
+ }
+ }
+
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/ExtendedRegistryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/ExtendedRegistryTest.java
new file mode 100644
index 000000000..65dfd5a5c
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/ExtendedRegistryTest.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicLong;
+
+@RunWith(JUnit4.class)
+public class ExtendedRegistryTest {
+
+ @Test
+ public void testCreateIdArray() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ Id id1 = r.createId("foo", "bar", "baz", "k", "v");
+ Id id2 = r.createId("foo", new TagList("k", "v", new TagList("bar", "baz")));
+ Assert.assertEquals(id1, id2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCreateIdArrayOdd() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ r.createId("foo", "bar", "baz", "k");
+ }
+
+ @Test
+ public void testCounterHelpers() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ Counter c1 = r.counter("foo", "bar", "baz", "k", "v");
+ Counter c2 = r.counter("foo", new TagList("k", "v", new TagList("bar", "baz")));
+ Counter c3 = r.counter("foo");
+ Assert.assertSame(c1, c2);
+ Assert.assertNotSame(c1, c3);
+ }
+
+ @Test
+ public void testDistributionSummaryHelpers() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ DistributionSummary c1 = r.distributionSummary("foo", "bar", "baz", "k", "v");
+ DistributionSummary c2 = r.distributionSummary("foo",
+ new TagList("k", "v", new TagList("bar", "baz")));
+ DistributionSummary c3 = r.distributionSummary("foo");
+ Assert.assertSame(c1, c2);
+ Assert.assertNotSame(c1, c3);
+ }
+
+ @Test
+ public void testTimerHelpers() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ Timer c1 = r.timer("foo", "bar", "baz", "k", "v");
+ Timer c2 = r.timer("foo", new TagList("k", "v", new TagList("bar", "baz")));
+ Timer c3 = r.timer("foo");
+ Assert.assertSame(c1, c2);
+ Assert.assertNotSame(c1, c3);
+ }
+
+ @Test
+ public void testLongTaskTimerHelpers() {
+ ManualClock clock = new ManualClock();
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry(clock));
+ LongTaskTimer c1 = r.longTaskTimer("foo", "bar", "baz", "k", "v");
+ Meter m1 = r.get(c1.id());
+ Assert.assertEquals(c1.id(), m1.id()); // registration
+
+ LongTaskTimer c2 = r.longTaskTimer("foo", new TagList("k", "v", new TagList("bar", "baz")));
+ Assert.assertEquals(c1.id(), c2.id());
+
+ long t1 = c1.start();
+ long t2 = c2.start();
+ clock.setMonotonicTime(1000L);
+ clock.setWallTime(1L);
+ DefaultLongTaskTimerTest.assertLongTaskTimer(r.get(c1.id()), 1L, 2, 2.0e-6);
+
+ c1.stop(t1);
+ DefaultLongTaskTimerTest.assertLongTaskTimer(r.get(c1.id()), 1L, 1, 1.0e-6);
+
+ c2.stop(t2);
+ DefaultLongTaskTimerTest.assertLongTaskTimer(r.get(c1.id()), 1L, 0, 0L);
+ }
+
+ @Test
+ public void testGaugeHelpers() {
+ AtomicLong al1 = new AtomicLong(1L);
+ AtomicLong al2 = new AtomicLong(2L);
+ AtomicLong al4 = new AtomicLong(4L);
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ AtomicLong v1 = r.gauge(r.createId("foo", "bar", "baz", "k", "v"), al1);
+ AtomicLong v2 = r.gauge("foo", new TagList("k", "v", new TagList("bar", "baz")), al2);
+ AtomicLong v3 = r.gauge("foo", al4);
+ Assert.assertSame(v1, al1);
+ Assert.assertSame(v2, al2);
+ Assert.assertSame(v3, al4);
+ Id id1 = r.createId("foo", "bar", "baz", "k", "v");
+ Id id2 = r.createId("foo");
+ Assert.assertEquals(r.get(id1).measure().iterator().next().value(), 3.0, 1e-12);
+ Assert.assertEquals(r.get(id2).measure().iterator().next().value(), 4.0, 1e-12);
+ }
+
+ @Test
+ public void testGaugeHelpersWithFunction() {
+ AtomicLong al1 = new AtomicLong(1L);
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry(new ManualClock(40, 0)));
+ DoubleFunction f = Functions.age(r.clock());
+ AtomicLong v1 = r.gauge("foo", al1, f);
+ Assert.assertSame(v1, al1);
+ Id id1 = r.createId("foo");
+ Assert.assertEquals(r.get(id1).measure().iterator().next().value(), 39.0 / 1000.0, 1e-12);
+ }
+
+ @Test
+ public void testCollectionSizeHelpers() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ LinkedBlockingDeque q1 = new LinkedBlockingDeque<>();
+ LinkedBlockingDeque q2 = r.collectionSize("queueSize", q1);
+ Assert.assertSame(q1, q2);
+ Id id = r.createId("queueSize");
+ Assert.assertEquals(r.get(id).measure().iterator().next().value(), 0.0, 1e-12);
+ q2.push("foo");
+ Assert.assertEquals(r.get(id).measure().iterator().next().value(), 1.0, 1e-12);
+ }
+
+ @Test
+ public void testMapSizeHelpers() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ ConcurrentHashMap q1 = new ConcurrentHashMap<>();
+ ConcurrentHashMap q2 = r.mapSize("mapSize", q1);
+ Assert.assertSame(q1, q2);
+ Id id = r.createId("mapSize");
+ Assert.assertEquals(r.get(id).measure().iterator().next().value(), 0.0, 1e-12);
+ q2.put("foo", "bar");
+ Assert.assertEquals(r.get(id).measure().iterator().next().value(), 1.0, 1e-12);
+ }
+
+ @Test
+ public void testMethodValueHelpers() {
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ LinkedBlockingDeque q1 = new LinkedBlockingDeque<>();
+ r.methodValue("queueSize", q1, "size");
+ Id id = r.createId("queueSize");
+ Assert.assertEquals(r.get(id).measure().iterator().next().value(), 0.0, 1e-12);
+ q1.push("foo");
+ Assert.assertEquals(r.get(id).measure().iterator().next().value(), 1.0, 1e-12);
+ }
+
+ @Test(expected = ClassCastException.class)
+ public void methodValueBadReturnType() {
+ System.setProperty("spectator.api.propagateWarnings", "true");
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ r.methodValue("queueSize", this, "toString");
+ }
+
+ @Test
+ public void methodValueBadReturnTypeNoPropagate() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ r.methodValue("queueSize", this, "toString");
+ Assert.assertNull(r.get(r.createId("queueSize")));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void methodValueUnknown() {
+ System.setProperty("spectator.api.propagateWarnings", "true");
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ r.methodValue("queueSize", this, "unknownMethod");
+ }
+
+ @Test
+ public void methodValueUnknownNoPropagate() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ ExtendedRegistry r = new ExtendedRegistry(new DefaultRegistry());
+ r.methodValue("queueSize", this, "unknownMethod");
+ Assert.assertNull(r.get(r.createId("queueSize")));
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/FunctionsTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/FunctionsTest.java
new file mode 100644
index 000000000..6c587994b
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/FunctionsTest.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FunctionsTest {
+
+ private final ManualClock clock = new ManualClock();
+ private final ExtendedRegistry registry = new ExtendedRegistry(new DefaultRegistry());
+
+ @Test
+ public void ageFunction() {
+ clock.setWallTime(5000L);
+ final DoubleFunction f = Functions.age(clock);
+ Assert.assertEquals(f.apply(1000L), 4.0, 1e-12);
+ }
+
+ private byte byteMethod() {
+ return (byte) 1;
+ }
+
+ @Test
+ public void invokeMethodByte() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "byteMethod"));
+ Assert.assertEquals(f.apply(this), 1.0, 1e-12);
+ }
+
+ private short shortMethod() {
+ return (short) 2;
+ }
+
+ @Test
+ public void invokeMethodShort() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "shortMethod"));
+ Assert.assertEquals(f.apply(this), 2.0, 1e-12);
+ }
+
+ private int intMethod() {
+ return 3;
+ }
+
+ @Test
+ public void invokeMethodInt() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "intMethod"));
+ Assert.assertEquals(f.apply(this), 3.0, 1e-12);
+ }
+
+ private long longMethod() {
+ return 4L;
+ }
+
+ @Test
+ public void invokeMethodLong() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "longMethod"));
+ Assert.assertEquals(f.apply(this), 4.0, 1e-12);
+ }
+
+ private Long wrapperLongMethod() {
+ return 5L;
+ }
+
+ @Test
+ public void invokeMethodWrapperLong() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(
+ registry.getMethod(getClass(), "wrapperLongMethod"));
+ Assert.assertEquals(f.apply(this), 5.0, 1e-12);
+ }
+
+ private Long throwsMethod() {
+ throw new IllegalStateException("fubar");
+ }
+
+ @Test
+ public void invokeBadMethod() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(getClass(), "throwsMethod"));
+ Assert.assertEquals(f.apply(this), Double.NaN, 1e-12);
+ }
+
+ @Test(expected = NoSuchMethodException.class)
+ public void invokeNoSuchMethod() throws Exception {
+ Functions.invokeMethod(registry.getMethod(getClass(), "unknownMethod"));
+ }
+
+ @Test
+ public void invokeOnSubclass() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(B.class, "two"));
+ Assert.assertEquals(f.apply(new B()), 2.0, 1e-12);
+ }
+
+ @Test
+ public void invokeOneA() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(A.class, "one"));
+ Assert.assertEquals(f.apply(new A()), 1.0, 1e-12);
+ }
+
+ @Test
+ public void invokeOneB() throws Exception {
+ final ValueFunction f = Functions.invokeMethod(registry.getMethod(B.class, "one"));
+ Assert.assertEquals(f.apply(new B()), -1.0, 1e-12);
+ }
+
+ private static class A {
+ public int one() {
+ return 1;
+ }
+ }
+
+ private static class B extends A {
+ public int one() {
+ return -1;
+ }
+
+ public int two() {
+ return 2;
+ }
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/MeasurementTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/MeasurementTest.java
new file mode 100644
index 000000000..927142d03
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/MeasurementTest.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+import nl.jqno.equalsverifier.Warning;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MeasurementTest {
+
+ @Test
+ public void testEqualsContract() {
+ EqualsVerifier
+ .forClass(Measurement.class)
+ .suppress(Warning.NULL_FIELDS)
+ .verify();
+ }
+
+ @Test
+ public void testToString() {
+ Id id = new DefaultId("foo");
+ Measurement m = new Measurement(id, 42L, 42.0);
+ Assert.assertEquals(m.toString(), "Measurement(foo,42,42.0)");
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopCounterTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopCounterTest.java
new file mode 100644
index 000000000..26f754803
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopCounterTest.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NoopCounterTest {
+ @Test
+ public void testId() {
+ Assert.assertEquals(NoopCounter.INSTANCE.id(), NoopId.INSTANCE);
+ Assert.assertTrue(!NoopCounter.INSTANCE.hasExpired());
+ }
+
+ @Test
+ public void testIncrement() {
+ NoopCounter c = NoopCounter.INSTANCE;
+ c.increment();
+ Assert.assertEquals(c.count(), 0L);
+ }
+
+ @Test
+ public void testIncrementAmount() {
+ NoopCounter c = NoopCounter.INSTANCE;
+ c.increment(42);
+ Assert.assertEquals(c.count(), 0L);
+ }
+
+ @Test
+ public void testMeasure() {
+ NoopCounter c = NoopCounter.INSTANCE;
+ c.increment(42);
+ Assert.assertTrue(!c.measure().iterator().hasNext());
+ }
+
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopDistributionSummaryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopDistributionSummaryTest.java
new file mode 100644
index 000000000..ff0c35bcb
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopDistributionSummaryTest.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NoopDistributionSummaryTest {
+ @Test
+ public void testId() {
+ Assert.assertEquals(NoopDistributionSummary.INSTANCE.id(), NoopId.INSTANCE);
+ Assert.assertTrue(!NoopDistributionSummary.INSTANCE.hasExpired());
+ }
+
+ @Test
+ public void testIncrement() {
+ NoopDistributionSummary t = NoopDistributionSummary.INSTANCE;
+ t.record(42);
+ Assert.assertEquals(t.count(), 0L);
+ Assert.assertEquals(t.totalAmount(), 0L);
+ }
+
+ @Test
+ public void testMeasure() {
+ NoopDistributionSummary t = NoopDistributionSummary.INSTANCE;
+ t.record(42);
+ Assert.assertTrue(!t.measure().iterator().hasNext());
+ }
+
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopIdTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopIdTest.java
new file mode 100644
index 000000000..cb4d09566
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopIdTest.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NoopIdTest {
+ @Test
+ public void testTags() {
+ Assert.assertTrue(!NoopId.INSTANCE.tags().iterator().hasNext());
+ }
+
+ @Test
+ public void testWithTag() {
+ Assert.assertEquals(NoopId.INSTANCE.withTag(new TagList("k", "v")), NoopId.INSTANCE);
+ }
+
+ @Test
+ public void testToString() {
+ Assert.assertEquals(NoopId.INSTANCE.toString(), "noop");
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopRegistryTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopRegistryTest.java
new file mode 100644
index 000000000..d87c7fb30
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopRegistryTest.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class NoopRegistryTest {
+
+ private final ManualClock clock = new ManualClock();
+
+ @Test
+ public void testCreateId() {
+ Registry r = new NoopRegistry();
+ Assert.assertEquals(r.createId("foo"), NoopId.INSTANCE);
+ }
+
+ @Test
+ public void testCreateIdWithTags() {
+ Registry r = new NoopRegistry();
+ TagList ts = new TagList("k", "v");
+ Assert.assertEquals(r.createId("foo", ts), NoopId.INSTANCE);
+ }
+
+ @Test
+ public void testRegister() {
+ Registry r = new NoopRegistry();
+ Counter c = new DefaultCounter(clock, r.createId("foo"));
+ r.register(c);
+ Assert.assertNull(r.get(c.id()));
+ }
+
+ @Test
+ public void testCounter() {
+ Registry r = new NoopRegistry();
+ Counter c = r.counter(r.createId("foo"));
+ c.increment();
+ Assert.assertEquals(c.count(), 0L);
+
+ Counter c2 = r.counter(r.createId("foo"));
+ Assert.assertSame(c, c2);
+ }
+
+ @Test
+ public void testTimer() {
+ Registry r = new NoopRegistry();
+ Timer t = r.timer(r.createId("foo"));
+ t.record(42L, TimeUnit.MILLISECONDS);
+ Assert.assertEquals(t.count(), 0L);
+
+ Timer t2 = r.timer(r.createId("foo"));
+ Assert.assertSame(t, t2);
+ }
+
+ @Test
+ public void testDistributionSummary() {
+ Registry r = new NoopRegistry();
+ DistributionSummary t = r.distributionSummary(r.createId("foo"));
+ t.record(42L);
+ Assert.assertEquals(t.count(), 0L);
+
+ DistributionSummary t2 = r.distributionSummary(r.createId("foo"));
+ Assert.assertSame(t, t2);
+ }
+
+ @Test
+ public void testGet() {
+ Registry r = new NoopRegistry();
+ Counter c = r.counter(r.createId("foo"));
+ Assert.assertNull(r.get(c.id()));
+ }
+
+ @Test
+ public void testIteratorEmpty() {
+ Registry r = new NoopRegistry();
+ for (Meter m : r) {
+ Assert.fail("should be empty, but found " + m.id());
+ }
+ }
+
+ @Test
+ public void testIterator() {
+ Registry r = new NoopRegistry();
+ r.counter(r.createId("foo"));
+ r.counter(r.createId("bar"));
+ for (Meter m : r) {
+ Assert.fail("should be empty, but found " + m.id());
+ }
+ }
+
+ @Test
+ public void testListener() {
+ RegistryListener listener = new RegistryListener() {
+ public void onAdd(Meter m) {
+ Assert.fail("shouldn't notify listeners");
+ }
+ };
+
+ Registry r = new NoopRegistry();
+ r.counter(r.createId("pre"));
+ r.addListener(listener);
+ r.counter(r.createId("foo"));
+ r.timer(r.createId("bar"));
+ r.distributionSummary(r.createId("baz"));
+ r.removeListener(listener);
+ r.counter(r.createId("post"));
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/NoopTimerTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/NoopTimerTest.java
new file mode 100644
index 000000000..bf6961a54
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/NoopTimerTest.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+public class NoopTimerTest {
+ @Test
+ public void testId() {
+ Assert.assertEquals(NoopTimer.INSTANCE.id(), NoopId.INSTANCE);
+ Assert.assertTrue(!NoopTimer.INSTANCE.hasExpired());
+ }
+
+ @Test
+ public void testIncrement() {
+ NoopTimer t = NoopTimer.INSTANCE;
+ t.record(42, TimeUnit.MILLISECONDS);
+ Assert.assertEquals(t.count(), 0L);
+ Assert.assertEquals(t.totalTime(), 0L);
+ }
+
+ @Test
+ public void testMeasure() {
+ NoopTimer t = NoopTimer.INSTANCE;
+ t.record(42, TimeUnit.MILLISECONDS);
+ Assert.assertTrue(!t.measure().iterator().hasNext());
+ }
+
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/ObjectGaugeTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/ObjectGaugeTest.java
new file mode 100644
index 000000000..ce8ce7d80
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/ObjectGaugeTest.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+@RunWith(JUnit4.class)
+public class ObjectGaugeTest {
+
+ private final ManualClock clock = new ManualClock();
+
+ @Test
+ public void testGC() {
+ ObjectGauge g = new ObjectGauge(
+ clock, NoopId.INSTANCE, new AtomicLong(42L), Functions.IDENTITY);
+ for (Measurement m : g.measure()) {
+ Assert.assertEquals(m.value(), 42.0, 1e-12);
+ }
+
+ // Verify we get NaN after gc, this is quite possibly flakey and can be commented out
+ // if needed
+ System.gc();
+ Assert.assertTrue(g.hasExpired());
+ for (Measurement m : g.measure()) {
+ Assert.assertEquals(m.value(), Double.NaN, 1e-12);
+ }
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/SpectatorTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/SpectatorTest.java
new file mode 100644
index 000000000..15f5f1aff
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/SpectatorTest.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class SpectatorTest {
+ @Test
+ public void testRegistry() {
+ Assert.assertNotNull(Spectator.registry());
+ }
+
+ @Test
+ public void testNewInstanceBadClass() {
+ System.setProperty("spectator.api.propagateWarnings", "false");
+ Assert.assertTrue(Spectator.newInstance("fubar") instanceof DefaultRegistry);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void testNewInstanceBadClassPropagate() {
+ System.setProperty("spectator.api.propagateWarnings", "true");
+ Spectator.newInstance("fubar");
+ }
+}
diff --git a/spectator-api/src/test/java/com/netflix/spectator/api/TagListTest.java b/spectator-api/src/test/java/com/netflix/spectator/api/TagListTest.java
new file mode 100644
index 000000000..87a7cafb4
--- /dev/null
+++ b/spectator-api/src/test/java/com/netflix/spectator/api/TagListTest.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright 2014 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.spectator.api;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public class TagListTest {
+
+ @Test
+ public void equalsContractTest() {
+ // NOTE: EqualsVerifier doesn't work with cached hash code
+ TagList ts1 = new TagList("k1", "v1");
+ TagList ts2 = new TagList("k2", "v2", ts1);
+ Assert.assertTrue(ts1.equals(ts1));
+ Assert.assertTrue(ts2.equals(ts2));
+ Assert.assertTrue(!ts1.equals(null));
+ Assert.assertTrue(!ts1.equals(new Object()));
+ Assert.assertTrue(!ts1.equals(new TagList("k1", "v2")));
+ Assert.assertTrue(!ts1.equals(new TagList("k2", "v1")));
+ Assert.assertTrue(!ts1.equals(new TagList("k1", "v1", ts2)));
+ Assert.assertTrue(ts2.equals(new TagList("k2", "v2", ts1)));
+ Assert.assertTrue(ts2.equals(new TagList("k2", "v2", new TagList("k1", "v1"))));
+ }
+
+ @Test
+ public void testSingle() {
+ TagList ts = new TagList("k", "v");
+ for (Tag t : ts) {
+ Assert.assertEquals(t, ts);
+ Assert.assertEquals(t.key(), "k");
+ Assert.assertEquals(t.value(), "v");
+ }
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testNullKey() {
+ new TagList(null, "v");
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testNullValue() {
+ new TagList("k", null);
+ }
+
+ @Test
+ public void testCreateFromMap() {
+ Map m = new HashMap<>();
+ m.put("k", "v");
+ TagList ts1 = TagList.create(m);
+ TagList ts2 = new TagList("k", "v");
+ Assert.assertEquals(ts1, ts2);
+ }
+
+ @Test
+ public void testCreateFromTagList() {
+ TagList ts = new TagList("k", "v");
+ TagList ts1 = TagList.create(ts);
+ TagList ts2 = new TagList("k", "v");
+ Assert.assertEquals(ts1, ts2);
+ }
+
+ @Test
+ public void testCreateFromIterable() {
+ Collection coll = Collections.singleton(new TagList("k", "v"));
+ TagList ts1 = TagList.create(coll);
+ TagList ts2 = new TagList("k", "v");
+ Assert.assertEquals(ts1, ts2);
+ }
+}
diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/CircularBuffer.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/CircularBuffer.java
new file mode 100644
index 000000000..fdf7750d6
--- /dev/null
+++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/CircularBuffer.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2014 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.spectator.gc;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReferenceArray;
+
+/**
+ * Fixed size buffer that overwrites previous entries after filling up all slots.
+ */
+class CircularBuffer {
+
+ private final AtomicInteger nextIndex;
+ private final AtomicReferenceArray data;
+
+ /** Create a new instance. */
+ CircularBuffer(int length) {
+ nextIndex = new AtomicInteger(0);
+ data = new AtomicReferenceArray<>(length);
+ }
+
+ /** Add a new item to the buffer. If the buffer is full a previous entry will get overwritten. */
+ void add(T item) {
+ int i = nextIndex.getAndIncrement() % data.length();
+ data.set(i, item);
+ }
+
+ /** Get the item in the buffer at position {@code i} or return null if it isn't set. */
+ T get(int i) {
+ return data.get(i);
+ }
+
+ /** Return the capacity of the buffer. */
+ int size() {
+ return data.length();
+ }
+
+ /** Return a list with a copy of the data in the buffer. */
+ List toList() {
+ List items = new ArrayList<>(data.length());
+ for (int i = 0; i < data.length(); ++i) {
+ T item = data.get(i);
+ if (item != null) {
+ items.add(item);
+ }
+ }
+ return items;
+ }
+}
diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEvent.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEvent.java
new file mode 100644
index 000000000..9111c6802
--- /dev/null
+++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEvent.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2014 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.spectator.gc;
+
+import com.sun.management.GarbageCollectionNotificationInfo;
+import com.sun.management.GcInfo;
+
+import java.util.Comparator;
+import java.util.Date;
+
+/**
+ * Metadata about a garbage collection event.
+ */
+public class GcEvent {
+
+ private final String name;
+ private final GarbageCollectionNotificationInfo info;
+ private final GcType type;
+ private final long startTime;
+
+ /**
+ * Create a new instance.
+ *
+ * @param info
+ * The info object from the notification emitter on the
+ * {@link java.lang.management.GarbageCollectorMXBean}.
+ * @param startTime
+ * Start time in milliseconds since the epoch. Note the info object has a start time relative
+ * to the time the jvm process was started.
+ */
+ public GcEvent(GarbageCollectionNotificationInfo info, long startTime) {
+ this.name = info.getGcName();
+ this.info = info;
+ this.type = HelperFunctions.getGcType(name);
+ this.startTime = startTime;
+ }
+
+ /** Type of GC event that occurred. */
+ public GcType getType() {
+ return type;
+ }
+
+ /** Name of the collector for the event. */
+ public String getName() {
+ return name;
+ }
+
+ /** Start time in milliseconds since the epoch. */
+ public long getStartTime() {
+ return startTime;
+ }
+
+ /**
+ * Info object from the {@link java.lang.management.GarbageCollectorMXBean} notification
+ * emitter.
+ */
+ public GarbageCollectionNotificationInfo getInfo() {
+ return info;
+ }
+
+ @Override
+ public String toString() {
+ GcInfo gcInfo = info.getGcInfo();
+ long totalBefore = HelperFunctions.getTotalUsage(gcInfo.getMemoryUsageBeforeGc());
+ long totalAfter = HelperFunctions.getTotalUsage(gcInfo.getMemoryUsageAfterGc());
+ long max = HelperFunctions.getTotalMaxUsage(gcInfo.getMemoryUsageAfterGc());
+
+ String unit = "K";
+ double cnv = 1000.0;
+ if (max > 1000000000L) {
+ unit = "G";
+ cnv = 1e9;
+ } else if (max > 1000000L) {
+ unit = "M";
+ cnv = 1e6;
+ }
+
+ String change = String.format(
+ "%.1f%s => %.1f%s / %.1f%s",
+ totalBefore / cnv, unit,
+ totalAfter / cnv, unit,
+ max / cnv, unit);
+ String percentChange = String.format(
+ "%.1f%% => %.1f%%", 100.0 * totalBefore / max, 100.0 * totalAfter / max);
+
+ final Date d = new Date(startTime);
+ return type.toString() + ": "
+ + name + ", id=" + gcInfo.getId() + ", at=" + d.toString()
+ + ", duration=" + gcInfo.getDuration() + "ms" + ", cause=[" + info.getGcCause() + "]"
+ + ", " + change + " (" + percentChange + ")";
+ }
+
+ /** Order events from oldest to newest. */
+ public static final Comparator TIME_ORDER = new Comparator() {
+ public int compare(GcEvent e1, GcEvent e2) {
+ return (int) (e1.getStartTime() - e2.getStartTime());
+ }
+ };
+
+ /** Order events from newest to oldest. */
+ public static final Comparator REVERSE_TIME_ORDER = new Comparator() {
+ public int compare(GcEvent e1, GcEvent e2) {
+ return (int) (e2.getStartTime() - e1.getStartTime());
+ }
+ };
+}
diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEventListener.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEventListener.java
new file mode 100644
index 000000000..17fc545c9
--- /dev/null
+++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcEventListener.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2014 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.spectator.gc;
+
+/** Listener for GC events. */
+public interface GcEventListener {
+ /** Invoked after a GC event occurs. */
+ void onComplete(GcEvent event);
+}
diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcLogger.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcLogger.java
new file mode 100644
index 000000000..ee211a998
--- /dev/null
+++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcLogger.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright 2014 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.spectator.gc;
+
+import com.netflix.spectator.api.Counter;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Spectator;
+import com.netflix.spectator.api.Timer;
+import com.netflix.spectator.impl.Preconditions;
+import com.sun.management.GarbageCollectionNotificationInfo;
+import com.sun.management.GcInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.management.ListenerNotFoundException;
+import javax.management.Notification;
+import javax.management.NotificationEmitter;
+import javax.management.NotificationListener;
+import javax.management.openmbean.CompositeData;
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MemoryPoolMXBean;
+import java.lang.management.MemoryUsage;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Logger to collect GC notifcation events.
+ */
+public final class GcLogger {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(GcLogger.class);
+
+ // One major GC per hour would require 168 for a week
+ // One minor GC per minute would require 180 for three hours
+ private static final int BUFFER_SIZE = 256;
+
+ // Max size of old generation memory pool
+ private static final AtomicLong MAX_DATA_SIZE =
+ Spectator.registry().gauge("jvm.gc.maxDataSize", new AtomicLong(0L));
+
+ // Size of old generation memory pool after a full GC
+ private static final AtomicLong LIVE_DATA_SIZE =
+ Spectator.registry().gauge("jvm.gc.liveDataSize", new AtomicLong(0L));
+
+ // Incremented for any positive increases in the size of the old generation memory pool
+ // before GC to after GC
+ private static final Counter PROMOTION_RATE =
+ Spectator.registry().counter("jvm.gc.promotionRate");
+
+ // Incremented for the increase in the size of the young generation memory pool after one GC
+ // to before the next
+ private static final Counter ALLOCATION_RATE =
+ Spectator.registry().counter("jvm.gc.allocationRate");
+
+ // Pause time due to GC event
+ private static final Id PAUSE_TIME = Spectator.registry().createId("jvm.gc.pause");
+
+ private final long jvmStartTime;
+
+ private final ConcurrentHashMap> gcLogs = new ConcurrentHashMap<>();
+
+ private long youngGenSizeAfter = 0L;
+
+ private String youngGenPoolName = null;
+ private String oldGenPoolName = null;
+
+ private GcNotificationListener notifListener = null;
+
+ private GcEventListener eventListener = null;
+
+ /** Create a new instance. */
+ public GcLogger() {
+ jvmStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
+ for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
+ CircularBuffer buffer = new CircularBuffer<>(BUFFER_SIZE);
+ gcLogs.put(mbean.getName(), buffer);
+ }
+
+ for (MemoryPoolMXBean mbean : ManagementFactory.getMemoryPoolMXBeans()) {
+ if (HelperFunctions.isYoungGenPool(mbean.getName())) {
+ youngGenPoolName = mbean.getName();
+ }
+ if (HelperFunctions.isOldGenPool(mbean.getName())) {
+ oldGenPoolName = mbean.getName();
+ }
+ }
+ }
+
+ /**
+ * Start collecting data about GC events.
+ *
+ * @param listener
+ * If not null, the listener will be called with the event objects after metrics and the
+ * log buffer is updated.
+ */
+ public synchronized void start(GcEventListener listener) {
+ Preconditions.checkState(notifListener == null, "logger already started");
+ eventListener = listener;
+ notifListener = new GcNotificationListener();
+ for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
+ if (mbean instanceof NotificationEmitter) {
+ final NotificationEmitter emitter = (NotificationEmitter) mbean;
+ emitter.addNotificationListener(notifListener, null, null);
+ }
+ }
+ }
+
+ /** Stop collecting GC events. */
+ public synchronized void stop() {
+ Preconditions.checkState(notifListener != null, "logger has not been started");
+ for (GarbageCollectorMXBean mbean : ManagementFactory.getGarbageCollectorMXBeans()) {
+ if (mbean instanceof NotificationEmitter) {
+ final NotificationEmitter emitter = (NotificationEmitter) mbean;
+ try {
+ emitter.removeNotificationListener(notifListener);
+ } catch (ListenerNotFoundException e) {
+ LOGGER.warn("could not remove gc listener", e);
+ }
+ }
+ }
+ notifListener = null;
+ }
+
+ /** Return the current set of GC events in the in-memory log. */
+ public List getLogs() {
+ final List logs = new ArrayList<>();
+ for (CircularBuffer buffer : gcLogs.values()) {
+ logs.addAll(buffer.toList());
+ }
+ Collections.sort(logs, GcEvent.REVERSE_TIME_ORDER);
+ return logs;
+ }
+
+ private void updateMetrics(String name, GcInfo info) {
+ final Map before = info.getMemoryUsageBeforeGc();
+ final Map after = info.getMemoryUsageAfterGc();
+
+ if (oldGenPoolName != null) {
+ final long oldBefore = before.get(oldGenPoolName).getUsed();
+ final long oldAfter = after.get(oldGenPoolName).getUsed();
+ final long delta = oldAfter - oldBefore;
+ if (delta > 0L) {
+ PROMOTION_RATE.increment(delta);
+ }
+
+ if (HelperFunctions.getGcType(name) == GcType.OLD) {
+ LIVE_DATA_SIZE.set(oldAfter);
+ final long oldMaxAfter = after.get(oldGenPoolName).getMax();
+ MAX_DATA_SIZE.set(oldMaxAfter);
+ }
+ }
+
+ if (youngGenPoolName != null) {
+ final long youngBefore = before.get(youngGenPoolName).getUsed();
+ final long youngAfter = after.get(youngGenPoolName).getUsed();
+ final long delta = youngBefore - youngGenSizeAfter;
+ youngGenSizeAfter = youngAfter;
+ if (delta > 0L) {
+ ALLOCATION_RATE.increment(delta);
+ }
+ }
+ }
+
+ private void processGcEvent(GarbageCollectionNotificationInfo info) {
+ GcEvent event = new GcEvent(info, jvmStartTime + info.getGcInfo().getStartTime());
+ gcLogs.get(info.getGcName()).add(event);
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info(event.toString());
+ }
+
+ // Update pause timer for the action and cause...
+ Id eventId = PAUSE_TIME
+ .withTag("action", info.getGcAction())
+ .withTag("cause", info.getGcCause());
+ Timer timer = Spectator.registry().timer(eventId);
+ timer.record(info.getGcInfo().getDuration(), TimeUnit.MILLISECONDS);
+
+ // Update promotion and allocation counters
+ updateMetrics(info.getGcName(), info.getGcInfo());
+
+ // Notify an event listener if registered
+ if (eventListener != null) {
+ try {
+ eventListener.onComplete(event);
+ } catch (Exception e) {
+ LOGGER.warn("exception thrown by event listener", e);
+ }
+ }
+ }
+
+ private class GcNotificationListener implements NotificationListener {
+ public void handleNotification(Notification notification, Object ref) {
+ final String type = notification.getType();
+ if (type.equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
+ CompositeData cd = (CompositeData) notification.getUserData();
+ GarbageCollectionNotificationInfo info = GarbageCollectionNotificationInfo.from(cd);
+ processGcEvent(info);
+ }
+ }
+ }
+}
diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcType.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcType.java
new file mode 100644
index 000000000..4b7dce360
--- /dev/null
+++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/GcType.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright 2014 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.spectator.gc;
+
+/**
+ * Simple classification of gc type to avoid reliance on names than can vary.
+ */
+public enum GcType {
+ /** Major collection. */
+ OLD,
+
+ /** Minor collection. */
+ YOUNG,
+
+ /** Could not determine the collection type. */
+ UNKNOWN
+}
diff --git a/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/HelperFunctions.java b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/HelperFunctions.java
new file mode 100644
index 000000000..136d20d11
--- /dev/null
+++ b/spectator-ext-gc/src/main/java/com/netflix/spectator/gc/HelperFunctions.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright 2014 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.spectator.gc;
+
+import com.sun.management.GcInfo;
+
+import java.lang.management.MemoryUsage;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Utility functions for GC. */
+final class HelperFunctions {
+
+ private static final Map KNOWN_COLLECTOR_NAMES = knownCollectors();
+
+ private HelperFunctions() {
+ }
+
+ private static Map knownCollectors() {
+ Map m = new HashMap<>();
+ m.put("ConcurrentMarkSweep", GcType.OLD);
+ m.put("Copy", GcType.YOUNG);
+ m.put("G1 Old Generation", GcType.OLD);
+ m.put("G1 Young Generation", GcType.YOUNG);
+ m.put("MarkSweepCompact", GcType.OLD);
+ m.put("PS MarkSweep", GcType.OLD);
+ m.put("PS Scavenge", GcType.YOUNG);
+ m.put("ParNew", GcType.YOUNG);
+ return Collections.unmodifiableMap(m);
+ }
+
+ /** Determine the type, old or young, based on the name of the collector. */
+ static GcType getGcType(String name) {
+ GcType t = KNOWN_COLLECTOR_NAMES.get(name);
+ return (t == null) ? GcType.UNKNOWN : t;
+ }
+
+ /** Returns true if memory pool name matches an old generation pool. */
+ static boolean isOldGenPool(String name) {
+ return name.endsWith("Old Gen") || name.endsWith("Tenured Gen");
+ }
+
+ /** Returns true if memory pool name matches an young generation pool. */
+ static boolean isYoungGenPool(String name) {
+ return name.endsWith("Eden Space");
+ }
+
+ /** Compute the total usage across all pools. */
+ static long getTotalUsage(Map usages) {
+ long sum = 0L;
+ for (Map.Entry e : usages.entrySet()) {
+ sum += e.getValue().getUsed();
+ }
+ return sum;
+ }
+
+ /** Compute the max usage across all pools. */
+ static long getTotalMaxUsage(Map usages) {
+ long sum = 0L;
+ for (Map.Entry e : usages.entrySet()) {
+ long max = e.getValue().getMax();
+ if (max > 0) {
+ sum += e.getValue().getMax();
+ }
+ }
+ return sum;
+ }
+
+ /** Compute the amount of data promoted during a GC event. */
+ static long getPromotionSize(GcInfo info) {
+ long totalBefore = getTotalUsage(info.getMemoryUsageBeforeGc());
+ long totalAfter = getTotalUsage(info.getMemoryUsageAfterGc());
+ return totalAfter - totalBefore;
+ }
+}
diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/nflx/ChronosGcEventListener.java b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/ChronosGcEventListener.java
new file mode 100644
index 000000000..ab0ae9aa6
--- /dev/null
+++ b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/ChronosGcEventListener.java
@@ -0,0 +1,131 @@
+/**
+ * Copyright 2014 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.spectator.nflx;
+
+import com.netflix.client.http.HttpRequest;
+import com.netflix.client.http.HttpResponse;
+import com.netflix.config.DynamicBooleanProperty;
+import com.netflix.config.DynamicPropertyFactory;
+import com.netflix.niws.client.http.RestClient;
+import com.netflix.spectator.gc.GcEvent;
+import com.netflix.spectator.gc.GcEventListener;
+import com.netflix.spectator.ribbon.RestClientFactory;
+import com.sun.management.GcInfo;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Listener that sends GC events to a chronos backend.
+ */
+public class ChronosGcEventListener implements GcEventListener {
+
+ private static final DynamicBooleanProperty ENABLED =
+ DynamicPropertyFactory.getInstance().getBooleanProperty("spectator.gc.chronosEnabled", true);
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ private final ExecutorService executor = Executors.newSingleThreadExecutor(
+ new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r, "ChronosGcEventListener");
+ t.setDaemon(true);
+ return t;
+ }
+ });
+
+ private final RestClient client = RestClientFactory.getClient("chronos_gc");
+
+ private String getenv(String k) {
+ String v = System.getenv(k);
+ return (v == null || v.length() == 0) ? "unknown" : v;
+ }
+
+ /** Convert a GC event into a map. */
+ Map toGcInfoMap(GcEvent event) {
+ final GcInfo info = event.getInfo().getGcInfo();
+ Map map = new HashMap<>();
+ map.put("id", info.getId());
+ map.put("startTime", event.getStartTime());
+ map.put("endTime", event.getStartTime() + info.getEndTime());
+ map.put("duration", info.getDuration());
+ map.put("memoryBeforeGc", info.getMemoryUsageBeforeGc());
+ map.put("memoryAfterGc", info.getMemoryUsageAfterGc());
+ return map;
+ }
+
+ /** Convert a GC event into a map. */
+ Map toEventMap(GcEvent event) {
+ Map map = new HashMap<>();
+ map.put("action", event.getInfo().getGcAction());
+ map.put("cause", event.getInfo().getGcCause());
+ map.put("name", event.getName());
+ map.put("gcInfo", toGcInfoMap(event));
+ map.put("app", getenv("NETFLIX_APP"));
+ map.put("cluster", getenv("NETFLIX_CLUSTER"));
+ map.put("asg", getenv("NETFLIX_AUTO_SCALE_GROUP"));
+ map.put("region", getenv("EC2_REGION"));
+ map.put("zone", getenv("EC2_AVAILABILITY_ZONE"));
+ map.put("ami", getenv("EC2_AMI_ID"));
+ map.put("node", getenv("EC2_INSTANCE_ID"));
+ return map;
+ }
+
+ @Override
+ public void onComplete(final GcEvent event) {
+ if (!ENABLED.get()) {
+ return;
+ }
+
+ try {
+ final byte[] json = mapper.writeValueAsBytes(toEventMap(event));
+ executor.submit(new Runnable() {
+ public void run() {
+ HttpRequest request = new HttpRequest.Builder()
+ .verb(HttpRequest.Verb.POST)
+ .uri(URI.create("/api/v2/event"))
+ .header("Content-Type", "application/json")
+ .entity(json)
+ .build();
+ try (HttpResponse response = client.executeWithLoadBalancer(request)) {
+ if (response.getStatus() != 200) {
+ logger.warn("failed to send GC event to chronos (status={})", response.getStatus());
+ }
+ } catch (Exception e) {
+ logger.warn("failed to send GC event to chronos", e);
+ }
+ }
+ });
+ } catch (IOException e) {
+ logger.warn("failed to send GC event to chronos", e);
+ }
+ }
+
+ /** Shutdown the executor used to send data to chronos. */
+ public void shutdown() {
+ executor.shutdown();
+ }
+}
diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/nflx/Plugin.java b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/Plugin.java
new file mode 100644
index 000000000..b3ef4e90f
--- /dev/null
+++ b/spectator-nflx/src/main/java/com/netflix/spectator/nflx/Plugin.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2014 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.spectator.nflx;
+
+import com.netflix.config.ConfigurationManager;
+import com.netflix.governator.annotations.AutoBindSingleton;
+import com.netflix.spectator.gc.GcLogger;
+import org.apache.commons.configuration.AbstractConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+
+/**
+ * Plugin for setting up spectator to report correctly into the standard Netflix stack.
+ */
+@AutoBindSingleton
+public final class Plugin {
+
+ private static final String CONFIG_FILE = "spectator.properties";
+
+ private static final GcLogger GC_LOGGER = new GcLogger();
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Plugin.class);
+
+ @PostConstruct
+ private void init() throws IOException {
+ ConfigurationManager.loadPropertiesFromResources(CONFIG_FILE);
+ AbstractConfiguration config = ConfigurationManager.getConfigInstance();
+ if (config.getBoolean("spectator.gc.loggingEnabled")) {
+ GC_LOGGER.start(new ChronosGcEventListener());
+ LOGGER.info("gc logging started");
+ } else {
+ LOGGER.info("gc logging is not enabled");
+ }
+ }
+}
diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/MeteredRestClient.java b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/MeteredRestClient.java
new file mode 100644
index 000000000..f12f2d2f2
--- /dev/null
+++ b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/MeteredRestClient.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2014 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.spectator.ribbon;
+
+import com.netflix.client.ClientException;
+import com.netflix.client.config.IClientConfig;
+import com.netflix.client.http.HttpRequest;
+import com.netflix.client.http.HttpResponse;
+import com.netflix.niws.client.http.RestClient;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Spectator;
+import com.netflix.spectator.api.Timer;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Subclass that provides instrumentation of requests, latency, and failures.
+ */
+public class MeteredRestClient extends RestClient {
+
+ private Timer latency;
+
+ private Id requests;
+ private Id exceptions;
+
+ private Id niwsRequests;
+ private Id niwsExceptions;
+
+ @Override
+ public void initWithNiwsConfig(IClientConfig config) {
+ super.initWithNiwsConfig(config);
+
+ final String client = "client";
+ final String cname = getClientName();
+ latency = Spectator.registry().timer("ribbon.http.latency", client, cname);
+
+ requests = Spectator.registry().createId("ribbon.http.requests", client, cname);
+ exceptions = Spectator.registry().createId("ribbon.http.exceptions", client, cname);
+
+ niwsRequests = Spectator.registry().createId("ribbon.http.niwsRequests", client, cname);
+ niwsExceptions = Spectator.registry().createId("ribbon.http.niwsExceptions", client, cname);
+ }
+
+ @Override
+ public HttpResponse execute(HttpRequest req) throws Exception {
+ final long start = System.nanoTime();
+ try {
+ final HttpResponse res = super.execute(req);
+ final String status = String.format("%d", res.getStatus());
+ Spectator.registry().counter(requests.withTag("status", status)).increment();
+ return res;
+ } catch (ClientException e) {
+ final String m = e.getErrorType().name();
+ Spectator.registry().counter(exceptions.withTag("error", m)).increment();
+ throw e;
+ } catch (Exception e) {
+ final String c = e.getClass().getSimpleName();
+ Spectator.registry().counter(exceptions.withTag("error", c)).increment();
+ throw e;
+ } finally {
+ latency.record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ @Override
+ public HttpResponse executeWithLoadBalancer(HttpRequest req) throws ClientException {
+ final long start = System.nanoTime();
+ try {
+ final HttpResponse res = super.executeWithLoadBalancer(req);
+ final String status = String.format("%d", res.getStatus());
+ Spectator.registry().counter(niwsRequests.withTag("status", status)).increment();
+ return res;
+ } catch (ClientException e) {
+ final String m = e.getErrorType().name();
+ Spectator.registry().counter(niwsExceptions.withTag("error", m)).increment();
+ throw e;
+ } catch (Exception e) {
+ final String c = e.getClass().getSimpleName();
+ Spectator.registry().counter(niwsExceptions.withTag("error", c)).increment();
+ throw e;
+ } finally {
+ latency.record(System.nanoTime() - start, TimeUnit.NANOSECONDS);
+ }
+ }
+}
diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RestClientFactory.java b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RestClientFactory.java
new file mode 100644
index 000000000..fcb61d0d1
--- /dev/null
+++ b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RestClientFactory.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright 2014 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.spectator.ribbon;
+
+import com.netflix.client.ClientFactory;
+import com.netflix.niws.client.http.RestClient;
+
+/**
+ * Helper for creating a {@link com.netflix.niws.client.http.RestClient} using the spectator
+ * client config implementation.
+ */
+public final class RestClientFactory {
+ private RestClientFactory() {
+ }
+
+ /**
+ * Get or create a {@link com.netflix.niws.client.http.RestClient} with the specified name. The
+ * client will use the {@link com.netflix.spectator.ribbon.RibbonClientConfigImpl} that changes
+ * some of the defaults to make the common cases work easier:
+ *
+ *
+ * - Namespace for the clients defaults to {@code niws.client} to avoid property name changes
+ * if switching between internal {@code platform-ipc} and {@code ribbon}.
+ * - The default server list class is set to {@code DiscoveryEnabledNIWSServerList}.
+ * - An instrumented RestClient class is returned.
+ *
+ *
+ * @param name
+ * Name of the client to retrieve.
+ * @return
+ * Rest client for the specified name.
+ */
+ public static RestClient getClient(String name) {
+ return (RestClient) ClientFactory.getNamedClient(name, RibbonClientConfigImpl.class);
+ }
+}
diff --git a/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RibbonClientConfigImpl.java b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RibbonClientConfigImpl.java
new file mode 100644
index 000000000..19f086f56
--- /dev/null
+++ b/spectator-nflx/src/main/java/com/netflix/spectator/ribbon/RibbonClientConfigImpl.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2014 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.spectator.ribbon;
+
+import com.netflix.client.config.DefaultClientConfigImpl;
+
+/**
+ * Customize some of the default settings used for rest clients.
+ */
+public class RibbonClientConfigImpl extends DefaultClientConfigImpl {
+
+ @Override
+ public String getNameSpace() {
+ return "niws.client";
+ }
+
+ @Override
+ public String getDefaultClientClassname() {
+ return "com.netflix.spectator.ribbon.MeteredRestClient";
+ }
+
+ @Override
+ public String getDefaultSeverListClass() {
+ return "com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList";
+ }
+}
diff --git a/spectator-nflx/src/main/resources/spectator.properties b/spectator-nflx/src/main/resources/spectator.properties
new file mode 100644
index 000000000..8c2f3cf42
--- /dev/null
+++ b/spectator-nflx/src/main/resources/spectator.properties
@@ -0,0 +1,32 @@
+#
+# Copyright 2014 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.
+#
+
+
+# Should we enable gc logging? Only checked at startup.
+spectator.gc.loggingEnabled=true
+
+# Should we send gc events to chronos backend? Logging must be enabled. This property is only
+# checked at startup.
+spectator.gc.chronosEnabled=true
+
+# Rest client for chronos gc backend
+chronos_gc.niws.client.AppName=CHRONOS_BACKEND
+chronos_gc.niws.client.ReadTimeout=15000
+chronos_gc.niws.client.ConnectTimeout=5000
+chronos_gc.niws.client.MaxAutoRetries=0
+chronos_gc.niws.client.MaxAutoRetriesNextServer=2
+chronos_gc.niws.client.OkToRetryOnAllOperations=true
+chronos_gc.niws.client.DeploymentContextBasedVipAddresses=chronos_backend-gc:7001
diff --git a/spectator-nflx/src/test/java/com/netflix/spectator/ribbon/MeteredRestClientTest.java b/spectator-nflx/src/test/java/com/netflix/spectator/ribbon/MeteredRestClientTest.java
new file mode 100644
index 000000000..307325941
--- /dev/null
+++ b/spectator-nflx/src/test/java/com/netflix/spectator/ribbon/MeteredRestClientTest.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2014 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.spectator.ribbon;
+
+import com.netflix.client.http.HttpRequest;
+import com.netflix.client.http.HttpResponse;
+import com.netflix.niws.client.http.RestClient;
+import com.netflix.spectator.api.ExtendedRegistry;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Spectator;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+
+@RunWith(JUnit4.class)
+public class MeteredRestClientTest {
+
+ private static final String client = "MeteredRestClientTest";
+
+ private static HttpServer server;
+ private static int port;
+
+ @BeforeClass
+ public static void startServer() throws Exception {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ port = server.getAddress().getPort();
+
+ server.createContext("/ok", new HttpHandler() {
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ exchange.sendResponseHeaders(200, 0L);
+ exchange.close();
+ }
+ });
+
+ server.start();
+
+ System.setProperty(client + ".niws.client.NIWSServerListClassName",
+ "com.netflix.loadbalancer.ConfigurationBasedServerList");
+ System.setProperty(client + ".niws.client.listOfServers",
+ "localhost:" + port);
+ }
+
+ @AfterClass
+ public static void stopServer() {
+ server.stop(0);
+ }
+
+ private int get(String loc) {
+ URI uri = URI.create(loc);
+ HttpRequest req = new HttpRequest.Builder()
+ .verb(HttpRequest.Verb.GET)
+ .uri(uri)
+ .build();
+ RestClient c = RestClientFactory.getClient(client);
+ try (HttpResponse res = uri.isAbsolute() ? c.execute(req) : c.executeWithLoadBalancer(req)) {
+ return res.getStatus();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return -1;
+ }
+ }
+
+ private long reqCount(int status) {
+ ExtendedRegistry r = Spectator.registry();
+ Id requests = r.createId("ribbon.http.requests", "client", client, "status", "" + status);
+ return r.counter(requests).count();
+ }
+
+ private long niwsReqCount(int status) {
+ ExtendedRegistry r = Spectator.registry();
+ Id requests = r.createId("ribbon.http.niwsRequests", "client", client, "status", "" + status);
+ return r.counter(requests).count();
+ }
+
+ @Test
+ public void executeOk() {
+ long before = reqCount(200);
+ Assert.assertEquals(get("http://localhost:" + port + "/ok"), 200);
+ Assert.assertEquals(reqCount(200), before + 1);
+ }
+
+ @Test
+ public void executeNotFound() {
+ long before200 = reqCount(200);
+ long before404 = reqCount(404);
+ Assert.assertEquals(get("http://localhost:" + port + "/not-found"), 404);
+ Assert.assertEquals(reqCount(200), before200);
+ Assert.assertEquals(reqCount(404), before404 + 1);
+ }
+
+ @Test
+ public void executeWithLbOk() {
+ long before = reqCount(200);
+ long nbefore = niwsReqCount(200);
+ Assert.assertEquals(get("/ok"), 200);
+ Assert.assertEquals(reqCount(200), before + 1);
+ Assert.assertEquals(niwsReqCount(200), nbefore + 1);
+ }
+
+ @Test
+ public void executeWithLbNotFound() {
+ long before200 = niwsReqCount(200);
+ long before404 = niwsReqCount(404);
+ Assert.assertEquals(get("/not-found"), 404);
+ Assert.assertEquals(niwsReqCount(200), before200);
+ Assert.assertEquals(niwsReqCount(404), before404 + 1);
+ }
+}
+
diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsCounter.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsCounter.java
new file mode 100644
index 000000000..de72bf4fa
--- /dev/null
+++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsCounter.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2014 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.spectator.metrics2;
+
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.Counter;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+
+import java.util.Collections;
+
+/** Counter implementation for the metrics2 registry. */
+class MetricsCounter implements Counter {
+
+ private final Clock clock;
+ private final Id id;
+ private final com.yammer.metrics.core.Meter impl;
+
+ /** Create a new instance. */
+ MetricsCounter(Clock clock, Id id, com.yammer.metrics.core.Meter impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ long now = clock.wallTime();
+ long v = impl.count();
+ return Collections.singleton(new Measurement(id, now, v));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void increment() {
+ impl.mark();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void increment(long amount) {
+ impl.mark(amount);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return impl.count();
+ }
+}
diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsDistributionSummary.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsDistributionSummary.java
new file mode 100644
index 000000000..78e5baf72
--- /dev/null
+++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsDistributionSummary.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright 2014 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.spectator.metrics2;
+
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.DistributionSummary;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+
+import java.util.Collections;
+
+/** Distribution summary implementation for the metric2 registry. */
+class MetricsDistributionSummary implements DistributionSummary {
+
+ private final Clock clock;
+ private final Id id;
+ private final com.yammer.metrics.core.Histogram impl;
+
+ /** Create a new instance. */
+ MetricsDistributionSummary(Clock clock, Id id, com.yammer.metrics.core.Histogram impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(long amount) {
+ impl.update(amount);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ final long now = clock.wallTime();
+ return Collections.singleton(new Measurement(id, now, impl.mean()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return impl.count();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long totalAmount() {
+ return (long) impl.sum();
+ }
+}
diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsRegistry.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsRegistry.java
new file mode 100644
index 000000000..06f1d6b5f
--- /dev/null
+++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsRegistry.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2014 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.spectator.metrics2;
+
+import com.netflix.spectator.api.*;
+import com.yammer.metrics.Metrics;
+import com.yammer.metrics.core.MetricName;
+
+import java.util.concurrent.TimeUnit;
+
+/** Registry implementation that maps spectator types to the metrics2 library. */
+public class MetricsRegistry extends AbstractRegistry {
+
+ private final com.yammer.metrics.core.MetricsRegistry impl;
+
+ /** Create a new instance. */
+ public MetricsRegistry() {
+ this(Clock.SYSTEM, Metrics.defaultRegistry());
+ }
+
+ /** Create a new instance. */
+ public MetricsRegistry(Clock clock, com.yammer.metrics.core.MetricsRegistry impl) {
+ super(clock);
+ this.impl = impl;
+ }
+
+ private MetricName toMetricName(Id id) {
+ final String name = id.name();
+ final int pos = name.lastIndexOf(".");
+ if (pos != -1) {
+ final String prefix = name.substring(0, pos);
+ final String suffix = name.substring(pos + 1);
+ return new MetricName("spectator", prefix, suffix);
+ } else {
+ return new MetricName("spectator", "default", id.name());
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Counter newCounter(Id id) {
+ final MetricName name = toMetricName(id);
+ return new MetricsCounter(clock(), id, impl.newMeter(name, "calls", TimeUnit.SECONDS));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected DistributionSummary newDistributionSummary(Id id) {
+ final MetricName name = toMetricName(id);
+ return new MetricsDistributionSummary(clock(), id, impl.newHistogram(name, false));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Timer newTimer(Id id) {
+ final MetricName name = toMetricName(id);
+ return new MetricsTimer(clock(), id, impl.newTimer(name, TimeUnit.SECONDS, TimeUnit.SECONDS));
+ }
+}
diff --git a/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsTimer.java b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsTimer.java
new file mode 100644
index 000000000..dc2aecd21
--- /dev/null
+++ b/spectator-reg-metrics2/src/main/java/com/netflix/spectator/metrics2/MetricsTimer.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright 2014 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.spectator.metrics2;
+
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+import com.netflix.spectator.api.Timer;
+
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+/** Timer implementation for the metrics2 registry. */
+class MetricsTimer implements Timer {
+
+ private final Clock clock;
+ private final Id id;
+ private final com.yammer.metrics.core.Timer impl;
+
+ /** Create a new instance. */
+ MetricsTimer(Clock clock, Id id, com.yammer.metrics.core.Timer impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(long amount, TimeUnit unit) {
+ impl.update(amount, unit);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ final long now = clock.wallTime();
+ return Collections.singleton(new Measurement(id, now, impl.meanRate()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public T record(Callable f) throws Exception {
+ final long s = clock.monotonicTime();
+ try {
+ return f.call();
+ } finally {
+ final long e = clock.monotonicTime();
+ record(e - s, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(Runnable f) {
+ final long s = clock.monotonicTime();
+ try {
+ f.run();
+ } finally {
+ final long e = clock.monotonicTime();
+ record(e - s, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return impl.count();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long totalTime() {
+ return (long) impl.sum();
+ }
+}
diff --git a/spectator-reg-metrics2/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry b/spectator-reg-metrics2/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry
new file mode 100644
index 000000000..dc8ac67dc
--- /dev/null
+++ b/spectator-reg-metrics2/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry
@@ -0,0 +1 @@
+com.netflix.spectator.metrics2.MetricsRegistry
diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsCounter.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsCounter.java
new file mode 100644
index 000000000..08e8d33bb
--- /dev/null
+++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsCounter.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2014 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.spectator.metrics3;
+
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.Counter;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+
+import java.util.Collections;
+
+/** Counter implementation for the metric3 registry. */
+class MetricsCounter implements Counter {
+
+ private final Clock clock;
+ private final Id id;
+ private final com.codahale.metrics.Meter impl;
+
+ /** Create a new instance. */
+ MetricsCounter(Clock clock, Id id, com.codahale.metrics.Meter impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ long now = clock.wallTime();
+ long v = impl.getCount();
+ return Collections.singleton(new Measurement(id, now, v));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void increment() {
+ impl.mark();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void increment(long amount) {
+ impl.mark(amount);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return impl.getCount();
+ }
+}
diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsDistributionSummary.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsDistributionSummary.java
new file mode 100644
index 000000000..7d103ed32
--- /dev/null
+++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsDistributionSummary.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2014 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.spectator.metrics3;
+
+import com.codahale.metrics.Snapshot;
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.DistributionSummary;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Distribution summary implementation for the metric3 registry. */
+class MetricsDistributionSummary implements DistributionSummary {
+
+ private final Clock clock;
+ private final Id id;
+ private final com.codahale.metrics.Histogram impl;
+ private final AtomicLong totalAmount;
+
+ /** Create a new instance. */
+ MetricsDistributionSummary(Clock clock, Id id, com.codahale.metrics.Histogram impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ this.totalAmount = new AtomicLong(0L);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(long amount) {
+ impl.update(amount);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ final long now = clock.wallTime();
+ final Snapshot snapshot = impl.getSnapshot();
+ return Collections.singleton(new Measurement(id, now, snapshot.getMean()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return impl.getCount();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long totalAmount() {
+ return totalAmount.get();
+ }
+}
diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsRegistry.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsRegistry.java
new file mode 100644
index 000000000..3666693e2
--- /dev/null
+++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsRegistry.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright 2014 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.spectator.metrics3;
+
+import com.netflix.spectator.api.*;
+
+/** Registry implementation that maps spectator types to the metrics3 library. */
+public class MetricsRegistry extends AbstractRegistry {
+
+ private final com.codahale.metrics.MetricRegistry impl;
+
+ /** Create a new instance. */
+ public MetricsRegistry() {
+ this(Clock.SYSTEM, new com.codahale.metrics.MetricRegistry());
+ }
+
+ /** Create a new instance. */
+ public MetricsRegistry(Clock clock, com.codahale.metrics.MetricRegistry impl) {
+ super(clock);
+ this.impl = impl;
+ }
+
+ private String toMetricName(Id id) {
+ StringBuilder buf = new StringBuilder();
+ buf.append(id.name());
+ for (Tag t : id.tags()) {
+ buf.append(t.key()).append("-").append(t.value());
+ }
+ return buf.toString();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Counter newCounter(Id id) {
+ final String name = toMetricName(id);
+ return new MetricsCounter(clock(), id, impl.meter(name));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected DistributionSummary newDistributionSummary(Id id) {
+ final String name = toMetricName(id);
+ return new MetricsDistributionSummary(clock(), id, impl.histogram(name));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Timer newTimer(Id id) {
+ final String name = toMetricName(id);
+ return new MetricsTimer(clock(), id, impl.timer(name));
+ }
+}
diff --git a/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsTimer.java b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsTimer.java
new file mode 100644
index 000000000..67779a36e
--- /dev/null
+++ b/spectator-reg-metrics3/src/main/java/com/netflix/spectator/metrics3/MetricsTimer.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright 2014 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.spectator.metrics3;
+
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+import com.netflix.spectator.api.Timer;
+
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Timer implementation for the metrics3 registry. */
+class MetricsTimer implements Timer {
+
+ private final Clock clock;
+ private final Id id;
+ private final com.codahale.metrics.Timer impl;
+ private final AtomicLong totalTime;
+
+ /** Create a new instance. */
+ MetricsTimer(Clock clock, Id id, com.codahale.metrics.Timer impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ this.totalTime = new AtomicLong(0L);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(long amount, TimeUnit unit) {
+ impl.update(amount, unit);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ final long now = clock.wallTime();
+ return Collections.singleton(new Measurement(id, now, impl.getMeanRate()));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public T record(Callable f) throws Exception {
+ final long s = clock.monotonicTime();
+ try {
+ return f.call();
+ } finally {
+ final long e = clock.monotonicTime();
+ record(e - s, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(Runnable f) {
+ final long s = clock.monotonicTime();
+ try {
+ f.run();
+ } finally {
+ final long e = clock.monotonicTime();
+ record(e - s, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return impl.getCount();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long totalTime() {
+ return totalTime.get();
+ }
+}
diff --git a/spectator-reg-metrics3/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry b/spectator-reg-metrics3/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry
new file mode 100644
index 000000000..de9ad58ee
--- /dev/null
+++ b/spectator-reg-metrics3/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry
@@ -0,0 +1 @@
+com.netflix.spectator.metrics3.MetricsRegistry
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoCounter.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoCounter.java
new file mode 100644
index 000000000..c27ec79c7
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoCounter.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.servo.monitor.Monitor;
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.Counter;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Counter implementation for the servo registry. */
+class ServoCounter implements Counter, ServoMeter {
+
+ private final Clock clock;
+ private final ServoId id;
+ private final com.netflix.servo.monitor.Counter impl;
+
+ // Local count so that we have more flexibility on servo counter impl without changing the
+ // value returned by the {@link #count()} method.
+ private final AtomicLong count;
+
+ /** Create a new instance. */
+ ServoCounter(Clock clock, ServoId id, com.netflix.servo.monitor.Counter impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ this.count = new AtomicLong(0L);
+ }
+
+ @Override
+ public Monitor> monitor() {
+ return impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ long now = clock.wallTime();
+ long v = count.get();
+ return Collections.singleton(new Measurement(id, now, v));
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void increment() {
+ impl.increment();
+ count.incrementAndGet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void increment(long amount) {
+ impl.increment(amount);
+ count.addAndGet(amount);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return count.get();
+ }
+}
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java
new file mode 100644
index 000000000..d74c68c69
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoDistributionSummary.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.servo.monitor.BasicDistributionSummary;
+import com.netflix.servo.monitor.Monitor;
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.DistributionSummary;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Distribution summary implementation for the servo registry. */
+class ServoDistributionSummary implements DistributionSummary, ServoMeter {
+
+ private final Clock clock;
+ private final ServoId id;
+ private final BasicDistributionSummary impl;
+
+ // Local count so that we have more flexibility on servo counter impl without changing the
+ // value returned by the {@link #count()} method.
+ private final AtomicLong count;
+ private final AtomicLong totalAmount;
+
+ private final Id countId;
+ private final Id totalAmountId;
+
+ /** Create a new instance. */
+ ServoDistributionSummary(Clock clock, ServoId id, BasicDistributionSummary impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ this.count = new AtomicLong(0L);
+ this.totalAmount = new AtomicLong(0L);
+ countId = id.withTag("statistic", "count");
+ totalAmountId = id.withTag("statistic", "totalAmount");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Monitor> monitor() {
+ return impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(long amount) {
+ impl.record(amount);
+ totalAmount.addAndGet(amount);
+ count.incrementAndGet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ final long now = clock.wallTime();
+ final List ms = new ArrayList<>(2);
+ ms.add(new Measurement(countId, now, count.get()));
+ ms.add(new Measurement(totalAmountId, now, totalAmount.get()));
+ return ms;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return count.get();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long totalAmount() {
+ return totalAmount.get();
+ }
+}
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoId.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoId.java
new file mode 100644
index 000000000..2d03a021f
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoId.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.servo.monitor.MonitorConfig;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Tag;
+
+import java.util.Iterator;
+
+/** Id implementation for the servo registry. */
+class ServoId implements Id {
+
+ private final MonitorConfig config;
+
+ /** Create a new instance. */
+ public ServoId(MonitorConfig config) {
+ this.config = config;
+ }
+
+ /** Return the monitor config this id is based on. */
+ MonitorConfig config() {
+ return config;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String name() {
+ return config.getName();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable tags() {
+ return new Iterable() {
+ public Iterator iterator() {
+ return new Iterator() {
+ private final Iterator iter = config.getTags().iterator();
+
+ public boolean hasNext() {
+ return iter.hasNext();
+ }
+
+ public Tag next() {
+ return new ServoTag(iter.next());
+ }
+
+ public void remove() {
+ iter.remove();
+ }
+ };
+ }
+ };
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id withTag(String k, String v) {
+ return new ServoId((new MonitorConfig.Builder(config)).withTag(k, v).build());
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id withTag(Tag t) {
+ return withTag(t.key(), t.value());
+ }
+}
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoMeter.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoMeter.java
new file mode 100644
index 000000000..373565b25
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoMeter.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.servo.monitor.Monitor;
+
+/** Meter that can return a servo monitor. */
+interface ServoMeter {
+ /** Returns the monitor corresponding to this meter. */
+ Monitor> monitor();
+}
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoRegistry.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoRegistry.java
new file mode 100644
index 000000000..4d7e310e9
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoRegistry.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.servo.DefaultMonitorRegistry;
+import com.netflix.servo.monitor.*;
+import com.netflix.spectator.api.*;
+import com.netflix.spectator.api.Counter;
+import com.netflix.spectator.api.Timer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/** Registry that maps spectator types to servo. */
+public class ServoRegistry extends AbstractRegistry implements CompositeMonitor {
+
+ private static final MonitorConfig DEFAULT_CONFIG =
+ (new MonitorConfig.Builder("spectator.registry")).build();
+
+ private final MonitorConfig config;
+
+ /** Create a new instance. */
+ public ServoRegistry() {
+ this(Clock.SYSTEM);
+ }
+
+ /** Create a new instance. */
+ public ServoRegistry(Clock clock) {
+ this(clock, DEFAULT_CONFIG);
+ }
+
+ /** Create a new instance. */
+ ServoRegistry(Clock clock, MonitorConfig config) {
+ super(clock);
+ this.config = config;
+ DefaultMonitorRegistry.getInstance().register(this);
+ }
+
+ private MonitorConfig toMonitorConfig(Id id) {
+ MonitorConfig.Builder builder = new MonitorConfig.Builder(id.name());
+ for (Tag t : id.tags()) {
+ builder.withTag(t.key(), t.value());
+ }
+ return builder.build();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Counter newCounter(Id id) {
+ MonitorConfig cfg = toMonitorConfig(id);
+ StepCounter counter = new StepCounter(cfg);
+ return new ServoCounter(clock(), new ServoId(cfg), counter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected DistributionSummary newDistributionSummary(Id id) {
+ MonitorConfig cfg = toMonitorConfig(id);
+ BasicDistributionSummary distributionSummary = new BasicDistributionSummary(cfg);
+ return new ServoDistributionSummary(clock(), new ServoId(cfg), distributionSummary);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected Timer newTimer(Id id) {
+ MonitorConfig cfg = toMonitorConfig(id);
+ BasicTimer timer = new BasicTimer(cfg, TimeUnit.SECONDS);
+ return new ServoTimer(clock(), new ServoId(cfg), timer);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Integer getValue() {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Integer getValue(int pollerIndex) {
+ return 0;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public MonitorConfig getConfig() {
+ return config;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public List> getMonitors() {
+ List> monitors = new ArrayList<>();
+ for (Meter meter : this) {
+ if (meter instanceof ServoMeter) {
+ monitors.add(((ServoMeter) meter).monitor());
+ } else {
+ for (Measurement m : meter.measure()) {
+ monitors.add(new NumberGauge(toMonitorConfig(m.id()), m.value()));
+ }
+ }
+ }
+ return monitors;
+ }
+}
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTag.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTag.java
new file mode 100644
index 000000000..43f538081
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTag.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.spectator.api.Tag;
+
+/** Tag implementation for the servo registry. */
+class ServoTag implements Tag {
+
+ private final com.netflix.servo.tag.Tag tag;
+
+ /** Create a new instance. */
+ public ServoTag(com.netflix.servo.tag.Tag tag) {
+ this.tag = tag;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String key() {
+ return tag.getKey();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String value() {
+ return tag.getValue();
+ }
+}
diff --git a/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java
new file mode 100644
index 000000000..e7d42b6de
--- /dev/null
+++ b/spectator-reg-servo/src/main/java/com/netflix/spectator/servo/ServoTimer.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright 2014 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.spectator.servo;
+
+import com.netflix.servo.monitor.Monitor;
+import com.netflix.spectator.api.Clock;
+import com.netflix.spectator.api.Id;
+import com.netflix.spectator.api.Measurement;
+import com.netflix.spectator.api.Timer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/** Timer implementation for the servo registry. */
+class ServoTimer implements Timer, ServoMeter {
+
+ private final Clock clock;
+ private final ServoId id;
+ private final com.netflix.servo.monitor.Timer impl;
+
+ // Local count so that we have more flexibility on servo counter impl without changing the
+ // value returned by the {@link #count()} method.
+ private final AtomicLong count;
+ private final AtomicLong totalTime;
+
+ private final Id countId;
+ private final Id totalTimeId;
+
+ /** Create a new instance. */
+ ServoTimer(Clock clock, ServoId id, com.netflix.servo.monitor.Timer impl) {
+ this.clock = clock;
+ this.id = id;
+ this.impl = impl;
+ this.count = new AtomicLong(0L);
+ this.totalTime = new AtomicLong(0L);
+ countId = id.withTag("statistic", "count");
+ totalTimeId = id.withTag("statistic", "totalTime");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Monitor> monitor() {
+ return impl;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Id id() {
+ return id;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasExpired() {
+ return false;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(long amount, TimeUnit unit) {
+ final long nanos = unit.toNanos(amount);
+ impl.record(amount, unit);
+ totalTime.addAndGet(nanos);
+ count.incrementAndGet();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Iterable measure() {
+ final long now = clock.wallTime();
+ final List ms = new ArrayList<>(2);
+ ms.add(new Measurement(countId, now, count.get()));
+ ms.add(new Measurement(totalTimeId, now, totalTime.get()));
+ return ms;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public T record(Callable f) throws Exception {
+ final long s = clock.monotonicTime();
+ try {
+ return f.call();
+ } finally {
+ final long e = clock.monotonicTime();
+ record(e - s, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void record(Runnable f) {
+ final long s = clock.monotonicTime();
+ try {
+ f.run();
+ } finally {
+ final long e = clock.monotonicTime();
+ record(e - s, TimeUnit.NANOSECONDS);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long count() {
+ return count.get();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public long totalTime() {
+ return totalTime.get();
+ }
+}
diff --git a/spectator-reg-servo/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry b/spectator-reg-servo/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry
new file mode 100644
index 000000000..a764a73fb
--- /dev/null
+++ b/spectator-reg-servo/src/main/resources/META-INF/services/com.netflix.spectator.api.Registry
@@ -0,0 +1 @@
+com.netflix.spectator.servo.ServoRegistry