Skip to content

Commit a5141de

Browse files
fix(instrumented_http): migrate to own http instrumenter
Signed-off-by: ivan katliarchuk <[email protected]>
1 parent b93d1e9 commit a5141de

File tree

9 files changed

+200
-34
lines changed

9 files changed

+200
-34
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ require (
3838
github.com/goccy/go-yaml v1.18.0
3939
github.com/google/go-cmp v0.7.0
4040
github.com/google/uuid v1.6.0
41-
github.com/linki/instrumented_http v0.3.0
4241
github.com/linode/linodego v1.53.0
4342
github.com/maxatome/go-testdeep v1.14.0
4443
github.com/miekg/dns v1.1.67

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -676,8 +676,6 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
676676
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
677677
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
678678
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
679-
github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA=
680-
github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk=
681679
github.com/linode/linodego v1.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI=
682680
github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA=
683681
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=

pkg/http/http.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// ref: https://github.com/linki/instrumented_http/blob/master/client.go
18+
19+
package http
20+
21+
import (
22+
"fmt"
23+
"net/http"
24+
"strings"
25+
"time"
26+
27+
"github.com/prometheus/client_golang/prometheus"
28+
)
29+
30+
var (
31+
requestDuration = prometheus.NewSummaryVec(
32+
prometheus.SummaryOpts{
33+
Name: "request_duration_seconds",
34+
Help: "The HTTP request latencies in seconds.",
35+
Subsystem: "http",
36+
ConstLabels: prometheus.Labels{"handler": "instrumented_http"},
37+
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
38+
},
39+
[]string{"scheme", "host", "path", "method", "status"},
40+
)
41+
)
42+
43+
func init() {
44+
prometheus.MustRegister(requestDuration)
45+
}
46+
47+
type CustomRoundTripper struct {
48+
next http.RoundTripper
49+
}
50+
51+
// CancelRequest is a no-op to satisfy interfaces that require it.
52+
// https://github.com/kubernetes/client-go/blob/34f52c14eaed7e50c845cc14e85e1c4c91e77470/transport/transport.go#L346
53+
func (r *CustomRoundTripper) CancelRequest(_ *http.Request) {
54+
}
55+
56+
func (r *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
57+
start := time.Now()
58+
resp, err := r.next.RoundTrip(req)
59+
60+
status := ""
61+
if resp != nil {
62+
status = fmt.Sprintf("%d", resp.StatusCode)
63+
}
64+
65+
labels := prometheus.Labels{
66+
"scheme": req.URL.Scheme,
67+
"host": req.URL.Host,
68+
"path": pathProcessor(req.URL.Path),
69+
"method": req.Method,
70+
"status": status,
71+
}
72+
requestDuration.With(labels).Observe(time.Since(start).Seconds())
73+
return resp, err
74+
}
75+
76+
func NewInstrumentedClient(next *http.Client) *http.Client {
77+
if next == nil {
78+
next = http.DefaultClient
79+
}
80+
81+
next.Transport = NewInstrumentedTransport(next.Transport)
82+
83+
return next
84+
}
85+
86+
func NewInstrumentedTransport(next http.RoundTripper) http.RoundTripper {
87+
if next == nil {
88+
next = http.DefaultTransport
89+
}
90+
91+
return &CustomRoundTripper{next: next}
92+
}
93+
94+
func pathProcessor(path string) string {
95+
parts := strings.Split(path, "/")
96+
return parts[len(parts)-1]
97+
}

pkg/http/http_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package http
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
"testing"
23+
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
type dummyTransport struct{}
28+
29+
func (d *dummyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
30+
return nil, fmt.Errorf("dummy error")
31+
}
32+
33+
func TestNewInstrumentedTransport(t *testing.T) {
34+
dt := &dummyTransport{}
35+
rt := NewInstrumentedTransport(dt)
36+
crt, ok := rt.(*CustomRoundTripper)
37+
require.True(t, ok)
38+
require.Equal(t, dt, crt.next)
39+
40+
// Should default to http.DefaultTransport if nil
41+
rt2 := NewInstrumentedTransport(nil)
42+
crt2, ok := rt2.(*CustomRoundTripper)
43+
require.True(t, ok)
44+
require.Equal(t, http.DefaultTransport, crt2.next)
45+
}
46+
47+
func TestNewInstrumentedClient(t *testing.T) {
48+
client := &http.Client{Transport: &dummyTransport{}}
49+
result := NewInstrumentedClient(client)
50+
require.Equal(t, client, result)
51+
_, ok := result.Transport.(*CustomRoundTripper)
52+
require.True(t, ok)
53+
54+
// Should default to http.DefaultClient if nil
55+
result2 := NewInstrumentedClient(nil)
56+
require.Equal(t, http.DefaultClient, result2)
57+
_, ok = result2.Transport.(*CustomRoundTripper)
58+
require.True(t, ok)
59+
}
60+
61+
func TestPathProcessor(t *testing.T) {
62+
tests := []struct {
63+
input string
64+
expected string
65+
}{
66+
{"/foo/bar", "bar"},
67+
{"/foo/", ""},
68+
{"/", ""},
69+
{"", ""},
70+
{"/foo/bar/baz", "baz"},
71+
{"foo/bar", "bar"},
72+
{"foo", "foo"},
73+
{"foo/", ""},
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.input, func(t *testing.T) {
78+
require.Equal(t, tt.expected, pathProcessor(tt.input))
79+
})
80+
}
81+
}

