feat: domain telemetry improvements

GitOrigin-RevId: 9a0825160976ff16b7a39024e650ecfaf9ce82a5
This commit is contained in:
shaunn 2025-09-24 07:23:30 -07:00 committed by ory-bot
parent a4e7ff83cf
commit 93345d7b9f
4 changed files with 184 additions and 28 deletions

View File

@ -4,6 +4,5 @@
"github.com/ory/x","Apache-2.0"
"github.com/stretchr/testify","MIT"
"go.opentelemetry.io/otel/sdk","Apache-2.0"
"go.opentelemetry.io/otel/sdk","BSD-3-Clause"
"golang.org/x/text","BSD-3-Clause"

1 module name licenses
4 github.com/stretchr/testify MIT
5 go.opentelemetry.io/otel/sdk Apache-2.0
6 go.opentelemetry.io/otel/sdk golang.org/x/text BSD-3-Clause
golang.org/x/text BSD-3-Clause
7
8

View File

@ -42,6 +42,7 @@ import (
"github.com/ory/x/otelx/semconv"
prometheus "github.com/ory/x/prometheusx"
"github.com/ory/x/reqlog"
"github.com/ory/x/urlx"
)
func init() {
@ -213,11 +214,21 @@ func serveAdmin(ctx context.Context, r *driver.RegistryDefault, cmd *cobra.Comma
}
func sqa(ctx context.Context, cmd *cobra.Command, d driver.Registry) *metricsx.Service {
// Safely retrieve public base url from config
var baseURL string
if u := d.Config().ServePublic(ctx).BaseURL; u != nil {
baseURL = u.Host
urls := []string{
d.Config().ServePublic(ctx).BaseURL.Host,
d.Config().ServeAdmin(ctx).BaseURL.Host,
d.Config().SelfServiceFlowLoginUI(ctx).Host,
d.Config().SelfServiceFlowSettingsUI(ctx).Host,
d.Config().SelfServiceFlowErrorURL(ctx).Host,
d.Config().SelfServiceFlowRegistrationUI(ctx).Host,
d.Config().SelfServiceFlowRecoveryUI(ctx).Host,
d.Config().ServePublic(ctx).Host,
d.Config().ServeAdmin(ctx).Host,
}
if c, y := d.Config().CORSPublic(ctx); y {
urls = append(urls, c.AllowedOrigins...)
}
host := urlx.ExtractPublicAddress(urls...)
// Creates only ones
// instance
@ -288,7 +299,7 @@ func sqa(ctx context.Context, cmd *cobra.Command, d driver.Registry) *metricsx.S
BatchSize: 1000,
Interval: time.Hour * 6,
},
Hostname: baseURL,
Hostname: host,
},
)
}

View File

