Skip to content
Open
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2c9466a
add http middleware
krishankumar01 Dec 23, 2025
7ccca56
add http transport
krishankumar01 Dec 29, 2025
8df0782
optimise http telemetry package
krishankumar01 Dec 29, 2025
01ecf4b
add test cases for Telemetry middleware
krishankumar01 Dec 29, 2025
0d8fda5
add auto instrumentation configs
krishankumar01 Dec 29, 2025
b2c07d5
Merge branch 'master' into kkumar-gcc/#726-auto-instrumentation-2
krishankumar01 Jan 2, 2026
395f109
add config to enable Telemetry for http clients
krishankumar01 Jan 2, 2026
bb1f529
add docs for config facade
krishankumar01 Jan 2, 2026
441ba96
add ConfigFacade nil warning
krishankumar01 Jan 2, 2026
cb3cb89
Merge branch 'master' into kkumar-gcc/#726-auto-instrumentation-2
krishankumar01 Jan 4, 2026
7c59c97
disable default telemetry
krishankumar01 Jan 4, 2026
ab72e2a
optimise transport
krishankumar01 Jan 4, 2026
413bdd6
add kill switch for instrumentation
krishankumar01 Jan 4, 2026
f999029
update the stubs
krishankumar01 Jan 4, 2026
98ec1f7
remove unnecessary handler
krishankumar01 Jan 4, 2026
ad59c56
optimise log instrumentation
krishankumar01 Jan 4, 2026
1fea34b
move route registration in the end
krishankumar01 Jan 4, 2026
794ed71
lazily initialize middleware and transport to work with new applicati…
krishankumar01 Jan 4, 2026
76021c2
optimise channel test
krishankumar01 Jan 4, 2026
eb97e1b
optimise channel test
krishankumar01 Jan 4, 2026
c23884a
merge master
krishankumar01 Jan 18, 2026
e4faad6
accept telemetry facade as an input instead of using global instance
krishankumar01 Jan 18, 2026
5141d18
use a callback to resolve the telemetry facade instance
krishankumar01 Jan 18, 2026
5a26971
optimise grpc handler to remove usage of telemetry and config facade
krishankumar01 Jan 18, 2026
5a5d9fc
optimise the grpc handler
krishankumar01 Jan 18, 2026
75130b9
Merge branch 'master' into kkumar-gcc/#726-auto-instrumentation-2
krishankumar01 Jan 20, 2026
09f4745
use telemetry transport if enabled
krishankumar01 Jan 20, 2026
9793141
optimise http auto instrumentation
krishankumar01 Jan 20, 2026
bd0645b
optimize log test cases
krishankumar01 Jan 20, 2026
f7e34b8
optimise
krishankumar01 Jan 21, 2026
777ce25
optimise
krishankumar01 Jan 21, 2026
43a7d8f
optimise
krishankumar01 Jan 26, 2026
9d3460c
optimise
krishankumar01 Jan 26, 2026
4fcd103
Merge branch 'master' into kkumar-gcc/#726-auto-instrumentation-2
krishankumar01 Jan 26, 2026
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 contracts/telemetry/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
oteltrace "go.opentelemetry.io/otel/trace"
)

type Resolver = func() Telemetry

