Skip to content

Commit 426ae64

Browse files
author
Ivan De Marino
authored
Introduce NewLoggingHTTPTransport and deprecate NewTransport (#1006)
* .gitignore JetBrains IDE project files * Removing `fmtcheck` target in `Makefile`, and instead add `lint` to trigger GolangCI-Lint (what we use today) * Introducing `helper/logging` `NewLoggingHTTPTransport` This deprecated the `NewTransport` facility. * Set golangci-lint GH Action to always pick the latest version of golangci-lint executable * Added test coverage to confirm request-body is captured, as well as log masking and filtering * Adding CHANGELOG entry * Place the req/res body in a dedicated field, and keep the log message short (i.e. "Sending HTTP Request / Received HTTP Response") * Bumping dependency to [email protected] * Add a new field to every HTTP transaction: `tf_http_trans_id` This is a UUID, and it's specific to the pair of HTTP Req/Res that 1 HTTP Transaction executes
1 parent 06cd54f commit 426ae64

File tree

10 files changed

+608
-12
lines changed

10 files changed

+608
-12
lines changed

Diff for: .changelog/1006.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:feature
2+
helper/logging: New `NewLoggingHTTPTransport()` and `NewSubsystemLoggingHTTPTransport()` functions, providing `http.RoundTripper` Transport implementations that log request/response using [terraform-plugin-log](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-log)
3+
```
4+
5+
```release-note:note
6+
helper/logging: Existing `NewTransport()` is now deprecated in favour of using the new `NewLoggingHTTPTransport()` or `NewSubsystemLoggingHTTPTransport()`
7+
```

Diff for: .github/workflows/ci-go.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ jobs:
2121
with:
2222
go-version-file: 'go.mod'
2323
- run: go mod download
24-
- uses: golangci/[email protected]
24+
- uses: golangci/golangci-lint-action@v3
25+
with:
26+
version: latest
2527
terraform-provider-corner:
2628
defaults:
2729
run:

Diff for: .gitignore

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
node_modules
2-
website-preview
2+
website-preview
3+
4+
# Jetbrains IDEs
5+
.idea/
6+
*.iws

Diff for: Makefile

+6-6
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,17 @@ WEBSITE_DOCKER_RUN_FLAGS=--interactive \
2222

2323
default: test
2424

25-
test: fmtcheck generate
25+
test: generate
2626
go test ./...
2727

28+
lint:
29+
golangci-lint run
30+
2831
generate:
2932
go generate ./...
3033

3134
fmt:
32-
gofmt -w $(GOFMT_FILES)
33-
34-
fmtcheck:
35-
@sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"
35+
gofmt -s -w -e $(GOFMT_FILES)
3636

3737
# Run the terraform.io website to preview local content changes
3838
website:
@@ -55,4 +55,4 @@ website/build-local:
5555
@docker build https://github.com/hashicorp/terraform-website.git\#$(WEBSITE_BRANCH) \
5656
-t $(WEBSITE_DOCKER_IMAGE_LOCAL)
5757

58-
.PHONY: default fmt fmtcheck generate test website website/local website/build-local
58+
.PHONY: default fmt lint generate test website website/local website/build-local

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ require (
1919
github.com/hashicorp/terraform-exec v0.17.2
2020
github.com/hashicorp/terraform-json v0.14.0
2121
github.com/hashicorp/terraform-plugin-go v0.12.0
22-
github.com/hashicorp/terraform-plugin-log v0.6.0
22+
github.com/hashicorp/terraform-plugin-log v0.7.0
2323
github.com/mitchellh/copystructure v1.2.0
2424
github.com/mitchellh/go-testing-interface v1.14.1
2525
github.com/mitchellh/mapstructure v1.5.0

Diff for: go.sum

+2-1
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e
127127
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
128128
github.com/hashicorp/terraform-plugin-go v0.12.0 h1:6wW9mT1dSs0Xq4LR6HXj1heQ5ovr5GxXNJwkErZzpJw=
129129
github.com/hashicorp/terraform-plugin-go v0.12.0/go.mod h1:kwhmaWHNDvT1B3QiSJdAtrB/D4RaKSY/v3r2BuoWK4M=
130-
github.com/hashicorp/terraform-plugin-log v0.6.0 h1:/Vq78uSIdUSZ3iqDc9PESKtwt8YqNKN6u+khD+lLjuw=
131130
github.com/hashicorp/terraform-plugin-log v0.6.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4=
131+
github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs=
132+
github.com/hashicorp/terraform-plugin-log v0.7.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4=
132133
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c h1:D8aRO6+mTqHfLsK/BC3j5OAoogv1WLRWzY1AaTo3rBg=
133134
github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI=
134135
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=

Diff for: helper/logging/logging_http_transport.go

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package logging
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
"errors"
8+
"io"
9+
"net/http"
10+
"net/http/httputil"
11+
"net/textproto"
12+
"strings"
13+
14+
"github.com/hashicorp/go-uuid"
15+
"github.com/hashicorp/terraform-plugin-log/tflog"
16+
)
17+
18+
// NewLoggingHTTPTransport creates a wrapper around an *http.RoundTripper,
19+
// designed to be used for the `Transport` field of http.Client.
20+
//
21+
// This logs each pair of HTTP request/response that it handles.
22+
// The logging is done via `tflog`, that is part of the terraform-plugin-log
23+
// library, included by this SDK.
24+
//
25+
// The request/response is logged via tflog.Debug, using the context.Context
26+
// attached to the http.Request that the transport receives as input
27+
// of http.RoundTripper RoundTrip method.
28+
//
29+
// It's responsibility of the developer using this transport, to ensure that each
30+
// http.Request it handles is configured with the SDK-initialized Provider Root Logger
31+
// context.Context, that it's passed to all resources/data-sources/provider entry-points
32+
// (i.e. schema.Resource fields like `CreateContext`, `ReadContext`, etc.).
33+
//
34+
// This also gives the developer the flexibility to further configure the
35+
// logging behaviour via the above-mentioned context: please see
36+
// https://www.terraform.io/plugin/log/writing.
37+
func NewLoggingHTTPTransport(t http.RoundTripper) *loggingHttpTransport {
38+
return &loggingHttpTransport{"", t}
39+
}
40+
41+
// NewSubsystemLoggingHTTPTransport creates a wrapper around an *http.RoundTripper,
42+
// designed to be used for the `Transport` field of http.Client.
43+
//
44+
// This logs each pair of HTTP request/response that it handles.
45+
// The logging is done via `tflog`, that is part of the terraform-plugin-log
46+
// library, included by this SDK.
47+
//
48+
// The request/response is logged via tflog.SubsystemDebug, using the context.Context
49+
// attached to the http.Request that the transport receives as input
50+
// of http.RoundTripper RoundTrip method, as well as the `subsystem` string
51+
// provided at construction time.
52+
//
53+
// It's responsibility of the developer using this transport, to ensure that each
54+
// http.Request it handles is configured with a Subsystem Logger
55+
// context.Context that was initialized via tflog.NewSubsystem.
56+
//
57+
// This also gives the developer the flexibility to further configure the
58+
// logging behaviour via the above-mentioned context: please see
59+
// https://www.terraform.io/plugin/log/writing.
60+
//
61+
// Please note: setting `subsystem` to an empty string it's equivalent to
62+
// using NewLoggingHTTPTransport.
63+
func NewSubsystemLoggingHTTPTransport(subsystem string, t http.RoundTripper) *loggingHttpTransport {
64+
return &loggingHttpTransport{subsystem, t}
65+
}
66+
67+
const (
68+
// FieldHttpOperationType is the field key used by NewLoggingHTTPTransport
69+
// and NewSubsystemLoggingHTTPTransport when logging the type of HTTP operation via tflog.
70+
FieldHttpOperationType = "tf_http_op_type"
71+
72+
// OperationHttpRequest is the field value used by NewLoggingHTTPTransport
73+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP request via tflog.
74+
OperationHttpRequest = "request"
75+
76+
// OperationHttpResponse is the field value used by NewLoggingHTTPTransport
77+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP response via tflog.
78+
OperationHttpResponse = "response"
79+
80+
// FieldHttpRequestMethod is the field key used by NewLoggingHTTPTransport
81+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP request method via tflog.
82+
FieldHttpRequestMethod = "tf_http_req_method"
83+
84+
// FieldHttpRequestUri is the field key used by NewLoggingHTTPTransport
85+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP request URI via tflog.
86+
FieldHttpRequestUri = "tf_http_req_uri"
87+
88+
// FieldHttpRequestProtoVersion is the field key used by NewLoggingHTTPTransport
89+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP request HTTP version via tflog.
90+
FieldHttpRequestProtoVersion = "tf_http_req_version"
91+
92+
// FieldHttpRequestBody is the field key used by NewLoggingHTTPTransport
93+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP request body via tflog.
94+
FieldHttpRequestBody = "tf_http_req_body"
95+
96+
// FieldHttpResponseProtoVersion is the field key used by NewLoggingHTTPTransport
97+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP response protocol version via tflog.
98+
FieldHttpResponseProtoVersion = "tf_http_res_version"
99+
100+
// FieldHttpResponseStatusCode is the field key used by NewLoggingHTTPTransport
101+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP response status code via tflog.
102+
FieldHttpResponseStatusCode = "tf_http_res_status_code"
103+
104+
// FieldHttpResponseStatusReason is the field key used by NewLoggingHTTPTransport
105+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP response status reason phrase via tflog.
106+
FieldHttpResponseStatusReason = "tf_http_res_status_reason"
107+
108+
// FieldHttpResponseBody is the field key used by NewLoggingHTTPTransport
109+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP response body via tflog.
110+
FieldHttpResponseBody = "tf_http_res_body"
111+
112+
// FieldHttpTransactionId is the field key used by NewLoggingHTTPTransport
113+
// and NewSubsystemLoggingHTTPTransport when logging an HTTP transaction via tflog.
114+
FieldHttpTransactionId = "tf_http_trans_id"
115+
)
116+
117+
type loggingHttpTransport struct {
118+
subsystem string
119+
transport http.RoundTripper
120+
}
121+
122+
func (t *loggingHttpTransport) RoundTrip(req *http.Request) (*http.Response, error) {
123+
ctx := req.Context()
124+
ctx = t.AddTransactionIdField(ctx)
125+
126+
// Decompose the request bytes in a message (HTTP body) and fields (HTTP headers), then log it
127+
fields, err := decomposeRequestForLogging(req)
128+
if err != nil {
129+
t.Error(ctx, "Failed to parse request bytes for logging", map[string]interface{}{
130+
"error": err,
131+
})
132+
} else {
133+
t.Debug(ctx, "Sending HTTP Request", fields)
134+
}
135+
136+
// Invoke the wrapped RoundTrip now
137+
res, err := t.transport.RoundTrip(req)
138+
if err != nil {
139+
return res, err
140+
}
141+
142+
// Decompose the response bytes in a message (HTTP body) and fields (HTTP headers), then log it
143+
fields, err = decomposeResponseForLogging(res)
144+
if err != nil {
145+
t.Error(ctx, "Failed to parse response bytes for logging", map[string]interface{}{
146+
"error": err,
147+
})
148+
} else {
149+
t.Debug(ctx, "Received HTTP Response", fields)
150+
}
151+
152+
return res, nil
153+
}
154+
155+
func (t *loggingHttpTransport) Debug(ctx context.Context, msg string, fields ...map[string]interface{}) {
156+
if t.subsystem != "" {
157+
tflog.SubsystemDebug(ctx, t.subsystem, msg, fields...)
158+
} else {
159+
tflog.Debug(ctx, msg, fields...)
160+
}
161+
}
162+
163+
func (t *loggingHttpTransport) Error(ctx context.Context, msg string, fields ...map[string]interface{}) {
164+
if t.subsystem != "" {
165+
tflog.SubsystemError(ctx, t.subsystem, msg, fields...)
166+
} else {
167+
tflog.Error(ctx, msg, fields...)
168+
}
169+
}
170+
171+
func (t *loggingHttpTransport) AddTransactionIdField(ctx context.Context) context.Context {
172+
tId, err := uuid.GenerateUUID()
173+
174+
if err != nil {
175+
tId = "Unable to assign Transaction ID: " + err.Error()
176+
}
177+
178+
if t.subsystem != "" {
179+
return tflog.SubsystemSetField(ctx, t.subsystem, FieldHttpTransactionId, tId)
180+
} else {
181+
return tflog.SetField(ctx, FieldHttpTransactionId, tId)
182+
183+
}
184+
}
185+
186+
func decomposeRequestForLogging(req *http.Request) (map[string]interface{}, error) {
187+
fields := make(map[string]interface{}, len(req.Header)+4)
188+
fields[FieldHttpOperationType] = OperationHttpRequest
189+
190+
fields[FieldHttpRequestMethod] = req.Method
191+
fields[FieldHttpRequestUri] = req.URL.RequestURI()
192+
fields[FieldHttpRequestProtoVersion] = req.Proto
193+
194+
// Get the full body of the request, including headers appended by http.Transport:
195+
// this is necessary because the http.Request at this stage doesn't contain
196+
// all the headers that will be eventually sent.
197+
// We rely on `httputil.DumpRequestOut` to obtain the actual bytes that will be sent out.
198+
reqBytes, err := httputil.DumpRequestOut(req, true)
199+
if err != nil {
200+
return nil, err
201+
}
202+
203+
// Create a reader around the request full body
204+
reqReader := textproto.NewReader(bufio.NewReader(bytes.NewReader(reqBytes)))
205+
206+
err = fieldHeadersFromRequestReader(reqReader, fields)
207+
if err != nil {
208+
return nil, err
209+
}
210+
211+
// Read the rest of the body content
212+
fields[FieldHttpRequestBody] = bodyFromRestOfRequestReader(reqReader)
213+
return fields, nil
214+
}
215+
216+
func fieldHeadersFromRequestReader(reader *textproto.Reader, fields map[string]interface{}) error {
217+
// Ignore the first line: it contains non-header content
218+
// that we have already captured.
219+
// Skipping this step, would cause the following call to `ReadMIMEHeader()`
220+
// to fail as it cannot parse the first line.
221+
_, err := reader.ReadLine()
222+
if err != nil {
223+
return err
224+
}
225+
226+
// Read the MIME-style headers
227+
mimeHeader, err := reader.ReadMIMEHeader()
228+
if err != nil {
229+
return err
230+
}
231+
232+
// Set the headers as fields to log
233+
for k, v := range mimeHeader {
234+
if len(v) == 1 {
235+
fields[k] = v[0]
236+
} else {
237+
fields[k] = v
238+
}
239+
}
240+
241+
return nil
242+
}
243+
244+
func bodyFromRestOfRequestReader(reader *textproto.Reader) string {
245+
var builder strings.Builder
246+
for {
247+
line, err := reader.ReadContinuedLine()
248+
if errors.Is(err, io.EOF) {
249+
break
250+
}
251+
builder.WriteString(line)
252+
}
253+
254+
return builder.String()
255+
}
256+
257+
func decomposeResponseForLogging(res *http.Response) (map[string]interface{}, error) {
258+
fields := make(map[string]interface{}, len(res.Header)+4)
259+
fields[FieldHttpOperationType] = OperationHttpResponse
260+
261+
fields[FieldHttpResponseProtoVersion] = res.Proto
262+
fields[FieldHttpResponseStatusCode] = res.StatusCode
263+
fields[FieldHttpResponseStatusReason] = res.Status
264+
265+
// Set the headers as fields to log
266+
for k, v := range res.Header {
267+
if len(v) == 1 {
268+
fields[k] = v[0]
269+
} else {
270+
fields[k] = v
271+
}
272+
}
273+
274+
// Read the whole response body
275+
resBody, err := io.ReadAll(res.Body)
276+
if err != nil {
277+
return nil, err
278+
}
279+
280+
// Wrap the bytes from the response body, back into an io.ReadCloser,
281+
// to respect the interface of http.Response, as expected by users of the
282+
// http.Client
283+
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
284+
285+
fields[FieldHttpResponseBody] = string(resBody)
286+
287+
return fields, nil
288+
}

0 commit comments

Comments
 (0)