@ -4,7 +4,6 @@
package metricsx
import (
"cmp"
"context"
"crypto/sha256"
"encoding/hex"
@ -18,27 +17,32 @@ import (
"sync"
"time"
"github.com/ory/x/httpx"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"github.com/ory/x/configx"
"github.com/gofrs/uuid"
"github.com/spf13/cobra"
"github.com/gofrs/uuid"
"github.com/ory/x/cmdx"
"github.com/ory/x/configx"
"github.com/ory/x/httpx"
"github.com/ory/x/logrusx"
"github.com/ory/x/resilience"
"github.com/ory/x/urlx"
"github.com/ory/analytics-go/v5"
)
const (
XForwardedHostHeader = "X-Forwarded-Host"
AuthorityHeader = ":authority"
)
var (
instance *Service
lock sync.Mutex
instance *Service
lock sync.Mutex
knownHeaders = []string{AuthorityHeader, XForwardedHostHeader}
)
// Service helps with providing context on metrics.
@ -282,16 +286,15 @@ func (sw *Service) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.
latency := time.Since(start).Milliseconds()
path := sw.anonymizePath(r.URL.Path)
host := sw.determineURLHost(r.Header.Get("X-Forwarded-Host"), r.Host)
host := urlx.ExtractPublicAddress(sw.o.Hostname, r.Header.Get(XForwardedHostHeader), r.Host)
// Collecting request info
stat, _ := httpx.GetResponseMeta(rw)
if err := sw.c.Enqueue(analytics.Page{
InstanceId: sw.instanceId,
DeploymentId: sw.o.DeploymentId,
Project: sw.o.Service,
InstanceId: sw.instanceId,
DeploymentId: sw.o.DeploymentId,
Project: sw.o.Service,
UrlHost: host,
UrlPath: path,
RequestCode: stat,
@ -316,11 +319,21 @@ func (sw *Service) UnaryInterceptor(ctx context.Context, req interface{}, info *
latency := time.Since(start).Milliseconds()
if err := sw.c.Enqueue(analytics.Page{
InstanceId: sw.instanceId,
DeploymentId: sw.o.DeploymentId,
Project: sw.o.Service,
hosts := []string{sw.o.Hostname}
if md, ok := metadata.FromIncomingContext(ctx); ok {
for _, h := range knownHeaders {
if v := md.Get(h); len(v) > 0 {
hosts = append(hosts, v[0])
}
}
}
host := urlx.ExtractPublicAddress(hosts...)
if err := sw.c.Enqueue(analytics.Page{
InstanceId: sw.instanceId,
DeploymentId: sw.o.DeploymentId,
Project: sw.o.Service,
UrlHost: host,
UrlPath: info.FullMethod,
RequestCode: int(status.Code(err)),
RequestLatency: int(latency),
@ -369,7 +382,3 @@ func (sw *Service) anonymizeQuery(query url.Values, salt string) string {
}
return query.Encode()
}
func (sw *Service) determineURLHost(xForwardedHostHeader, hostHeader string) string {
return cmp.Or(sw.o.Hostname, xForwardedHostHeader, hostHeader)
}

137
oryx/urlx/extract.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package urlx
import (
"context"
"net"
"net/url"
"strings"
"sync"
"time"
)
// hostCache caches DNS lookup results for hostnames to avoid repeated lookups.
// The cache is thread-safe and stores true/false whether a hostname resolves to public IPs.
type hostCache struct {
mu sync.RWMutex
cache map[string]bool
}
// get retrieves a cached value for a hostname. Returns value and whether it was found.
func (hc *hostCache) get(hostname string) (bool, bool) {
hc.mu.RLock()
defer hc.mu.RUnlock()
isPublic, found := hc.cache[hostname]
return isPublic, found
}
// set stores the lookup result for a hostname.
func (hc *hostCache) set(hostname string, isPublic bool) {
hc.mu.Lock()
defer hc.mu.Unlock()
hc.cache[hostname] = isPublic
}
// localCache lives for the lifetime of the main process. The cache
// size is not expected to grow more than a few hundred bytes.
var localCache = &hostCache{
cache: make(map[string]bool),
}
// ExtractPublicAddress iterates over parameters and extracts the first public
// address found. Parameter values are assumed to be in priority order. Returns
// an empty string if only private addresses are available.
func ExtractPublicAddress(values ...string) string {
for _, value := range values {
if value == "" || value == "*" {
continue
}
host := value
// parse URL addresses
if u, err := url.Parse(value); err == nil && len(u.Host) > 1 {
host = removeWildcardsFromHostname(u.Host)
}
// strip port on both URL and non-URL addresses
hostname, _, err := net.SplitHostPort(host)
if err != nil {
hostname = host
}
// for IP addresses
if ip := net.ParseIP(hostname); ip != nil {
if !isPrivateIP(ip) {
return host
}
continue
}
// for hostnames, first check cache
if isPublic, found := localCache.get(hostname); found {
if isPublic {
return host
}
continue
}
// otherwise, perform DNS lookup & cache result
isPublic := isPublicHostname(hostname)
localCache.set(hostname, isPublic)
if isPublic {
return host
}
}
return ""
}
// isPrivateIP checks if an IP address is private (RFC 1918/4193).
func isPrivateIP(ip net.IP) bool {
return ip.IsPrivate() ||
ip.IsLoopback() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsUnspecified() // 0.0.0.0 or ::
}
// isPublicHostname performs DNS lookup to determine if hostname resolves to public IPs.
// Returns true if at least one resolved IP is public, false if all are private or lookup fails.
func isPublicHostname(hostname string) bool {
// avoid DNS lookup if localhost
lower := strings.ToLower(hostname)
if lower == "localhost" {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
if err != nil {
return false
}
for _, ip := range ips {
if !isPrivateIP(ip.IP) {
return true
}
}
return false
}
// removeWildcardsFromHostname removes wildcard segments from a hostname string
// by splitting on dots and filtering out asterisk-only segments.
func removeWildcardsFromHostname(hostname string) string {
sep := strings.Split(hostname, ".")
clean := make([]string, 0, len(sep))
for _, s := range sep {
if s != "*" && s != "" {
clean = append(clean, s)
}
}
return strings.Join(clean, ".")
}