mirror of https://github.com/ory/kratos
feat: domain telemetry improvements
GitOrigin-RevId: 9a0825160976ff16b7a39024e650ecfaf9ce82a5
This commit is contained in:
parent
a4e7ff83cf
commit
93345d7b9f
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, ".")
|
||||
}
|
||||
Loading…
Reference in New Issue