provider/aws/config.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ import (
2020
"context"
2121
"fmt"
2222
"net/http"
23-
"strings"
2423

2524
awsv2 "github.com/aws/aws-sdk-go-v2/aws"
2625
"github.com/aws/aws-sdk-go-v2/aws/retry"
2726
"github.com/aws/aws-sdk-go-v2/config"
2827
stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds"
2928
"github.com/aws/aws-sdk-go-v2/service/sts"
30-
"github.com/linki/instrumented_http"
3129
"github.com/sirupsen/logrus"
3230

31+
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
32+
3333
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
3434
)
3535

@@ -84,12 +84,7 @@ func newV2Config(awsConfig AWSSessionConfig) (awsv2.Config, error) {
8484
config.WithRetryer(func() awsv2.Retryer {
8585
return retry.AddWithMaxAttempts(retry.NewStandard(), awsConfig.APIRetries)
8686
}),
87-
config.WithHTTPClient(instrumented_http.NewClient(&http.Client{}, &instrumented_http.Callbacks{
88-
PathProcessor: func(path string) string {
89-
parts := strings.Split(path, "/")
90-
return parts[len(parts)-1]
91-
},
92-
})),
87+
config.WithHTTPClient(extdnshttp.NewInstrumentedClient(&http.Client{})),
9388
config.WithSharedConfigProfile(awsConfig.Profile),
9489
}
9590

provider/google/google.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ import (
2020
"context"
2121
"fmt"
2222
"sort"
23-
"strings"
2423
"time"
2524

2625
"cloud.google.com/go/compute/metadata"
27-
"github.com/linki/instrumented_http"
2826
log "github.com/sirupsen/logrus"
2927
"golang.org/x/oauth2/google"
3028
dns "google.golang.org/api/dns/v1"
3129
googleapi "google.golang.org/api/googleapi"
3230
"google.golang.org/api/option"
3331

32+
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
33+
3434
"sigs.k8s.io/external-dns/endpoint"
3535
"sigs.k8s.io/external-dns/plan"
3636
"sigs.k8s.io/external-dns/provider"
@@ -131,12 +131,7 @@ func NewGoogleProvider(ctx context.Context, project string, domainFilter *endpoi
131131
return nil, err
132132
}
133133

134-
gcloud = instrumented_http.NewClient(gcloud, &instrumented_http.Callbacks{
135-
PathProcessor: func(path string) string {
136-
parts := strings.Split(path, "/")
137-
return parts[len(parts)-1]
138-
},
139-
})
134+
gcloud = extdnshttp.NewInstrumentedClient(gcloud)
140135

141136
dnsClient, err := dns.NewService(ctx, option.WithHTTPClient(gcloud))
142137
if err != nil {

provider/pihole/client.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ import (
2828
"net/url"
2929
"strings"
3030

31-
"github.com/linki/instrumented_http"
3231
log "github.com/sirupsen/logrus"
3332
"golang.org/x/net/html"
3433

34+
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
35+
3536
"sigs.k8s.io/external-dns/endpoint"
3637
"sigs.k8s.io/external-dns/provider"
3738
)
@@ -71,7 +72,8 @@ func newPiholeClient(cfg PiholeConfig) (piholeAPI, error) {
7172
},
7273
},
7374
}
74-
cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{})
75+
76+
cl := extdnshttp.NewInstrumentedClient(httpClient)
7577

