Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/executor/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ var RootCmd = &cobra.Command{
if err := executor.DoPush(image, opts); err != nil {
exit(fmt.Errorf("error pushing image: %w", err))
}
util.LogRegistryConnections()
tracing.Shutdown(nil)
},
}
Expand Down Expand Up @@ -574,6 +575,7 @@ func exit(err error) {
// exits with the given error and exit code
func exitWithCode(err error, exitCode int) {
fmt.Fprintln(os.Stderr, err)
util.LogRegistryConnections()
tracing.Shutdown(err)
os.Exit(exitCode)
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/warmer/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ var RootCmd = &cobra.Command{
if err := warmer.WarmCache(opts); err != nil {
exit(fmt.Errorf("failed warming cache: %w", err))
}
util.LogRegistryConnections()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
}

Expand Down Expand Up @@ -170,5 +171,6 @@ func isURL(path string) bool {

func exit(err error) {
fmt.Println(err)
util.LogRegistryConnections()
os.Exit(1)
}
11 changes: 11 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ Attribute values are capped at 64 KiB. `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT`
| `kaniko.build_id` | sha256 of Dockerfile content + target, for grouping runs of the same build (falls back to the path when the Dockerfile is unreadable) |
| `kaniko.ff.*` | explicitly-set `FF_KANIKO_*` feature flags (flags left at their defaults are not reported) |
| `service.name` | `kaniko`, unless `OTEL_SERVICE_NAME` is set |
| `kaniko.registry.sockets.opened` | TCP connections the build made to registries |
| `kaniko.registry.sockets.closed` | how many of those were closed before the build ended |
| `kaniko.registry.sockets.open_at_exit` | connections still open when the build ended |
| `kaniko.registry.sockets.peak` | highest number open at the same time |
| `kaniko.registry.requests` | HTTP requests to registries |
| `kaniko.registry.requests.reused` | how many of those reused a connection |
| `kaniko.registry.tls.handshakes` | TLS handshakes |
| `kaniko.registry.tls.ms` | time those handshakes took |
| `kaniko.registry.dial.ms` | time spent opening connections |
| `kaniko.registry.idle.ms` | total time connections sat idle before being reused |

## Command spans

Expand All @@ -38,3 +48,4 @@ Attribute values are capped at 64 KiB. `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT`
| `kaniko.stage` | stage index (integer) |
| `kaniko.cache.hit` | `true` when the command was replayed from cache (only with `--cache`, absent when caching is off) |
| `kaniko.cache.key` | cache key for the command (only with `--cache`) |

150 changes: 150 additions & 0 deletions pkg/connstats/connstats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
Copyright 2026 OSS Container Tools

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 connstats counts the sockets and requests of the transports built by
// util.MakeTransport, so registry connection reuse is visible without a proxy in
// front of the registry. A proxy cannot see through TLS to a real registry.
package connstats

import (
"context"
"crypto/tls"
"net"
"net/http"
"net/http/httptrace"
"sync"
"sync/atomic"
"time"
)

var (
socketsOpened atomic.Int64
socketsClosed atomic.Int64
socketsOpen atomic.Int64
peakOpen atomic.Int64
dialTime atomic.Int64
requests atomic.Int64
reused atomic.Int64
handshakes atomic.Int64
handshakeTime atomic.Int64
idleTime atomic.Int64
)

// DialFunc matches http.Transport.DialContext.
type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)

// WrapDial counts every socket the transport opens and closes. net/http reports
// neither to its caller, and httptrace has no close hook.
func WrapDial(dial DialFunc) DialFunc {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
start := time.Now()
conn, err := dial(ctx, network, addr)
// a dial that fails still spent the time, and a build against an
// unreachable registry spends most of its time here
dialTime.Add(int64(time.Since(start)))
if err != nil {
return nil, err
}
socketsOpened.Add(1)
open := socketsOpen.Add(1)
for {
peak := peakOpen.Load()
if open <= peak || peakOpen.CompareAndSwap(peak, open) {
break
}
}
return &countedConn{Conn: conn}, nil
}
}

type countedConn struct {
net.Conn
once sync.Once
}

func (c *countedConn) Close() error {
c.once.Do(func() {
socketsClosed.Add(1)
socketsOpen.Add(-1)
})
return c.Conn.Close()
}

// Trace records whether a request reused its connection, how long that
// connection sat idle first, and what the TLS handshake cost.
func Trace(rt http.RoundTripper) http.RoundTripper {
return &tracedTransport{inner: rt}
}

type tracedTransport struct {
inner http.RoundTripper
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (t *tracedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
requests.Add(1)
// both handshake hooks run on the goroutine doing the dial
var start time.Time
trace := &httptrace.ClientTrace{
TLSHandshakeStart: func() {
start = time.Now()
},
TLSHandshakeDone: func(_ tls.ConnectionState, err error) {
if err != nil {
return
}
handshakes.Add(1)
handshakeTime.Add(int64(time.Since(start)))
},
GotConn: func(info httptrace.GotConnInfo) {
if info.Reused {
reused.Add(1)
}
if info.WasIdle {
idleTime.Add(int64(info.IdleTime))
}
},
}
return t.inner.RoundTrip(r.WithContext(httptrace.WithClientTrace(r.Context(), trace)))
}

// Stats totals the registry traffic so far.
type Stats struct {
SocketsOpened int64
SocketsClosed int64
SocketsOpen int64
PeakSockets int64
Requests int64
Reused int64
TLSHandshakes int64
DialTime time.Duration
TLSTime time.Duration
IdleTime time.Duration
}

func Snapshot() Stats {
return Stats{
SocketsOpened: socketsOpened.Load(),
SocketsClosed: socketsClosed.Load(),
SocketsOpen: socketsOpen.Load(),
PeakSockets: peakOpen.Load(),
Requests: requests.Load(),
Reused: reused.Load(),
TLSHandshakes: handshakes.Load(),
DialTime: time.Duration(dialTime.Load()),
TLSTime: time.Duration(handshakeTime.Load()),
IdleTime: time.Duration(idleTime.Load()),
}
}
17 changes: 17 additions & 0 deletions pkg/tracing/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (

"github.com/osscontainertools/kaniko/pkg/assert"
"github.com/osscontainertools/kaniko/pkg/config"
"github.com/osscontainertools/kaniko/pkg/connstats"
"github.com/osscontainertools/kaniko/pkg/timing"
"github.com/osscontainertools/kaniko/pkg/version"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -145,6 +146,21 @@ func onAssertion(name, msg string) {
Shutdown(fmt.Errorf("assertion violated [%s]: %s", name, msg))
}

func registryAttrs(s connstats.Stats) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.Int64("kaniko.registry.sockets.opened", s.SocketsOpened),
attribute.Int64("kaniko.registry.sockets.closed", s.SocketsClosed),
attribute.Int64("kaniko.registry.sockets.open_at_exit", s.SocketsOpen),
attribute.Int64("kaniko.registry.sockets.peak", s.PeakSockets),
attribute.Int64("kaniko.registry.requests", s.Requests),
attribute.Int64("kaniko.registry.requests.reused", s.Reused),
attribute.Int64("kaniko.registry.tls.handshakes", s.TLSHandshakes),
attribute.Int64("kaniko.registry.tls.ms", s.TLSTime.Milliseconds()),
attribute.Int64("kaniko.registry.dial.ms", s.DialTime.Milliseconds()),
attribute.Int64("kaniko.registry.idle.ms", s.IdleTime.Milliseconds()),
}
}

// buildAttrs holds what kaniko knows; fleet identity comes from
// OTEL_RESOURCE_ATTRIBUTES. build_id groups runs of the same Dockerfile+target.
func buildAttrs(opts *config.KanikoOptions, dockerfile []byte) []attribute.KeyValue {
Expand Down Expand Up @@ -189,6 +205,7 @@ func Shutdown(err error) {
return
}
if rootSpan != nil {
rootSpan.SetAttributes(registryAttrs(connstats.Snapshot())...)
if err != nil {
rootSpan.SetStatus(codes.Error, err.Error())
} else {
Expand Down
51 changes: 42 additions & 9 deletions pkg/util/transport_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ import (
"net/http"
"os"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/osscontainertools/kaniko/pkg/assert"
"github.com/osscontainertools/kaniko/pkg/config"
"github.com/osscontainertools/kaniko/pkg/connstats"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -77,18 +81,36 @@ func init() {
systemKeyPairLoader = &X509KeyPairLoader{}
}

// connstats.Trace drops CloseIdleConnections, which nothing notices while
// go-containerregistry's own wrapper drops it too.
func init() {
_, forwards := any(transport.NewRetry(nil)).(interface{ CloseIdleConnections() })
assert.Assert("util.transport.close-idle-dropped", !forwards, "go-containerregistry forwards CloseIdleConnections, so the connstats wrapper has to forward it as well")
}

// MakeTransport returns a transport for registryName, wired up to count the
// sockets and requests it makes.
func MakeTransport(opts config.RegistryOptions, registryName string) (http.RoundTripper, error) {
tr, err := makeTransport(opts, registryName)
if err != nil {
return nil, err
}
tr.DialContext = connstats.WrapDial(tr.DialContext)
return connstats.Trace(tr), nil
}
Comment thread
mzihlmann marked this conversation as resolved.

func makeTransport(opts config.RegistryOptions, registryName string) (*http.Transport, error) {
// Create a transport to set our user-agent.
var tr http.RoundTripper = http.DefaultTransport.(*http.Transport).Clone()
tr := http.DefaultTransport.(*http.Transport).Clone()
if opts.SkipTLSVerify || opts.SkipTLSVerifyRegistries.Contains(registryName) {
tr.(*http.Transport).TLSClientConfig = &tls.Config{
tr.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
} else if certificatePath := opts.RegistriesCertificates[registryName]; certificatePath != "" {
if err := systemCertLoader.append(certificatePath); err != nil {
return nil, fmt.Errorf("failed to load certificate %s for %s: %w", certificatePath, registryName, err)
}
tr.(*http.Transport).TLSClientConfig = &tls.Config{
tr.TLSClientConfig = &tls.Config{
RootCAs: systemCertLoader.value(),
}
}
Expand All @@ -102,17 +124,28 @@ func MakeTransport(opts config.RegistryOptions, registryName string) (http.Round
if err != nil {
return nil, fmt.Errorf("failed to load client certificate/key '%s' for %s: %w", clientCertificatePath, registryName, err)
}
tr.(*http.Transport).TLSClientConfig.Certificates = []tls.Certificate{cert}
if tr.TLSClientConfig == nil {
tr.TLSClientConfig = &tls.Config{}
}
tr.TLSClientConfig.Certificates = []tls.Certificate{cert}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if config.FF.DisableHTTP2 {
t := tr.(*http.Transport)
t.ForceAttemptHTTP2 = false
if t.TLSClientConfig == nil {
t.TLSClientConfig = &tls.Config{}
tr.ForceAttemptHTTP2 = false
if tr.TLSClientConfig == nil {
tr.TLSClientConfig = &tls.Config{}
}
t.TLSClientConfig.NextProtos = []string{"http/1.1"}
tr.TLSClientConfig.NextProtos = []string{"http/1.1"}
}

return tr, nil
}

// LogRegistryConnections reports how the build used its registry sockets.
func LogRegistryConnections() {
stats := connstats.Snapshot()
logrus.Debugf("registry connections: sockets opened=%d closed=%d open=%d peak=%d, requests=%d reused=%d, tls handshakes=%d in %v, dialing %v, idle before reuse %v",
stats.SocketsOpened, stats.SocketsClosed, stats.SocketsOpen, stats.PeakSockets,
stats.Requests, stats.Reused, stats.TLSHandshakes, stats.TLSTime.Round(time.Millisecond),
stats.DialTime.Round(time.Millisecond), stats.IdleTime.Round(time.Millisecond))
}
5 changes: 2 additions & 3 deletions pkg/util/transport_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"testing"

"github.com/osscontainertools/kaniko/pkg/config"
Expand Down Expand Up @@ -162,10 +161,10 @@ func Test_makeTransport(t *testing.T) {
systemCertLoader = certPool
systemKeyPairLoader = &mockedKeyPairLoader{}
t.Run(tt.name, func(_ *testing.T) {
tr, err := MakeTransport(tt.opts, registryName)
tr, err := makeTransport(tt.opts, registryName)
var tlsConfig *tls.Config
if err == nil {
tlsConfig = tr.(*http.Transport).TLSClientConfig
tlsConfig = tr.TLSClientConfig
}
tt.check(tlsConfig, certPool, err)
})
Expand Down
Loading