diff --git a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcLogEntry.java b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcLogEntry.java index bf7009e4d..9c8f0989c 100644 --- a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcLogEntry.java +++ b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcLogEntry.java @@ -52,6 +52,7 @@ public final class IpcLogEntry { private String owner; private IpcResult result; + private IpcSource source; private String protocol; @@ -64,6 +65,7 @@ public final class IpcLogEntry { private String vip; private String endpoint; + private String method; private String clientRegion; private String clientZone; @@ -208,6 +210,14 @@ public IpcLogEntry withResult(IpcResult result) { return this; } + /** + * Set the source for this request. See {@link IpcSource} for more information. + */ + public IpcLogEntry withSource(IpcSource source) { + this.source = source; + return this; + } + /** * Set the high level status for the request. See {@link IpcStatus} for more * information. @@ -307,6 +317,22 @@ public IpcLogEntry withEndpoint(String endpoint) { return this; } + /** + * Set the method used for this request. + * See {@link IpcMethod} for possible values. + */ + public IpcLogEntry withMethod(IpcMethod method) { + return withMethod(method.value()); + } + + /** + * Set the method used for this request. + */ + public IpcLogEntry withMethod(String method) { + this.method = method; + return this; + } + /** * Set the client region for the request. In the case of the server side this will be * automatically filled in if the {@link NetflixHeader#Zone} is specified on the client @@ -899,6 +925,7 @@ public String toString() { .addField("protocol", protocol) .addField("uri", uri) .addField("path", path) + .addField("method", method) .addField("endpoint", endpoint) .addField("vip", vip) .addField("clientRegion", clientRegion) @@ -918,6 +945,7 @@ public String toString() { .addField("attempt", attempt) .addField("attemptFinal", attemptFinal) .addField("result", result) + .addField("source", source) .addField("status", status) .addField("statusDetail", statusDetail) .addField("exceptionClass", getExceptionClass()) diff --git a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMethod.java b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMethod.java new file mode 100644 index 000000000..a43613cbc --- /dev/null +++ b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMethod.java @@ -0,0 +1,94 @@ +/** + * Copyright 2024 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.ipc; + +import com.netflix.spectator.api.Tag; + +public enum IpcMethod implements Tag { + + /** + * Represents a unary gRPC method. + */ + unary, + + /** + * Represents a client streaming gRPC method. + */ + client_streaming, + + /** + * Represents a server streaming gRPC method. + */ + server_streaming, + + /** + * Represents a bidirectional streaming gRPC method. + */ + bidi_streaming, + + /** + * Represents an HTTP GET request. + */ + get, + + /** + * Represents an HTTP POST request. + */ + post, + + /** + * Represents an HTTP PUT request. + */ + put, + + /** + * Represents an HTTP PATCH request. + */ + patch, + + /** + * Represents an HTTP DELETE request. + */ + delete, + + /** + * Represents an HTTP OPTIONS request. + */ + options, + + /** + * Represents a GraphQL query. + */ + query, + + /** + * Represents a GraphQL mutation. + */ + mutation, + + /** + * Represents a GraphQL subscription. + */ + subscription; + + @Override public String key() { + return IpcTagKey.method.key(); + } + + @Override public String value() { + return name(); + } +} diff --git a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMetric.java b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMetric.java index a3c1ee98f..072f588e8 100644 --- a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMetric.java +++ b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcMetric.java @@ -228,6 +228,43 @@ public enum IpcMetric { IpcTagKey.protocol, IpcTagKey.vip ) + ), + + /** + * V2 - Timer recording the number and latency of outbound requests. + */ + apiClientCall( + "api.client.call", + EnumSet.of( + IpcTagKey.vip, + IpcTagKey.method, + IpcTagKey.endpoint, + IpcTagKey.owner, + IpcTagKey.id, + IpcTagKey.source, + IpcTagKey.result, + IpcTagKey.status, + IpcTagKey.statusDetail + ), + EnumSet.noneOf(IpcTagKey.class) + ), + + /** + * V2 - Timer recording the number and latency of inbound requests. + */ + apiServerCall( + "api.server.call", + EnumSet.of( + IpcTagKey.method, + IpcTagKey.endpoint, + IpcTagKey.owner, + IpcTagKey.id, + IpcTagKey.source, + IpcTagKey.result, + IpcTagKey.status, + IpcTagKey.statusDetail + ), + EnumSet.noneOf(IpcTagKey.class) ); private final String metricName; diff --git a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcSource.java b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcSource.java new file mode 100644 index 000000000..baaf25c24 --- /dev/null +++ b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcSource.java @@ -0,0 +1,81 @@ +/** + * Copyright 2024 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.ipc; + +import com.netflix.spectator.api.Tag; + +public enum IpcSource implements Tag { + + /** + * No call was made due to errors potentially. + */ + none, + + /** + * Data sourced directly from EVCache as the cache implementation (when the exact cache is known). + */ + evcache, + + /** + * Data sourced from a cache where the cache implementation is not directly known or abstracted. + */ + cache, + + /** + * Static fallback was used to fetch the data. + */ + fallback, + + /** + * Response fetched using mesh. + */ + mesh, + + /** + * Response fetched directly from the downstream service (or if not known to be mesh). + */ + direct, + + /** + * Data sourced from a validation handler that may short-circuit the response immediately for failed validation. + */ + validation, + + /** + * Data sourced and returned directly by a filter or interceptor. + */ + filter, + + /** + * Data fetched from an in-memory cache. + */ + memory, + + /** + * Data sourced from a user defined business logic handler or root data fetcher. + */ + application; + + @Override + public String key() { + return IpcTagKey.source.key(); + } + + @Override + public String value() { + return name(); + } +} diff --git a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcTagKey.java b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcTagKey.java index 08d2280fa..c3526c249 100644 --- a/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcTagKey.java +++ b/spectator-ext-ipc/src/main/java/com/netflix/spectator/ipc/IpcTagKey.java @@ -32,6 +32,12 @@ public enum IpcTagKey { */ result("ipc.result"), + /** + * Indicates where the result was ultimately sourced from such as cache, direct, + * proxy, fallback, etc. See {@link IpcSource} for possible values. + */ + source("ipc.source"), + /** * Dimension indicating a high level status for the request. These values are the same * for all implementations to make it easier to query across services. See {@link IpcStatus} @@ -41,8 +47,9 @@ public enum IpcTagKey { /** * Optional dimension indicating a more detailed status. The values for this are - * implementation specific. For example with HTTP, the status code would be a likely - * value used here. + * implementation specific. For example, the {@link #status} may be + * {@code connection_error} and {@code statusDetail} would be {@code no_servers}, + * {@code connect_timeout}, {@code ssl_handshake_failure}, etc. */ statusDetail("ipc.status.detail"), @@ -103,6 +110,11 @@ public enum IpcTagKey { */ protocol("ipc.protocol"), + /** + * Method used to make the IPC request. See {@link IpcMethod} for possible values. + */ + method("ipc.method"), + /** * Region where the client is located. * diff --git a/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcLogEntryTest.java b/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcLogEntryTest.java index 8e7336e4e..21b006953 100644 --- a/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcLogEntryTest.java +++ b/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcLogEntryTest.java @@ -242,6 +242,26 @@ public void endpointViaUri404() { Assertions.assertEquals("unknown", actual); } + @Test + public void method() { + String expected = IpcMethod.get.value(); + String actual = (String) entry + .withMethod(IpcMethod.get) + .convert(this::toMap) + .get("method"); + Assertions.assertEquals(expected, actual); + } + + @Test + public void customMethod() { + String expected = "websocket"; + String actual = (String) entry + .withMethod(expected) + .convert(this::toMap) + .get("method"); + Assertions.assertEquals(expected, actual); + } + @Test public void clientNode() { String expected = "i-12345"; @@ -458,6 +478,15 @@ public void addResponseEndpointHeader() { Assertions.assertEquals("/api/v1/test", map.get("endpoint")); } + @Test + public void source() { + String expected = IpcSource.direct.value(); + String actual = (String) entry + .withSource(IpcSource.direct) + .convert(this::toMap).get("source"); + Assertions.assertEquals(expected, actual); + } + @Test public void httpStatusOk() { String actual = (String) entry diff --git a/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcMetricTest.java b/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcMetricTest.java index 46683b4a9..278acca10 100644 --- a/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcMetricTest.java +++ b/spectator-ext-ipc/src/test/java/com/netflix/spectator/ipc/IpcMetricTest.java @@ -252,4 +252,33 @@ public void validateFailureInjectionInvalid() { IpcMetric.clientCall.validate(id); }); } + + @Test + public void validateApiIdOk() { + Id id = registry.createId(IpcMetric.apiClientCall.metricName()) + .withTag(IpcTagKey.owner.tag("test")) + .withTag(IpcTagKey.vip.tag("localhost")) + .withTag(IpcMethod.get) + .withTag(IpcTagKey.endpoint.tag("hello")) + .withTag(IpcTagKey.id.tag("HelloEndpoint")) + .withTag(IpcSource.direct) + .withTag(IpcResult.success) + .withTag(IpcStatus.success) + .withTag(IpcTagKey.statusDetail.tag("200")); + IpcMetric.apiClientCall.validate(id, true); + } + + @Test + public void validateApiServerIdOk() { + Id id = registry.createId(IpcMetric.apiServerCall.metricName()) + .withTag(IpcTagKey.owner.tag("test")) + .withTag(IpcMethod.get) + .withTag(IpcTagKey.endpoint.tag("hello")) + .withTag(IpcTagKey.id.tag("HelloEndpoint")) + .withTag(IpcSource.direct) + .withTag(IpcResult.success) + .withTag(IpcStatus.success) + .withTag(IpcTagKey.statusDetail.tag("200")); + IpcMetric.apiServerCall.validate(id, true); + } }