You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
Generate gRPC Client target parsing func (#7424)
In order to add [gRPC server attributes for exporter observability](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/otel/sdk-metrics.md#metric-otelsdkexporterspaninflight), this information needs to be parsed into a host and port. The added generated files provides `ParseCanonicalTarget` for this functionality. This is added as a generated template as it is expected to be needed for all OTLP exporters (e.g. #7404, #7353). Split from work added to #7404 cc @yumosx ### Benchamarks ``` goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/observ cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz │ otlptracegrpc-observ-target.bmark.result │ │ sec/op │ ParseTarget/HostName-8 80.90n ± 1% ParseTarget/HostPort-8 123.2n ± 4% ParseTarget/IPv4WithoutPort-8 94.25n ± 2% ParseTarget/IPv4WithPort-8 136.2n ± 1% ParseTarget/IPv6Bare-8 195.5n ± 2% ParseTarget/IPv6Bracket-8 191.2n ± 3% ParseTarget/IPv6WithPort-8 128.6n ± 4% ParseTarget/UnixSocket-8 15.73n ± 4% ParseTarget/UnixAbstractSocket-8 15.71n ± 6% ParseTarget/Passthrough-8 129.3n ± 18% geomean 84.98n │ otlptracegrpc-observ-target.bmark.result │ │ B/op │ ParseTarget/HostName-8 48.00 ± 0% ParseTarget/HostPort-8 48.00 ± 0% ParseTarget/IPv4WithoutPort-8 16.00 ± 0% ParseTarget/IPv4WithPort-8 48.00 ± 0% ParseTarget/IPv6Bare-8 16.00 ± 0% ParseTarget/IPv6Bracket-8 16.00 ± 0% ParseTarget/IPv6WithPort-8 48.00 ± 0% ParseTarget/UnixSocket-8 0.000 ± 0% ParseTarget/UnixAbstractSocket-8 0.000 ± 0% ParseTarget/Passthrough-8 48.00 ± 0% geomean ¹ ¹ summaries must be >0 to compute geomean │ otlptracegrpc-observ-target.bmark.result │ │ allocs/op │ ParseTarget/HostName-8 1.000 ± 0% ParseTarget/HostPort-8 1.000 ± 0% ParseTarget/IPv4WithoutPort-8 1.000 ± 0% ParseTarget/IPv4WithPort-8 1.000 ± 0% ParseTarget/IPv6Bare-8 1.000 ± 0% ParseTarget/IPv6Bracket-8 1.000 ± 0% ParseTarget/IPv6WithPort-8 1.000 ± 0% ParseTarget/UnixSocket-8 0.000 ± 0% ParseTarget/UnixAbstractSocket-8 0.000 ± 0% ParseTarget/Passthrough-8 1.000 ± 0% geomean ¹ ¹ summaries must be >0 to compute geomean ```
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/otlp/observ/target.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package {{ .pkg }} // import "{{ .pkg_path }}"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
schemeUnix = "unix"
|
||||
schemeUnixAbstract = "unix-abstract"
|
||||
)
|
||||
|
||||
// ParseCanonicalTarget parses a target string and returns the extracted host
|
||||
// (domain address or IP), the target port, or an error.
|
||||
//
|
||||
// If no port is specified, -1 is returned.
|
||||
//
|
||||
// If no host is specified, an empty string is returned.
|
||||
//
|
||||
// The target string is expected to always have the form
|
||||
// "<scheme>://[authority]/<endpoint>". For example:
|
||||
// - "dns:///example.com:42"
|
||||
// - "dns://8.8.8.8/example.com:42"
|
||||
// - "unix:///path/to/socket"
|
||||
// - "unix-abstract:///socket-name"
|
||||
// - "passthrough:///192.34.2.1:42"
|
||||
//
|
||||
// The target is expected to come from the CanonicalTarget method of a gRPC
|
||||
// Client.
|
||||
func ParseCanonicalTarget(target string) (string, int, error) {
|
||||
const sep = "://"
|
||||
|
||||
// Find scheme. Do not allocate the string by using url.Parse.
|
||||
idx := strings.Index(target, sep)
|
||||
if idx == -1 {
|
||||
return "", -1, fmt.Errorf("invalid target %q: missing scheme", target)
|
||||
}
|
||||
scheme, endpoint := target[:idx], target[idx+len(sep):]
|
||||
|
||||
// Check for unix schemes.
|
||||
if scheme == schemeUnix || scheme == schemeUnixAbstract {
|
||||
return parseUnix(endpoint)
|
||||
}
|
||||
|
||||
// Strip leading slash and any authority.
|
||||
if i := strings.Index(endpoint, "/"); i != -1 {
|
||||
endpoint = endpoint[i+1:]
|
||||
}
|
||||
|
||||
// DNS, passthrough, and custom resolvers.
|
||||
return parseEndpoint(endpoint)
|
||||
}
|
||||
|
||||
// parseUnix parses unix socket targets.
|
||||
func parseUnix(endpoint string) (string, int, error) {
|
||||
// Format: unix[-abstract]://path
|
||||
//
|
||||
// We should have "/path" (empty authority) if valid.
|
||||
if len(endpoint) >= 1 && endpoint[0] == '/' {
|
||||
// Return the full path including leading slash.
|
||||
return endpoint, -1, nil
|
||||
}
|
||||
|
||||
// If there's no leading slash, it means there might be an authority
|
||||
// Check for authority case (should error): "authority/path"
|
||||
if slashIdx := strings.Index(endpoint, "/"); slashIdx > 0 {
|
||||
return "", -1, fmt.Errorf("invalid (non-empty) authority: %s", endpoint[:slashIdx])
|
||||
}
|
||||
|
||||
return "", -1, errors.New("invalid unix target format")
|
||||
}
|
||||
|
||||
// parseEndpoint parses an endpoint from a gRPC target.
|
||||
//
|
||||
// It supports the following formats:
|
||||
// - "host"
|
||||
// - "host%zone"
|
||||
// - "host:port"
|
||||
// - "host%zone:port"
|
||||
// - "ipv4"
|
||||
// - "ipv4%zone"
|
||||
// - "ipv4:port"
|
||||
// - "ipv4%zone:port"
|
||||
// - "ipv6"
|
||||
// - "ipv6%zone"
|
||||
// - "[ipv6]"
|
||||
// - "[ipv6%zone]"
|
||||
// - "[ipv6]:port"
|
||||
// - "[ipv6%zone]:port"
|
||||
//
|
||||
// It returns the host or host%zone (domain address or IP), the port (or -1 if
|
||||
// not specified), or an error if the input is not a valid.
|
||||
func parseEndpoint(endpoint string) (string, int, error) {
|
||||
// First check if the endpoint is just an IP address.
|
||||
if ip := parseIP(endpoint); ip != "" {
|
||||
return ip, -1, nil
|
||||
}
|
||||
|
||||
// If there's no colon, there is no port (IPv6 with no port checked above).
|
||||
if !strings.Contains(endpoint, ":") {
|
||||
return endpoint, -1, nil
|
||||
}
|
||||
|
||||
host, portStr, err := net.SplitHostPort(endpoint)
|
||||
if err != nil {
|
||||
return "", -1, fmt.Errorf("invalid host:port %q: %w", endpoint, err)
|
||||
}
|
||||
|
||||
const base, bitSize = 10, 16
|
||||
port16, err := strconv.ParseUint(portStr, base, bitSize)
|
||||
if err != nil {
|
||||
return "", -1, fmt.Errorf("invalid port %q: %w", portStr, err)
|
||||
}
|
||||
port := int(port16) // port is guaranteed to be in the range [0, 65535].
|
||||
|
||||
return host, port, nil
|
||||
}
|
||||
|
||||
// parseIP attempts to parse the entire endpoint as an IP address.
|
||||
// It returns the normalized string form of the IP if successful,
|
||||
// or an empty string if parsing fails.
|
||||
func parseIP(ip string) string {
|
||||
// Strip leading and trailing brackets for IPv6 addresses.
|
||||
if len(ip) >= 2 && ip[0] == '[' && ip[len(ip)-1] == ']' {
|
||||
ip = ip[1 : len(ip)-1]
|
||||
}
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
// Return the normalized string form of the IP.
|
||||
return addr.String()
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/otlp/observ/target_test.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package {{ .pkg }}
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseTarget(t *testing.T) {
|
||||
// gRPC target naming is defined here:
|
||||
// https://github.com/grpc/grpc/blob/74232c6bd3c0f4bc35bad035dbeecf5cbc834a11/doc/naming.md
|
||||
//
|
||||
// The Go gRPC client only supports the "dns", "unix", "unix-abstract", and
|
||||
// "passthrough" schemes natively with "dns" being the default:
|
||||
// https://pkg.go.dev/google.golang.org/grpc@v1.75.1/internal/resolver
|
||||
//
|
||||
// Other schemes (e.g., "consul", "zk") are supported via custom resolvers
|
||||
// that can be registered with the gRPC resolver package. These custom
|
||||
// resolvers are still expected to follow the general target string format
|
||||
// when rendered with the CanonicalTarget method:
|
||||
//
|
||||
// <scheme>://<authority>/<endpoint>
|
||||
//
|
||||
// All target strings in these tests are rendered with the
|
||||
// CanonicalTarget method. Therefore they all follow the above format.
|
||||
tests := []struct {
|
||||
target string
|
||||
host string
|
||||
port int
|
||||
}{
|
||||
// DNS scheme: hostname and port.
|
||||
{target: "dns:///:8080", host: "", port: 8080},
|
||||
{target: "dns:///example.com", host: "example.com", port: -1},
|
||||
{target: "dns:///example.com%eth0", host: "example.com%eth0", port: -1},
|
||||
{target: "dns:///example.com:42", host: "example.com", port: 42},
|
||||
{target: "dns:///example.com%eth0:42", host: "example.com%eth0", port: 42},
|
||||
|
||||
// DNS scheme: hostname and port with authority.
|
||||
{target: "dns://8.8.8.8/example.com", host: "example.com", port: -1},
|
||||
{target: "dns://8.8.8.8/example.com%eth0", host: "example.com%eth0", port: -1},
|
||||
{target: "dns://8.8.8.8/example.com:42", host: "example.com", port: 42},
|
||||
{target: "dns://8.8.8.8/example.com%eth0:42", host: "example.com%eth0", port: 42},
|
||||
|
||||
// DNS scheme: IPv4 address and port.
|
||||
{target: "dns:///192.168.1.1", host: "192.168.1.1", port: -1},
|
||||
{target: "dns:///192.168.1.1%eth0", host: "192.168.1.1%eth0", port: -1},
|
||||
{target: "dns:///192.168.1.1:8080", host: "192.168.1.1", port: 8080},
|
||||
{target: "dns:///192.168.1.1%eth0:8080", host: "192.168.1.1%eth0", port: 8080},
|
||||
|
||||
// DNS scheme: IPv6 address and port.
|
||||
{target: "dns:///2001:0db8:85a3:0000:0000:8a2e:0370:7334", host: "2001:db8:85a3::8a2e:370:7334", port: -1},
|
||||
{target: "dns:///2001:db8:85a3:0:0:8a2e:370:7334", host: "2001:db8:85a3::8a2e:370:7334", port: -1},
|
||||
{target: "dns:///2001:db8:85a3::8a2e:370:7334", host: "2001:db8:85a3::8a2e:370:7334", port: -1},
|
||||
{target: "dns:///2001:db8:85a3::8a2e:370:7334%eth0", host: "2001:db8:85a3::8a2e:370:7334%eth0", port: -1},
|
||||
{target: "dns:///[2001:db8:85a3::8a2e:370:7334]", host: "2001:db8:85a3::8a2e:370:7334", port: -1},
|
||||
{target: "dns:///[2001:db8:85a3::8a2e:370:7334%eth0]", host: "2001:db8:85a3::8a2e:370:7334%eth0", port: -1},
|
||||
{target: "dns:///[::1]:9090", host: "::1", port: 9090},
|
||||
{target: "dns:///[::1%eth0]:9090", host: "::1%eth0", port: 9090},
|
||||
|
||||
// Unix domain sockets.
|
||||
{target: "unix:///tmp/grpc.sock", host: "/tmp/grpc.sock", port: -1},
|
||||
{target: "unix:///absolute_path", host: "/absolute_path", port: -1},
|
||||
|
||||
// Unix domain socket in abstract namespace.
|
||||
{target: "unix-abstract:///abstract-socket-name", host: "/abstract-socket-name", port: -1},
|
||||
|
||||
// International domain names.
|
||||
{target: "dns:///测试.example.com:8080", host: "测试.example.com", port: 8080},
|
||||
|
||||
// Port edge cases.
|
||||
{target: "dns:///example.com:0", host: "example.com", port: 0},
|
||||
{target: "dns:///example.com:65535", host: "example.com", port: 65535},
|
||||
|
||||
// Case sensitivity.
|
||||
{target: "dns:///EXAMPLE.COM:8080", host: "EXAMPLE.COM", port: 8080},
|
||||
{target: "dns:///Example.Com:8080", host: "Example.Com", port: 8080},
|
||||
|
||||
// Custom and passthrough resolvers scheme
|
||||
{target: "passthrough:///localhost:50051", host: "localhost", port: 50051},
|
||||
{target: "passthrough:///10.0.0.2:7777", host: "10.0.0.2", port: 7777},
|
||||
{target: "consul:///my-service", host: "my-service", port: -1},
|
||||
{target: "zk:///services/my-service", host: "services/my-service", port: -1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
host, port, err := ParseCanonicalTarget(tt.target)
|
||||
if err != nil {
|
||||
t.Errorf("parseTarget(%q) unexpected error: %v", tt.target, err)
|
||||
continue
|
||||
}
|
||||
if host != tt.host {
|
||||
t.Errorf("parseTarget(%q) host = %q, want %q", tt.target, host, tt.host)
|
||||
}
|
||||
if port != tt.port {
|
||||
t.Errorf("parseTarget(%q) port = %d, want %d", tt.target, port, tt.port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTargetErrors(t *testing.T) {
|
||||
targets := []string{
|
||||
"dns:///example.com:invalid", // Non-numeric port in URL.
|
||||
"dns:///example.com:8080:9090", // Multiple colons in port.
|
||||
"dns:///example.com:99999", // Port out of range.
|
||||
"dns:///example.com:-1", // Port out of range.
|
||||
"unix://localhost/sock", // Non-empty authority for unix scheme.
|
||||
"unix:", // Empty unix scheme.
|
||||
"unix-abstract://", // Empty unix-abstract scheme.
|
||||
"unix-abstract://authority/sock", // Non-empty authority for unix-abstract scheme.
|
||||
"contains-cont\roll-cha\rs", // Invalid URL.
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
host, port, err := ParseCanonicalTarget(target)
|
||||
if err == nil {
|
||||
t.Errorf("parseTarget(%q) expected error, got nil", target)
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
t.Errorf("parseTarget(%q) host = %q, want empty", target, host)
|
||||
}
|
||||
|
||||
if port != -1 {
|
||||
t.Errorf("parseTarget(%q) port = %d, want -1", target, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseTarget(b *testing.B) {
|
||||
benchmarks := []struct {
|
||||
name string
|
||||
target string
|
||||
}{
|
||||
{"HostName", "dns:///example.com"},
|
||||
{"HostPort", "dns:///example.com:8080"},
|
||||
{"IPv4WithoutPort", "dns:///192.168.1.1"},
|
||||
{"IPv4WithPort", "dns:///192.168.1.1:8080"},
|
||||
{"IPv6Bare", "dns:///2001:db8::1"},
|
||||
{"IPv6Bracket", "dns:///[2001:db8::1]"},
|
||||
{"IPv6WithPort", "dns:///[2001:db8::1]:8080"},
|
||||
{"UnixSocket", "unix:///tmp/grpc.sock"},
|
||||
{"UnixAbstractSocket", "unix-abstract:///abstract-socket-name"},
|
||||
{"Passthrough", "passthrough:///localhost:50051"},
|
||||
}
|
||||
|
||||
var (
|
||||
host string
|
||||
port int
|
||||
err error
|
||||
)
|
||||
for _, bm := range benchmarks {
|
||||
b.Run(bm.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
host, port, err = ParseCanonicalTarget(bm.target)
|
||||
}
|
||||
})
|
||||
}
|
||||
_, _, _ = host, port, err
|
||||
}
|
||||
Reference in New Issue
Block a user