Skip to content

Commit 6993fa1

Browse files
authored
Middleware: HTTP support (part 3/4) (#12845)
#### Description Adds the HTTP middleware support from #12842. #### Link to tracking issue Part of #12603. #### Testing Yes. #### Documentation Added.
1 parent 9a54388 commit 6993fa1

File tree

21 files changed

+694
-24
lines changed

21 files changed

+694
-24
lines changed

.chloggen/middleware-http.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
7+
component: confighttp
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Add HTTP middleware support.
11+
12+
# One or more tracking issues or pull requests related to the change
13+
issues: [12603, 9591, 7441]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# Optional: The change log or logs in which this entry should be included.
21+
# e.g. '[user]' or '[user, api]'
22+
# Include 'user' if the change is relevant to end users.
23+
# Include 'api' if there is a change to a library API.
24+
# Default: '[user]'
25+
change_logs: [user]

cmd/builder/internal/builder/main_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var replaceModules = []string{
4747
"/config/configcompression",
4848
"/config/configgrpc",
4949
"/config/confighttp",
50+
"/config/configmiddleware",
5051
"/config/confignet",
5152
"/config/configopaque",
5253
"/config/configretry",
@@ -79,6 +80,8 @@ var replaceModules = []string{
7980
"/extension/extensionauth",
8081
"/extension/extensionauth/extensionauthtest",
8182
"/extension/extensioncapabilities",
83+
"/extension/extensionmiddleware",
84+
"/extension/extensionmiddleware/extensionmiddlewaretest",
8285
"/extension/extensiontest",
8386
"/extension/zpagesextension",
8487
"/extension/xextension",

cmd/otelcorecol/builder-config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ replaces:
4646
- go.opentelemetry.io/collector/config/configcompression => ../../config/configcompression
4747
- go.opentelemetry.io/collector/config/configgrpc => ../../config/configgrpc
4848
- go.opentelemetry.io/collector/config/confighttp => ../../config/confighttp
49+
- go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
4950
- go.opentelemetry.io/collector/config/confignet => ../../config/confignet
5051
- go.opentelemetry.io/collector/config/configopaque => ../../config/configopaque
5152
- go.opentelemetry.io/collector/config/configretry => ../../config/configretry
@@ -79,6 +80,8 @@ replaces:
7980
- go.opentelemetry.io/collector/extension/extensionauth => ../../extension/extensionauth
8081
- go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest => ../../extension/extensionauth/extensionauthtest
8182
- go.opentelemetry.io/collector/extension/extensioncapabilities => ../../extension/extensioncapabilities
83+
- go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
84+
- go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
8285
- go.opentelemetry.io/collector/extension/extensiontest => ../../extension/extensiontest
8386
- go.opentelemetry.io/collector/extension/memorylimiterextension => ../../extension/memorylimiterextension
8487
- go.opentelemetry.io/collector/extension/xextension => ../../extension/xextension

cmd/otelcorecol/go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ require (
8989
go.opentelemetry.io/collector/config/configcompression v1.30.0 // indirect
9090
go.opentelemetry.io/collector/config/configgrpc v0.124.0 // indirect
9191
go.opentelemetry.io/collector/config/confighttp v0.124.0 // indirect
92+
go.opentelemetry.io/collector/config/configmiddleware v0.0.0-00010101000000-000000000000 // indirect
9293
go.opentelemetry.io/collector/config/confignet v1.30.0 // indirect
9394
go.opentelemetry.io/collector/config/configopaque v1.30.0 // indirect
9495
go.opentelemetry.io/collector/config/configretry v1.30.0 // indirect
@@ -107,6 +108,7 @@ require (
107108
go.opentelemetry.io/collector/exporter/xexporter v0.124.0 // indirect
108109
go.opentelemetry.io/collector/extension/extensionauth v1.30.0 // indirect
109110
go.opentelemetry.io/collector/extension/extensioncapabilities v0.124.0 // indirect
111+
go.opentelemetry.io/collector/extension/extensionmiddleware v1.30.0 // indirect
110112
go.opentelemetry.io/collector/extension/extensiontest v0.124.0 // indirect
111113
go.opentelemetry.io/collector/extension/xextension v0.124.0 // indirect
112114
go.opentelemetry.io/collector/featuregate v1.30.0 // indirect
@@ -186,6 +188,8 @@ replace go.opentelemetry.io/collector/config/configgrpc => ../../config/configgr
186188

187189
replace go.opentelemetry.io/collector/config/confighttp => ../../config/confighttp
188190

191+
replace go.opentelemetry.io/collector/config/configmiddleware => ../../config/configmiddleware
192+
189193
replace go.opentelemetry.io/collector/config/confignet => ../../config/confignet
190194

191195
replace go.opentelemetry.io/collector/config/configopaque => ../../config/configopaque
@@ -252,6 +256,10 @@ replace go.opentelemetry.io/collector/extension/extensionauth/extensionauthtest
252256

253257
replace go.opentelemetry.io/collector/extension/extensioncapabilities => ../../extension/extensioncapabilities
254258

259+
replace go.opentelemetry.io/collector/extension/extensionmiddleware => ../../extension/extensionmiddleware
260+
261+
replace go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest => ../../extension/extensionmiddleware/extensionmiddlewaretest
262+
255263
replace go.opentelemetry.io/collector/extension/extensiontest => ../../extension/extensiontest
256264

257265
replace go.opentelemetry.io/collector/extension/memorylimiterextension => ../../extension/memorylimiterextension

config/confighttp/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ README](../configtls/README.md).
5858
- [`http2_ping_timeout`](https://pkg.go.dev/golang.org/x/net/http2#Transport)
5959
- [`cookies`](https://pkg.go.dev/net/http#CookieJar)
6060
- [`enabled`] if enabled, the client will store cookies from server responses and reuse them in subsequent requests.
61+
- [`middlewares`](../configmiddleware/README.md)
6162

6263
Example:
6364

@@ -107,6 +108,7 @@ will not be enabled.
107108
- [`tls`](../configtls/README.md)
108109
- [`auth`](../configauth/README.md)
109110
- `request_params`: a list of query parameter names to add to the auth context, along with the HTTP headers
111+
- [`middlewares`](../configmiddleware/README.md)
110112

111113
You can enable [`attribute processor`][attribute-processor] to append any http header to span's attribute using custom key. You also need to enable the "include_metadata"
112114

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package confighttp
5+
6+
import (
7+
"context"
8+
"errors"
9+
"io"
10+
"net/http"
11+
"net/http/httptest"
12+
"strings"
13+
"testing"
14+
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
18+
"go.opentelemetry.io/collector/component"
19+
"go.opentelemetry.io/collector/component/componenttest"
20+
"go.opentelemetry.io/collector/config/configmiddleware"
21+
"go.opentelemetry.io/collector/extension"
22+
"go.opentelemetry.io/collector/extension/extensionmiddleware"
23+
"go.opentelemetry.io/collector/extension/extensionmiddleware/extensionmiddlewaretest"
24+
)
25+
26+
// testClientMiddleware is a test middleware that appends a string to the response body
27+
type testClientMiddleware struct {
28+
extension.Extension
29+
extensionmiddleware.GetHTTPRoundTripperFunc
30+
}
31+
32+
func newTestClientMiddleware(name string) component.Component {
33+
return &testClientMiddleware{
34+
Extension: extensionmiddlewaretest.NewNop(),
35+
GetHTTPRoundTripperFunc: func(transport http.RoundTripper) (http.RoundTripper, error) {
36+
return extensionmiddlewaretest.HTTPClientFunc(
37+
func(req *http.Request) (*http.Response, error) {
38+
resp, err := transport.RoundTrip(req)
39+
if err != nil {
40+
return resp, err
41+
}
42+
43+
// Read the original body
44+
body, err := io.ReadAll(resp.Body)
45+
if err != nil {
46+
return resp, err
47+
}
48+
_ = resp.Body.Close()
49+
50+
// Create a new body with the appended text
51+
newBody := string(body) + "\r\noutput by " + name
52+
53+
// Replace the response body
54+
resp.Body = io.NopCloser(strings.NewReader(newBody))
55+
resp.ContentLength = int64(len(newBody))
56+
57+
return resp, nil
58+
}), nil
59+
},
60+
}
61+
}
62+
63+
func newTestClientConfig(name string) configmiddleware.Config {
64+
return configmiddleware.Config{
65+
ID: component.MustNewID(name),
66+
}
67+
}
68+
69+
func TestClientMiddlewares(t *testing.T) {
70+
// Create a test server that returns "OK"
71+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
72+
w.WriteHeader(http.StatusOK)
73+
_, _ = w.Write([]byte("OK"))
74+
}))
75+
defer server.Close()
76+
77+
// Register two test extensions
78+
host := &mockHost{
79+
ext: map[component.ID]component.Component{
80+
component.MustNewID("test1"): newTestClientMiddleware("test1"),
81+
component.MustNewID("test2"): newTestClientMiddleware("test2"),
82+
},
83+
}
84+
85+
// Test with different middleware configurations
86+
testCases := []struct {
87+
name string
88+
middlewares []configmiddleware.Config
89+
expectedOutput string
90+
}{
91+
{
92+
name: "no_middlewares",
93+
middlewares: nil,
94+
expectedOutput: "OK",
95+
},
96+
{
97+
name: "single_middleware",
98+
middlewares: []configmiddleware.Config{
99+
newTestClientConfig("test1"),
100+
},
101+
expectedOutput: "OK\r\noutput by test1",
102+
},
103+
{
104+
name: "multiple_middlewares",
105+
middlewares: []configmiddleware.Config{
106+
newTestClientConfig("test1"),
107+
newTestClientConfig("test2"),
108+
},
109+
expectedOutput: "OK\r\noutput by test2\r\noutput by test1",
110+
},
111+
}
112+
113+
for _, tc := range testCases {
114+
t.Run(tc.name, func(t *testing.T) {
115+
// Create HTTP client config with the test middlewares
116+
clientConfig := ClientConfig{
117+
Endpoint: server.URL,
118+
Middlewares: tc.middlewares,
119+
}
120+
121+
// Create the client
122+
client, err := clientConfig.ToClient(context.Background(), host, componenttest.NewNopTelemetrySettings())
123+
require.NoError(t, err)
124+
125+
// Create a request to the test server
126+
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
127+
require.NoError(t, err)
128+
129+
// Send the request
130+
resp, err := client.Do(req)
131+
require.NoError(t, err)
132+
defer resp.Body.Close()
133+
134+
// Check the response
135+
body, err := io.ReadAll(resp.Body)
136+
require.NoError(t, err)
137+
assert.Equal(t, tc.expectedOutput, string(body))
138+
})
139+
}
140+
}
141+
142+
func TestClientMiddlewareErrors(t *testing.T) {
143+
// Create a test server that returns "OK"
144+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
145+
w.WriteHeader(http.StatusOK)
146+
_, _ = w.Write([]byte("OK"))
147+
}))
148+
defer server.Close()
149+
150+
// Test cases for HTTP client middleware errors
151+
httpTests := []struct {
152+
name string
153+
host component.Host
154+
config ClientConfig
155+
errText string
156+
}{
157+
{
158+
name: "extension_not_found",
159+
host: &mockHost{
160+
ext: map[component.ID]component.Component{},
161+
},
162+
config: ClientConfig{
163+
Endpoint: server.URL,
164+
Middlewares: []configmiddleware.Config{
165+
{
166+
ID: component.MustNewID("nonexistent"),
167+
},
168+
},
169+
},
170+
errText: "failed to resolve middleware \"nonexistent\": middleware not found",
171+
},
172+
{
173+
name: "get_round_tripper_fails",
174+
host: &mockHost{
175+
ext: map[component.ID]component.Component{
176+
component.MustNewID("errormw"): extensionmiddlewaretest.NewErr(errors.New("http middleware error")),
177+
},
178+
},
179+
config: ClientConfig{
180+
Endpoint: server.URL,
181+
Middlewares: []configmiddleware.Config{
182+
{
183+
ID: component.MustNewID("errormw"),
184+
},
185+
},
186+
},
187+
errText: "http middleware error",
188+
},
189+
}
190+
191+
for _, tc := range httpTests {
192+
t.Run(tc.name, func(t *testing.T) {
193+
// Trying to create the client should fail
194+
_, err := tc.config.ToClient(context.Background(), tc.host, componenttest.NewNopTelemetrySettings())
195+
require.Error(t, err)
196+
assert.Contains(t, err.Error(), tc.errText)
197+
})
198+
}
199+
}
200+
201+
// Test failures for gRPC client middlewares by creating a mock implementation
202+
// that can fail in similar ways to HTTP clients
203+
func TestGRPCClientMiddlewareErrors(t *testing.T) {
204+
// Test cases for gRPC client middleware errors
205+
grpcTests := []struct {
206+
name string
207+
host component.Host
208+
config ClientConfig
209+
errText string
210+
}{
211+
{
212+
name: "grpc_extension_not_found",
213+
host: &mockHost{
214+
ext: map[component.ID]component.Component{},
215+
},
216+
config: ClientConfig{
217+
Endpoint: "localhost:1234",
218+
Middlewares: []configmiddleware.Config{
219+
{
220+
ID: component.MustNewID("nonexistent"),
221+
},
222+
},
223+
},
224+
errText: "failed to resolve middleware \"nonexistent\": middleware not found",
225+
},
226+
{
227+
name: "grpc_get_client_options_fails",
228+
host: &mockHost{
229+
ext: map[component.ID]component.Component{
230+
component.MustNewID("errormw"): extensionmiddlewaretest.NewErr(errors.New("grpc middleware error")),
231+
},
232+
},
233+
config: ClientConfig{
234+
Endpoint: "localhost:1234",
235+
Middlewares: []configmiddleware.Config{
236+
{
237+
ID: component.MustNewID("errormw"),
238+
},
239+
},
240+
},
241+
errText: "grpc middleware error",
242+
},
243+
}
244+
245+
for _, tc := range grpcTests {
246+
t.Run(tc.name, func(t *testing.T) {
247+
// For gRPC, we need to use the configgrpc.ClientConfig structure
248+
// We'll test the middleware failure path here using the HTTP client approach,
249+
// as the middleware resolution logic is the same
250+
_, err := tc.config.ToClient(context.Background(), tc.host, componenttest.NewNopTelemetrySettings())
251+
require.Error(t, err)
252+
assert.Contains(t, err.Error(), tc.errText)
253+
})
254+
}
255+
}

0 commit comments

Comments
 (0)