7678
p := &piholeClient{
7779
cfg: cfg,

provider/pihole/clientV6.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ import (
3030
"strconv"
3131
"strings"
3232

33-
"github.com/linki/instrumented_http"
3433
log "github.com/sirupsen/logrus"
3534

35+
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
36+
3637
"sigs.k8s.io/external-dns/endpoint"
3738
"sigs.k8s.io/external-dns/provider"
3839
)
@@ -65,7 +66,7 @@ func newPiholeClientV6(cfg PiholeConfig) (piholeAPI, error) {
6566
},
6667
}
6768

68-
cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{})
69+
cl := extdnshttp.NewInstrumentedClient(httpClient)
6970

7071
p := &piholeClientV6{
7172
cfg: cfg,
@@ -164,17 +165,17 @@ func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endp
164165
DNSName, Target = recs[1], recs[0]
165166
switch rtype {
166167
case endpoint.RecordTypeA:
167-
//PiHole return A and AAAA records. Filter to only keep the A records
168+
// PiHole return A and AAAA records. Filter to only keep the A records
168169
if !isValidIPv4(Target) {
169170
continue
170171
}
171172
case endpoint.RecordTypeAAAA:
172-
//PiHole return A and AAAA records. Filter to only keep the AAAA records
173+
// PiHole return A and AAAA records. Filter to only keep the AAAA records
173174
if !isValidIPv6(Target) {
174175
continue
175176
}
176177
case endpoint.RecordTypeCNAME:
177-
//PiHole return only CNAME records.
178+
// PiHole return only CNAME records.
178179
// CNAME format is DNSName,target, ttl?
179180
DNSName, Target = recs[0], recs[1]
180181
if len(recs) == 3 { // TTL is present

source/store.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ import (
2222
"fmt"
2323
"net/http"
2424
"os"
25-
"strings"
25+
2626
"sync"
2727
"time"
2828

2929
"github.com/cloudfoundry-community/go-cfclient"
30-
"github.com/linki/instrumented_http"
3130
openshift "github.com/openshift/client-go/route/clientset/versioned"
3231
log "github.com/sirupsen/logrus"
3332
istioclient "istio.io/client-go/pkg/clientset/versioned"
@@ -38,6 +37,8 @@ import (
3837
"k8s.io/client-go/tools/clientcmd"
3938
gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
4039

40+
extdnshttp "sigs.k8s.io/external-dns/pkg/http"
41+
4142
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
4243
)
4344

@@ -623,14 +624,11 @@ func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time
623624
if err != nil {
624625
return nil, err
625626
}
627+
626628
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
627-
return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{
628-
PathProcessor: func(path string) string {
629-
parts := strings.Split(path, "/")
630-
return parts[len(parts)-1]
631-
},
632-
})
629+
return extdnshttp.NewInstrumentedTransport(rt)
633630
}
631+
634632
config.Timeout = requestTimeout
635633
return config, nil
636634
}

0 commit comments

Comments
 (0)