From 658f31933ca4273c8a5be5794518e1ff36808825 Mon Sep 17 00:00:00 2001 From: Ben McIlwain Date: Thu, 7 Jun 2018 15:39:55 -0700 Subject: [PATCH] Add metrics for the new Check API New metrics are necessary because the new API no longer wraps an EPP flow, therefore does not get metrics for free. Metrics include - An EventMetric for processing time - An IncrementableMetric for request count, with availability (available/reserved/registered) and pricing (standard/premium) fields ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=199708592 --- .../registry/flows/CheckApi2Action.java | 60 +++++++-- .../registry/flows/CheckApiMetrics.java | 70 ++++++++++ .../flows/domain/DomainFlowUtils.java | 2 +- .../monitoring/whitebox/CheckApiMetric.java | 124 ++++++++++++++++++ .../monitoring/whitebox/WhiteboxModule.java | 5 + .../registry/flows/CheckApi2ActionTest.java | 112 +++++++++++++--- .../whitebox/CheckApiMetricTest.java | 48 +++++++ 7 files changed, 391 insertions(+), 30 deletions(-) create mode 100644 java/google/registry/flows/CheckApiMetrics.java create mode 100644 java/google/registry/monitoring/whitebox/CheckApiMetric.java create mode 100644 javatests/google/registry/monitoring/whitebox/CheckApiMetricTest.java diff --git a/java/google/registry/flows/CheckApi2Action.java b/java/google/registry/flows/CheckApi2Action.java index 60bf5d95f..54b9b123a 100644 --- a/java/google/registry/flows/CheckApi2Action.java +++ b/java/google/registry/flows/CheckApi2Action.java @@ -21,6 +21,15 @@ import static google.registry.flows.domain.DomainFlowUtils.validateDomainNameWit import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPredelegation; import static google.registry.model.registry.label.ReservationType.getTypeOfHighestSeverity; import static google.registry.model.registry.label.ReservedList.getReservationTypes; +import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.AVAILABLE; +import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.REGISTERED; +import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.RESERVED; +import static google.registry.monitoring.whitebox.CheckApiMetric.Status.INVALID_NAME; +import static google.registry.monitoring.whitebox.CheckApiMetric.Status.INVALID_REGISTRY_PHASE; +import static google.registry.monitoring.whitebox.CheckApiMetric.Status.SUCCESS; +import static google.registry.monitoring.whitebox.CheckApiMetric.Status.UNKNOWN_ERROR; +import static google.registry.monitoring.whitebox.CheckApiMetric.Tier.PREMINUM; +import static google.registry.monitoring.whitebox.CheckApiMetric.Tier.STANDARD; import static google.registry.pricing.PricingEngineProxy.isDomainPremium; import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static org.json.simple.JSONValue.toJSONString; @@ -33,10 +42,13 @@ import com.google.common.net.InternetDomainName; import com.google.common.net.MediaType; import dagger.Module; import google.registry.flows.domain.DomainFlowUtils.BadCommandForRegistryPhaseException; +import google.registry.flows.domain.DomainFlowUtils.InvalidIdnDomainLabelException; import google.registry.model.domain.DomainResource; import google.registry.model.index.ForeignKeyIndex; import google.registry.model.registry.Registry; import google.registry.model.registry.label.ReservationType; +import google.registry.monitoring.whitebox.CheckApiMetric; +import google.registry.monitoring.whitebox.CheckApiMetric.Availability; import google.registry.request.Action; import google.registry.request.Parameter; import google.registry.request.Response; @@ -66,17 +78,25 @@ public class CheckApi2Action implements Runnable { @Inject Response response; @Inject Clock clock; + @Inject CheckApiMetric.Builder metricBuilder; + @Inject CheckApiMetrics checkApiMetrics; @Inject CheckApi2Action() {} @Override public void run() { - response.setHeader("Content-Disposition", "attachment"); - response.setHeader("X-Content-Type-Options", "nosniff"); - response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - response.setContentType(MediaType.JSON_UTF_8); - response.setPayload(toJSONString(doCheck())); + try { + response.setHeader("Content-Disposition", "attachment"); + response.setHeader("X-Content-Type-Options", "nosniff"); + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + response.setContentType(MediaType.JSON_UTF_8); + response.setPayload(toJSONString(doCheck())); + } finally { + CheckApiMetric metric = metricBuilder.build(); + checkApiMetrics.incrementCheckApiRequest(metric); + checkApiMetrics.recordProcessingTime(metric); + } } private Map doCheck() { @@ -86,6 +106,7 @@ public class CheckApi2Action implements Runnable { domainString = canonicalizeDomainName(nullToEmpty(domain)); domainName = validateDomainName(domainString); } catch (IllegalArgumentException | EppException e) { + metricBuilder.status(INVALID_NAME); return fail("Must supply a valid domain name on an authoritative TLD"); } try { @@ -97,26 +118,37 @@ public class CheckApi2Action implements Runnable { try { verifyNotInPredelegation(registry, now); } catch (BadCommandForRegistryPhaseException e) { + metricBuilder.status(INVALID_REGISTRY_PHASE); return fail("Check in this TLD is not allowed in the current registry phase"); } - String errorMsg = - checkExists(domainString, now) - ? "In use" - : checkReserved(domainName).orElse(null); + boolean isRegistered = checkExists(domainString, now); + Optional reservedError = Optional.empty(); + boolean isReserved = false; + if (!isRegistered) { + reservedError = checkReserved(domainName); + isReserved = reservedError.isPresent(); + } + Availability availability = isRegistered ? REGISTERED : (isReserved ? RESERVED : AVAILABLE); + String errorMsg = isRegistered ? "In use" : (isReserved ? reservedError.get() : null); - boolean available = (errorMsg == null); ImmutableMap.Builder responseBuilder = new ImmutableMap.Builder<>(); - responseBuilder.put("status", "success").put("available", available); - if (available) { - responseBuilder.put("tier", isDomainPremium(domainString, now) ? "premium" : "standard"); + metricBuilder.status(SUCCESS).availability(availability); + responseBuilder.put("status", "success").put("available", availability.equals(AVAILABLE)); + + boolean isPremium = isDomainPremium(domainString, now); + metricBuilder.tier(isPremium ? PREMINUM : STANDARD); + if (availability.equals(AVAILABLE)) { + responseBuilder.put("tier", isPremium ? "premium" : "standard"); } else { responseBuilder.put("reason", errorMsg); } return responseBuilder.build(); - } catch (EppException e) { + } catch (InvalidIdnDomainLabelException e) { + metricBuilder.status(INVALID_NAME); return fail(e.getResult().getMsg()); } catch (Exception e) { + metricBuilder.status(UNKNOWN_ERROR); logger.atWarning().withCause(e).log("Unknown error"); return fail("Invalid request"); } diff --git a/java/google/registry/flows/CheckApiMetrics.java b/java/google/registry/flows/CheckApiMetrics.java new file mode 100644 index 000000000..51d22488b --- /dev/null +++ b/java/google/registry/flows/CheckApiMetrics.java @@ -0,0 +1,70 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.flows; + +import static com.google.monitoring.metrics.EventMetric.DEFAULT_FITTER; + +import com.google.common.collect.ImmutableSet; +import com.google.monitoring.metrics.EventMetric; +import com.google.monitoring.metrics.IncrementableMetric; +import com.google.monitoring.metrics.LabelDescriptor; +import com.google.monitoring.metrics.MetricRegistryImpl; +import google.registry.monitoring.whitebox.CheckApiMetric; +import google.registry.monitoring.whitebox.CheckApiMetric.Availability; +import google.registry.monitoring.whitebox.CheckApiMetric.Tier; +import javax.inject.Inject; + +/** Helpers for updating domain check metrics. */ +public class CheckApiMetrics { + + private static final ImmutableSet LABEL_DESCRIPTORS = + ImmutableSet.of( + LabelDescriptor.create("tier", "Price tier of the domain name."), + LabelDescriptor.create("availability", "Availability of the domain name."), + LabelDescriptor.create("status", "The return status of the check.")); + + private static final IncrementableMetric requests = + MetricRegistryImpl.getDefault() + .newIncrementableMetric( + "/check_api/requests", "Count of CheckApi Requests", "count", LABEL_DESCRIPTORS); + + private static final EventMetric processingTime = + MetricRegistryImpl.getDefault() + .newEventMetric( + "/check_api/processing_time", + "CheckApi Processing Time", + "milliseconds", + LABEL_DESCRIPTORS, + DEFAULT_FITTER); + + @Inject + CheckApiMetrics() {} + + public void incrementCheckApiRequest(CheckApiMetric metric) { + requests.increment( + metric.tier().map(Tier::getDisplayLabel).orElse(""), + metric.availability().map(Availability::getDisplayLabel).orElse(""), + metric.status().getDisplayLabel()); + } + + public void recordProcessingTime(CheckApiMetric metric) { + long elapsedTime = metric.endTimestamp().getMillis() - metric.startTimestamp().getMillis(); + processingTime.record( + elapsedTime, + metric.tier().map(Tier::getDisplayLabel).orElse(""), + metric.availability().map(Availability::getDisplayLabel).orElse(""), + metric.status().getDisplayLabel()); + } +} diff --git a/java/google/registry/flows/domain/DomainFlowUtils.java b/java/google/registry/flows/domain/DomainFlowUtils.java index 0bddd38a9..fd3bc3ffd 100644 --- a/java/google/registry/flows/domain/DomainFlowUtils.java +++ b/java/google/registry/flows/domain/DomainFlowUtils.java @@ -1160,7 +1160,7 @@ public class DomainFlowUtils { } /** Domain label is not allowed by IDN table. */ - static class InvalidIdnDomainLabelException extends ParameterValueRangeErrorException { + public static class InvalidIdnDomainLabelException extends ParameterValueRangeErrorException { public InvalidIdnDomainLabelException() { super("Domain label is not allowed by IDN table"); } diff --git a/java/google/registry/monitoring/whitebox/CheckApiMetric.java b/java/google/registry/monitoring/whitebox/CheckApiMetric.java new file mode 100644 index 000000000..33b869f9d --- /dev/null +++ b/java/google/registry/monitoring/whitebox/CheckApiMetric.java @@ -0,0 +1,124 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.monitoring.whitebox; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import google.registry.util.Clock; +import java.util.Optional; +import org.joda.time.DateTime; + +/** A value class for recording attributes of a domain check metric. */ +@AutoValue +public abstract class CheckApiMetric { + + /** Price tier of a domain name. */ + public enum Tier { + STANDARD("standard"), + PREMINUM("premium"); + + private final String displayLabel; + + private Tier(String displayLabel) { + this.displayLabel = displayLabel; + } + + public String getDisplayLabel() { + return displayLabel; + } + } + + /** Availability status of a domain. */ + public enum Availability { + RESERVED("reserved"), + REGISTERED("registered"), + AVAILABLE("available"); + + private final String displayLabel; + + private Availability(String displayLabel) { + this.displayLabel = displayLabel; + } + + public String getDisplayLabel() { + return displayLabel; + } + } + + /** Status of the CheckApi command. */ + public enum Status { + SUCCESS("success"), + INVALID_NAME("invalid_name"), + INVALID_REGISTRY_PHASE("invalid_registry_phase"), + UNKNOWN_ERROR("unknown_error"); + + private final String displayLabel; + + private Status(String displayLabel) { + this.displayLabel = displayLabel; + } + + public String getDisplayLabel() { + return displayLabel; + } + } + + public abstract DateTime startTimestamp(); + + public abstract DateTime endTimestamp(); + + public abstract Status status(); + + public abstract Optional tier(); + + public abstract Optional availability(); + + public static Builder builder(Clock clock) { + return new AutoValue_CheckApiMetric.Builder().startTimestamp(clock.nowUtc()).setClock(clock); + } + + CheckApiMetric() {} + + /** Builder for {@link CheckApiMetric}. */ + @AutoValue.Builder + public abstract static class Builder { + + private Clock clock; + + /** Saves the {@link Clock} for end-time determination. */ + Builder setClock(Clock clock) { + checkNotNull(clock, "clock"); + this.clock = clock; + return this; + } + + public CheckApiMetric build() { + return this.endTimestamp(clock.nowUtc()).autoBuild(); + } + + abstract Builder startTimestamp(DateTime startTimestamp); + + abstract Builder endTimestamp(DateTime endTimestamp); + + public abstract Builder status(Status status); + + public abstract Builder tier(Tier tier); + + public abstract Builder availability(Availability availability); + + abstract CheckApiMetric autoBuild(); + } +} diff --git a/java/google/registry/monitoring/whitebox/WhiteboxModule.java b/java/google/registry/monitoring/whitebox/WhiteboxModule.java index 769e3923e..2203964b4 100644 --- a/java/google/registry/monitoring/whitebox/WhiteboxModule.java +++ b/java/google/registry/monitoring/whitebox/WhiteboxModule.java @@ -71,6 +71,11 @@ public class WhiteboxModule { return EppMetric.builderForRequest(requestLogId, clock); } + @Provides + static CheckApiMetric.Builder provideCheckApiMetricBuilder(Clock clock) { + return CheckApiMetric.builder(clock); + } + @Provides @Named(QUEUE_BIGQUERY_STREAMING_METRICS) static Queue provideBigQueryStreamingMetricsQueue() { diff --git a/javatests/google/registry/flows/CheckApi2ActionTest.java b/javatests/google/registry/flows/CheckApi2ActionTest.java index ce24d9c0a..658738ba0 100644 --- a/javatests/google/registry/flows/CheckApi2ActionTest.java +++ b/javatests/google/registry/flows/CheckApi2ActionTest.java @@ -15,29 +15,53 @@ package google.registry.flows; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.AVAILABLE; +import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.REGISTERED; +import static google.registry.monitoring.whitebox.CheckApiMetric.Availability.RESERVED; +import static google.registry.monitoring.whitebox.CheckApiMetric.Tier.PREMINUM; +import static google.registry.monitoring.whitebox.CheckApiMetric.Tier.STANDARD; import static google.registry.testing.DatastoreHelper.createTld; import static google.registry.testing.DatastoreHelper.persistActiveDomain; import static google.registry.testing.DatastoreHelper.persistReservedList; import static google.registry.testing.DatastoreHelper.persistResource; +import static org.mockito.Mockito.verify; import google.registry.model.registry.Registry; import google.registry.model.registry.Registry.TldState; +import google.registry.monitoring.whitebox.CheckApiMetric; +import google.registry.monitoring.whitebox.CheckApiMetric.Availability; +import google.registry.monitoring.whitebox.CheckApiMetric.Status; +import google.registry.monitoring.whitebox.CheckApiMetric.Tier; import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; +import google.registry.testing.MockitoJUnitRule; import java.util.Map; +import org.joda.time.DateTime; import org.json.simple.JSONValue; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; /** Tests for {@link CheckApi2Action}. */ @RunWith(JUnit4.class) public class CheckApi2ActionTest { + private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z"); + @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); + @Rule public final MockitoJUnitRule mocks = MockitoJUnitRule.create(); + + @Mock private CheckApiMetrics checkApiMetrics; + @Captor private ArgumentCaptor metricCaptor; + + private DateTime endTime; @Before public void init() throws Exception { @@ -54,129 +78,187 @@ public class CheckApi2ActionTest { CheckApi2Action action = new CheckApi2Action(); action.domain = domain; action.response = new FakeResponse(); - action.clock = new FakeClock(); + FakeClock fakeClock = new FakeClock(START_TIME); + action.clock = fakeClock; + action.metricBuilder = CheckApiMetric.builder(fakeClock); + action.checkApiMetrics = checkApiMetrics; + fakeClock.advanceOneMilli(); + endTime = fakeClock.nowUtc(); + action.run(); return (Map) JSONValue.parse(((FakeResponse) action.response).getPayload()); } @Test - public void testFailure_nullDomain() throws Exception { + public void testFailure_nullDomain() { assertThat(getCheckResponse(null)) .containsExactly( "status", "error", "reason", "Must supply a valid domain name on an authoritative TLD"); + + verifyFailureMetric(Status.INVALID_NAME); } @Test - public void testFailure_emptyDomain() throws Exception { + public void testFailure_emptyDomain() { assertThat(getCheckResponse("")) .containsExactly( "status", "error", "reason", "Must supply a valid domain name on an authoritative TLD"); + + verifyFailureMetric(Status.INVALID_NAME); } @Test - public void testFailure_invalidDomain() throws Exception { + public void testFailure_invalidDomain() { assertThat(getCheckResponse("@#$%^")) .containsExactly( "status", "error", "reason", "Must supply a valid domain name on an authoritative TLD"); + + verifyFailureMetric(Status.INVALID_NAME); } @Test - public void testFailure_singlePartDomain() throws Exception { + public void testFailure_singlePartDomain() { assertThat(getCheckResponse("foo")) .containsExactly( "status", "error", "reason", "Must supply a valid domain name on an authoritative TLD"); + + verifyFailureMetric(Status.INVALID_NAME); } @Test - public void testFailure_nonExistentTld() throws Exception { + public void testFailure_nonExistentTld() { assertThat(getCheckResponse("foo.bar")) .containsExactly( "status", "error", "reason", "Must supply a valid domain name on an authoritative TLD"); + + verifyFailureMetric(Status.INVALID_NAME); } @Test - public void testFailure_invalidIdnTable() throws Exception { + public void testFailure_invalidIdnTable() { assertThat(getCheckResponse("ΑΒΓ.example")) .containsExactly( "status", "error", "reason", "Domain label is not allowed by IDN table"); + + verifyFailureMetric(Status.INVALID_NAME); } @Test - public void testFailure_tldInPredelegation() throws Exception { + public void testFailure_tldInPredelegation() { createTld("predelegated", TldState.PREDELEGATION); assertThat(getCheckResponse("foo.predelegated")) .containsExactly( "status", "error", "reason", "Check in this TLD is not allowed in the current registry phase"); + + verifyFailureMetric(Status.INVALID_REGISTRY_PHASE); } @Test - public void testSuccess_availableStandard() throws Exception { + public void testSuccess_availableStandard() { assertThat(getCheckResponse("somedomain.example")) .containsExactly( "status", "success", "available", true, "tier", "standard"); + + verifySuccessMetric(STANDARD, AVAILABLE); } @Test - public void testSuccess_availableCapital() throws Exception { + public void testSuccess_availableCapital() { assertThat(getCheckResponse("SOMEDOMAIN.EXAMPLE")) .containsExactly( "status", "success", "available", true, "tier", "standard"); + + verifySuccessMetric(STANDARD, AVAILABLE); } @Test - public void testSuccess_availableUnicode() throws Exception { + public void testSuccess_availableUnicode() { assertThat(getCheckResponse("ééé.example")) .containsExactly( "status", "success", "available", true, "tier", "standard"); + + verifySuccessMetric(STANDARD, AVAILABLE); } @Test - public void testSuccess_availablePunycode() throws Exception { + public void testSuccess_availablePunycode() { assertThat(getCheckResponse("xn--9caaa.example")) .containsExactly( "status", "success", "available", true, "tier", "standard"); + + verifySuccessMetric(STANDARD, AVAILABLE); } @Test - public void testSuccess_availablePremium() throws Exception { + public void testSuccess_availablePremium() { assertThat(getCheckResponse("rich.example")) .containsExactly( "status", "success", "available", true, "tier", "premium"); + + verifySuccessMetric(PREMINUM, AVAILABLE); } @Test - public void testSuccess_alreadyRegistered() throws Exception { + public void testSuccess_alreadyRegistered() { persistActiveDomain("somedomain.example"); assertThat(getCheckResponse("somedomain.example")) .containsExactly( "status", "success", "available", false, "reason", "In use"); + + verifySuccessMetric(STANDARD, REGISTERED); } @Test - public void testSuccess_reserved() throws Exception { + public void testSuccess_reserved() { assertThat(getCheckResponse("foo.example")) .containsExactly( "status", "success", "available", false, "reason", "Reserved"); + + verifySuccessMetric(STANDARD, RESERVED); + } + + private void verifySuccessMetric(Tier tier, Availability availability) { + verify(checkApiMetrics).incrementCheckApiRequest(metricCaptor.capture()); + CheckApiMetric metric = metricCaptor.getValue(); + + verify(checkApiMetrics).recordProcessingTime(metric); + assertThat(metric.availability()).hasValue(availability); + assertThat(metric.tier()).hasValue(tier); + assertThat(metric.status()).isEqualTo(Status.SUCCESS); + assertThat(metric.startTimestamp()).isEqualTo(START_TIME); + assertThat(metric.endTimestamp()).isEqualTo(endTime); + } + + private void verifyFailureMetric(Status status) { + verify(checkApiMetrics).incrementCheckApiRequest(metricCaptor.capture()); + CheckApiMetric metric = metricCaptor.getValue(); + + verify(checkApiMetrics).recordProcessingTime(metric); + assertThat(metric.availability()).isEmpty(); + assertThat(metric.tier()).isEmpty(); + assertThat(metric.status()).isEqualTo(status); + assertThat(metric.startTimestamp()).isEqualTo(START_TIME); + assertThat(metric.endTimestamp()).isEqualTo(endTime); } } diff --git a/javatests/google/registry/monitoring/whitebox/CheckApiMetricTest.java b/javatests/google/registry/monitoring/whitebox/CheckApiMetricTest.java new file mode 100644 index 000000000..0a1793f50 --- /dev/null +++ b/javatests/google/registry/monitoring/whitebox/CheckApiMetricTest.java @@ -0,0 +1,48 @@ +// Copyright 2018 The Nomulus Authors. All Rights Reserved. +// +// 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 google.registry.monitoring.whitebox; + +import static com.google.common.truth.Truth.assertThat; + +import google.registry.monitoring.whitebox.CheckApiMetric.Status; +import google.registry.testing.FakeClock; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Unit tests for {@link CheckApiMetric}. */ +@RunWith(JUnit4.class) +public class CheckApiMetricTest { + private static final DateTime START_TIME = DateTime.parse("2000-01-01T00:00:00.0Z"); + + private final FakeClock clock = new FakeClock(START_TIME); + private CheckApiMetric.Builder metricBuilder; + + @Before + public void setup() { + metricBuilder = CheckApiMetric.builder(clock); + } + + @Test + public void testSuccess_timestampsAreSet() { + clock.advanceOneMilli(); + CheckApiMetric metric = metricBuilder.status(Status.SUCCESS).build(); + + assertThat(metric.startTimestamp()).isEqualTo(START_TIME); + assertThat(metric.endTimestamp()).isEqualTo(clock.nowUtc()); + } +}