/** * Copyright 2015-2017 The OpenZipkin Authors * * 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 zipkin; import java.net.InetAddress; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Locale; import zipkin.internal.InetAddresses; import zipkin.internal.JsonCodec; import zipkin.internal.Nullable; import zipkin.internal.Util; import static zipkin.internal.Util.UTF_8; import static zipkin.internal.Util.checkArgument; import static zipkin.internal.Util.checkNotNull; /** * Indicates the network context of a service recording an annotation with two exceptions. * * <p>When a BinaryAnnotation, and key is {@link Constants#CLIENT_ADDR} or {@link * Constants#SERVER_ADDR}, the endpoint indicates the source or destination of an RPC. This * exception allows zipkin to display network context of uninstrumented services, or clients such as * web browsers. */ public final class Endpoint { /** * @deprecated as leads to null pointer exceptions on port. Use {@link #builder()} instead. */ @Deprecated public static Endpoint create(String serviceName, int ipv4, int port) { return new Endpoint(serviceName, ipv4, null, port == 0 ? null : (short) (port & 0xffff)); } public static Endpoint create(String serviceName, int ipv4) { return new Endpoint(serviceName, ipv4, null, null); } /** * Classifier of a source or destination in lowercase, such as "zipkin-server". * * <p>This is the primary parameter for trace lookup, so should be intuitive as possible, for * example, matching names in service discovery. * * <p>Conventionally, when the service name isn't known, service_name = "unknown". However, it is * also permissible to set service_name = "" (empty string). The difference in the latter usage is * that the span will not be queryable by service name unless more information is added to the * span with non-empty service name, e.g. an additional annotation from the server. * * <p>Particularly clients may not have a reliable service name at ingest. One approach is to set * service_name to "" at ingest, and later assign a better label based on binary annotations, such * as user agent. */ public final String serviceName; /** * IPv4 endpoint address packed into 4 bytes or zero if unknown. * * <p>Ex for the IP 1.2.3.4, it would be {@code (1 << 24) | (2 << 16) | (3 << 8) | 4} * * @see java.net.Inet4Address#getAddress() */ public final int ipv4; /** * IPv6 endpoint address packed into 16 bytes or null if unknown. * * @see java.net.Inet6Address#getAddress() * @since Zipkin 1.4 */ @Nullable public final byte[] ipv6; /** * Port of the IP's socket or null, if not known. * * <p>Note: this is to be treated as an unsigned integer, so watch for negatives. * <p>Ex. * <pre>{@code * Integer unsignedPort = endpoint.port == null ? null : endpoint.port & 0xffff; * }</pre> * * @see java.net.InetSocketAddress#getPort() */ @Nullable public final Short port; Endpoint(String serviceName, int ipv4, byte[] ipv6, Short port) { this.serviceName = checkNotNull(serviceName, "serviceName").isEmpty() ? "" : serviceName.toLowerCase(Locale.ROOT); this.ipv4 = ipv4; this.ipv6 = ipv6; this.port = port; } public Builder toBuilder() { return new Builder(this); } public static Builder builder() { return new Builder(); } public static final class Builder { private String serviceName; private Integer ipv4; private byte[] ipv6; private Short port; Builder() { } Builder(Endpoint source) { this.serviceName = source.serviceName; this.ipv4 = source.ipv4; this.ipv6 = source.ipv6; this.port = source.port; } /** @see Endpoint#serviceName */ public Builder serviceName(String serviceName) { this.serviceName = serviceName; return this; } /** * Returns true if {@link #ipv4(int)} or {@link #ipv6(byte[])} could be parsed from the input. * * <p>Returns boolean not this for conditional parsing. For example: * <pre>{@code * if (!builder.parseIp(input.getHeader("X-Forwarded-For"))) { * builder.parseIp(input.getRemoteAddr()); * } * }</pre> * * @see #parseIp(String) * @since 1.24 */ public boolean parseIp(@Nullable InetAddress addr) { if (addr == null) return false; byte[] addressBytes = addr.getAddress(); if (addressBytes.length == 4) { ipv4(ByteBuffer.wrap(addressBytes).getInt()); } else if (addressBytes.length == 16) { ipv6(addressBytes); } else { return false; } return true; } /** * Returns true if {@link #ipv4(int)} or {@link #ipv6(byte[])} could be parsed from the input. * * <p>Returns boolean not this for conditional parsing. For example: * <pre>{@code * if (!builder.parseIp(input.getHeader("X-Forwarded-For"))) { * builder.parseIp(input.getRemoteAddr()); * } * }</pre> * * @see #parseIp(InetAddress) * @since 1.24 */ public boolean parseIp(@Nullable String ipString) { if (ipString == null) return false; byte[] addressBytes = InetAddresses.ipStringToBytes(ipString); if (addressBytes == null) return false; if (addressBytes.length == 4) { ipv4(ByteBuffer.wrap(addressBytes).getInt()); } else if (addressBytes.length == 16) { ipv6(addressBytes); } else { return false; } return true; } /** @see Endpoint#ipv4 */ public Builder ipv4(int ipv4) { this.ipv4 = ipv4; return this; } /** * When not null, this sets the {@link Endpoint#ipv6}, unless the input is an <a * href="https://tools.ietf.org/html/rfc4291#section-2.5.5.2">IPv4-Compatible or IPv4-Mapped * Embedded IPv6 Address</a>. In such case, {@link #ipv4(int)} is called with the embedded * address. * * @see Endpoint#ipv6 */ public Builder ipv6(@Nullable byte[] ipv6) { if (ipv6 == null) return this; checkArgument(ipv6.length == 16, "ipv6 addresses are 16 bytes: " + ipv6.length); for (int i = 0; i < 10; i++) { // Embedded IPv4 addresses start with unset 80 bits if (ipv6[i] != 0) { this.ipv6 = ipv6; return this; } } ByteBuffer buf = ByteBuffer.wrap(ipv6, 10, 6); short flag = buf.getShort(); if (flag == 0 || flag == -1) { // IPv4-Compatible or IPv4-Mapped int ipv4 = buf.getInt(); if (flag == 0 && ipv4 == 1) { this.ipv6 = ipv6; // ::1 is localhost, not an embedded compat address } else { this.ipv4 = ipv4; } } else { this.ipv6 = ipv6; } return this; } /** * Use this to set the port to an externally defined value. * * <p>Don't pass {@link Endpoint#port} to this method, as it may result in a * NullPointerException. Instead, use {@link Endpoint#toBuilder()} or {@link #port(Short)}. * * @param port port associated with the endpoint. zero coerces to null (unknown) * @see Endpoint#port */ public Builder port(int port) { checkArgument(port <= 0xffff, "invalid port %s", port); this.port = port <= 0 ? null : (short) (port & 0xffff); return this; } /** @see Endpoint#port */ public Builder port(@Nullable Short port) { if (port == null || port != 0) { this.port = port; } return this; } public Endpoint build() { return new Endpoint(serviceName, ipv4 == null ? 0 : ipv4, ipv6, port); } } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof Endpoint) { Endpoint that = (Endpoint) o; return (this.serviceName.equals(that.serviceName)) && (this.ipv4 == that.ipv4) && (Arrays.equals(this.ipv6, that.ipv6)) && Util.equal(this.port, that.port); } return false; } @Override public int hashCode() { int h = 1; h *= 1000003; h ^= serviceName.hashCode(); h *= 1000003; h ^= ipv4; h *= 1000003; h ^= Arrays.hashCode(ipv6); h *= 1000003; h ^= (port == null) ? 0 : port.hashCode(); return h; } @Override public String toString() { return new String(JsonCodec.writeEndpoint(this), UTF_8); } }