type Telemetry interface {
// Logger returns a log.Logger instance for emitting structured log records under the given instrumentation name.
// Optional log.LoggerOption parameters allow customization of logger behavior.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v3 v3.6.2
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/contrib/propagators/b3 v1.39.0
go.opentelemetry.io/otel v1.39.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.15.0
Expand Down Expand Up @@ -62,6 +63,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chigopher/pathlib v0.19.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
Expand Down Expand Up @@ -274,6 +276,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0 h1:PI7pt9pkSnimWcp5sQhUA9OzLbc3Ba4sL+VEUTNsxrk=
go.opentelemetry.io/contrib/propagators/b3 v1.39.0/go.mod h1:5gV/EzPnfYIwjzj+6y8tbGW2PKWhcsz5e/7twptRVQY=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
Expand Down
3 changes: 3 additions & 0 deletions http/client/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ type Config struct {
// IdleConnTimeout is the maximum amount of time an idle (keep-alive) connection
// will remain idle before closing itself.
IdleConnTimeout time.Duration `json:"idle_conn_timeout"`

// EnableTelemetry determines if OpenTelemetry tracing/metrics are enabled for this client.
EnableTelemetry bool `json:"enable_telemetry"`
}
39 changes: 28 additions & 11 deletions http/client/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ import (
"net/http"
"sync"

contractsconfig "github.com/goravel/framework/contracts/config"
"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/contracts/http/client"
contractstelemetry "github.com/goravel/framework/contracts/telemetry"
"github.com/goravel/framework/errors"
telemetryhttp "github.com/goravel/framework/telemetry/instrumentation/http"
)

var _ client.Factory = (*Factory)(nil)

type Factory struct {
client.Request

json foundation.Json
config *FactoryConfig
factoryConfig *FactoryConfig
config contractsconfig.Config
json foundation.Json
telemetryResolver contractstelemetry.Resolver

clients sync.Map
mu sync.RWMutex

Expand All @@ -24,14 +30,21 @@ type Factory struct {
stray []string
}

func NewFactory(config *FactoryConfig, json foundation.Json) (*Factory, error) {
if config == nil {
func NewFactory(
factoryConfig *FactoryConfig,
config contractsconfig.Config,
json foundation.Json,
telemetryResolver contractstelemetry.Resolver,
) (*Factory, error) {
if factoryConfig == nil {
return nil, errors.HttpClientConfigNotSet
}

factory := &Factory{
config: config,
json: json,
factoryConfig: factoryConfig,
config: config,
json: json,
telemetryResolver: telemetryResolver,
}

if err := factory.bindDefault(); err != nil {
Expand Down Expand Up @@ -81,7 +94,7 @@ func (r *Factory) AssertSentCount(count int) bool {
}

func (r *Factory) Client(names ...string) client.Request {
name := r.config.Default
name := r.factoryConfig.Default
if len(names) > 0 && names[0] != "" {
name = names[0]
}
Expand All @@ -95,7 +108,7 @@ func (r *Factory) Client(names ...string) client.Request {
return newRequestWithError(err)
}

cfg := r.config.Clients[name]
cfg := r.factoryConfig.Clients[name]
return NewRequest(httpClient, r.json, cfg.BaseUrl, name)
}

Expand Down Expand Up @@ -151,15 +164,15 @@ func (r *Factory) Sequence() client.FakeSequence {
}

func (r *Factory) bindDefault() error {
name := r.config.Default
name := r.factoryConfig.Default
c, err := r.resolveClient(name, r.fakeState)
if err != nil {
return err
}

// Bind the default client to the embedded Request implementation
// so that methods like Http.Get() use the default configuration.
r.Request = NewRequest(c, r.json, r.config.Clients[name].BaseUrl, name)
r.Request = NewRequest(c, r.json, r.factoryConfig.Clients[name].BaseUrl, name)

return nil
}
Expand All @@ -184,7 +197,7 @@ func (r *Factory) resolveClient(name string, state *FakeState) (*http.Client, er
return val.(*http.Client), nil
}

cfg, ok := r.config.Clients[name]
cfg, ok := r.factoryConfig.Clients[name]
if !ok {
return nil, errors.HttpClientConnectionNotFound.Args(name)
}
Expand All @@ -198,6 +211,10 @@ func (r *Factory) resolveClient(name string, state *FakeState) (*http.Client, er
baseTransport.IdleConnTimeout = cfg.IdleConnTimeout

var transport http.RoundTripper = baseTransport
if cfg.EnableTelemetry && r.telemetryResolver != nil {
transport = telemetryhttp.NewTransport(r.config, r.telemetryResolver(), transport)
}

if state != nil {
// If testing mode is active, wrap the real transport with our interceptor.
transport = NewFakeTransport(state, baseTransport, r.json)
Expand Down
100 changes: 81 additions & 19 deletions http/client/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,28 @@ import (
"time"

"github.com/stretchr/testify/suite"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
metricnoop "go.opentelemetry.io/otel/metric/noop"
"go.opentelemetry.io/otel/propagation"
tracenoop "go.opentelemetry.io/otel/trace/noop"

"github.com/goravel/framework/contracts/foundation"
"github.com/goravel/framework/contracts/http/client"
contractstelemetry "github.com/goravel/framework/contracts/telemetry"
"github.com/goravel/framework/errors"
"github.com/goravel/framework/foundation/json"
mocksconfig "github.com/goravel/framework/mocks/config"
mockstelemetry "github.com/goravel/framework/mocks/telemetry"
)

type FactoryTestSuite struct {
suite.Suite
json foundation.Json
factory *Factory
config *FactoryConfig
json foundation.Json
factory *Factory
factoryConfig *FactoryConfig
mockConfig *mocksconfig.Config
mockTelemetry *mockstelemetry.Telemetry
mockResolver contractstelemetry.Resolver
}

func TestFactoryTestSuite(t *testing.T) {
Expand All @@ -30,7 +40,14 @@ func TestFactoryTestSuite(t *testing.T) {

func (s *FactoryTestSuite) SetupTest() {
s.json = json.New()
s.config = &FactoryConfig{
s.mockConfig = mocksconfig.NewConfig(s.T())
s.mockTelemetry = mockstelemetry.NewTelemetry(s.T())

s.mockResolver = func() contractstelemetry.Telemetry {
return s.mockTelemetry
}

s.factoryConfig = &FactoryConfig{
Default: "main",
Clients: map[string]Config{
"main": {
Expand All @@ -48,7 +65,7 @@ func (s *FactoryTestSuite) SetupTest() {
},
}
var err error
s.factory, err = NewFactory(s.config, s.json)
s.factory, err = NewFactory(s.factoryConfig, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)
}

Expand Down Expand Up @@ -77,8 +94,8 @@ func (s *FactoryTestSuite) TestClient_Resolution() {
}

func (s *FactoryTestSuite) TestErrorHandling() {
s.Run("handles nil config safely", func() {
f, err := NewFactory(nil, s.json)
s.Run("handles nil factoryConfig safely", func() {
f, err := NewFactory(nil, s.mockConfig, s.json, s.mockResolver)
s.Nil(f)
s.ErrorIs(err, errors.HttpClientConfigNotSet)
})
Expand All @@ -93,12 +110,12 @@ func (s *FactoryTestSuite) TestErrorHandling() {
s.Contains(err.Error(), "[missing_client]")
})

s.Run("returns lazy error when default is empty in config", func() {
s.Run("returns lazy error when default is empty in factoryConfig", func() {
cfg := &FactoryConfig{
Default: "",
Clients: map[string]Config{"main": {}},
}
f, err := NewFactory(cfg, s.json)
f, err := NewFactory(cfg, s.mockConfig, s.json, s.mockResolver)
s.ErrorIs(err, errors.HttpClientDefaultNotSet)
s.Nil(f)
})
Expand All @@ -113,7 +130,7 @@ func (s *FactoryTestSuite) TestConcurrency() {
"new2": {BaseUrl: "https://new2.com", Timeout: 3 * time.Second},
},
}
f, err := NewFactory(cfg, s.json)
f, err := NewFactory(cfg, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

var wg sync.WaitGroup
Expand Down Expand Up @@ -163,7 +180,7 @@ func (s *FactoryTestSuite) TestBaseUrl_Override() {
"main": {BaseUrl: "https://wrong-url.com"},
},
}
f, err := NewFactory(cfg, s.json)
f, err := NewFactory(cfg, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

s.Run("overrides config base url", func() {
Expand All @@ -182,7 +199,12 @@ func (s *FactoryTestSuite) TestProxy_HttpMethods() {
}))
defer server.Close()

f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.json)
f, err := NewFactory(
&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}},
s.mockConfig,
s.json,
s.mockResolver,
)
s.NoError(err)

tests := []struct {
Expand Down Expand Up @@ -229,7 +251,12 @@ func (s *FactoryTestSuite) TestProxy_Headers() {
}))
defer server.Close()

f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.json)
f, err := NewFactory(
&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}},
s.mockConfig,
s.json,
s.mockResolver,
)
s.NoError(err)

s.Run("WithHeader & WithHeaders", func() {
Expand Down Expand Up @@ -306,7 +333,7 @@ func (s *FactoryTestSuite) TestProxy_QueryParameters() {
}))
defer server.Close()

f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.json)
f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

s.Run("WithQueryParameter", func() {
Expand Down Expand Up @@ -342,7 +369,7 @@ func (s *FactoryTestSuite) TestProxy_UrlParameters() {
}))
defer server.Close()

f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.json)
f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

s.Run("WithUrlParameter", func() {
Expand Down Expand Up @@ -371,7 +398,7 @@ func (s *FactoryTestSuite) TestProxy_Cookies() {
}))
defer server.Close()

f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.json)
f, err := NewFactory(&FactoryConfig{Default: "test", Clients: map[string]Config{"test": {BaseUrl: server.URL}}}, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

s.Run("WithCookie", func() {
Expand Down Expand Up @@ -423,7 +450,7 @@ func (s *FactoryTestSuite) TestRouting_Integration() {
"server_b": {BaseUrl: serverB.URL},
},
}
f, err := NewFactory(cfg, s.json)
f, err := NewFactory(cfg, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

s.Run("proxy methods hit default server", func() {
Expand All @@ -443,6 +470,41 @@ func (s *FactoryTestSuite) TestRouting_Integration() {
})
}

func (s *FactoryTestSuite) TestTelemetry_Integration() {
factoryConfig := &FactoryConfig{
Default: "telemetry_client",
Clients: map[string]Config{
"telemetry_client": {
BaseUrl: "https://example.com",
EnableTelemetry: true,
},
},
}

s.mockConfig.EXPECT().GetBool("telemetry.instrumentation.http_client", true).
Return(true).Once()

s.mockTelemetry.EXPECT().TracerProvider().Return(tracenoop.NewTracerProvider()).Once()
s.mockTelemetry.EXPECT().MeterProvider().Return(metricnoop.NewMeterProvider()).Once()
s.mockTelemetry.EXPECT().Propagator().Return(propagation.NewCompositeTextMapPropagator()).Once()

factory, err := NewFactory(factoryConfig, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

req := factory.Client("telemetry_client")
s.NotNil(req)

concreteReq, ok := req.(*Request)
s.True(ok, "Request should be of type *client.Request")

httpClient := concreteReq.HttpClient()
s.NotNil(httpClient.Transport)
s.IsType(&otelhttp.Transport{}, httpClient.Transport)

_, isPlainTransport := httpClient.Transport.(*http.Transport)
s.False(isPlainTransport)
}

func (s *FactoryTestSuite) TestFake_GithubUserProfile() {
userMap := map[string]any{
"login": "goravel",
Expand Down Expand Up @@ -547,7 +609,7 @@ func (s *FactoryTestSuite) TestFactory_AllowStrayRequests() {
}))
defer server.Close()

s.factory.config.Clients["github"] = Config{BaseUrl: server.URL}
s.factory.factoryConfig.Clients["github"] = Config{BaseUrl: server.URL}
s.factory.Reset()

s.factory.Fake(nil).PreventStrayRequests().AllowStrayRequests([]string{server.URL + "/*"})
Expand Down Expand Up @@ -593,7 +655,7 @@ func (s *FactoryTestSuite) TestIntegration_RealServer() {
"local": {BaseUrl: server.URL},
},
}
factory, err := NewFactory(cfg, s.json)
factory, err := NewFactory(cfg, s.mockConfig, s.json, s.mockResolver)
s.NoError(err)

resp, err := factory.Post("/repos", nil)
Expand Down
Loading
Loading