Skip to content

firetiger-oss/otelnet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

otelnet

OpenTelemetry instrumentation for Go's networking primitives.

This package provides tracing wrappers for dial functions and DNS resolvers, giving you visibility into what happens during connection establishment. It captures DNS resolution time, TCP handshake duration, and the actual IP addresses your application connects to.

Motivation

When you instrument an HTTP client with OpenTelemetry, you typically get a single span covering the entire request. While useful, this hides important details about what's happening at the network level:

  • DNS resolution: How long did it take to resolve the hostname? Did the resolver return multiple addresses? Which IP version was used?
  • Connection establishment: Which IP address did the client actually connect to? How long did the TCP handshake take? Did the client try multiple addresses before succeeding?
  • Failure diagnosis: When a connection fails, was it a DNS problem or a TCP timeout? If multiple addresses were returned, did all of them fail?

This information is critical for debugging latency issues, understanding network behavior, and diagnosing intermittent failures. A slow DNS resolver can add hundreds of milliseconds to your request latency. A server with multiple A records might have one unhealthy endpoint causing retries. IPv6 connectivity issues might be silently falling back to IPv4.

otelnet fills this observability gap by instrumenting the low-level dial and resolver functions that HTTP clients and other network code use internally. You get dedicated spans for DNS lookup, dial, and connection attempts, each with semantic attributes following OpenTelemetry conventions.

Installation

go get github.com/firetiger-oss/otelnet@latest

Usage

Instrumenting an HTTP Client

The most common use case is wrapping the dial function used by http.Transport:

package main

import (
    "net"
    "net/http"

    "github.com/firetiger-oss/otelnet"
)

func main() {
    // Wrap the default resolver with tracing
    resolver := otelnet.NewResolver(net.DefaultResolver)

    // Create an instrumented dial function
    dialer := &net.Dialer{}
    dialFunc := otelnet.DialFunc(dialer.DialContext, otelnet.WithResolver(resolver))

    // Use it in your HTTP transport
    transport := &http.Transport{
        DialContext: dialFunc,
    }

    client := &http.Client{Transport: transport}

    // Requests made with this client will now produce detailed network spans
    resp, err := client.Get("https://example.com")
    // ...
}

Instrumenting a Dial Function Without DNS Tracing

If you only want connection tracing without DNS resolution spans, don't pass a resolver:

dialFunc := otelnet.DialFunc(dialer.DialContext)

In this mode, DNS resolution still happens (via the underlying dial function), but no dns.lookup span is created. This is useful when connecting to IP addresses directly or when you want less verbose traces.

Using a Custom Resolver

You can wrap any resolver that implements the Resolver interface:

type Resolver interface {
    LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
}

This allows you to instrument custom DNS clients or caching resolvers:

customResolver := &MyCustomResolver{}
tracingResolver := otelnet.NewResolver(customResolver)

Produced Spans

The instrumentation creates the following spans:

dns.lookup (when using a tracing resolver)

  • dns.question.name: The hostname being resolved
  • dns.answer.count: Number of IP addresses returned

net.dial (parent span for the connection attempt)

  • destination.address: The target hostname or IP
  • destination.port: The target port
  • network.local.address: Local address after connection
  • network.local.port: Local port after connection
  • network.peer.address: Remote address after connection
  • network.peer.port: Remote port after connection
  • network.transport: tcp or udp
  • network.type: ipv4 or ipv6

net.connect (child span for each connection attempt)

  • network.peer.address: The IP address being connected to
  • network.peer.port: The port being connected to

When a hostname resolves to multiple IP addresses, you'll see multiple net.connect spans as children of the net.dial span, one for each address tried until a connection succeeds.

About

OpenTelemetry instrumentation for the net package

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages