Skip to content

Commit 930a517

Browse files
authored
Implement protocol data logging (#135)
Reference: #107 Refactors `tf5server` and `tf6server` log handling into shared internal/logging package to prevent additional code duplication. To enable protocol data handling, utilizes the `TF_LOG_SDK_PROTO_DATA_DIR` environment variable to enable writing individual data field files to disk. In the future, this may need to be offered as a server configuration so it can work better in conjunction with testing frameworks (e.g. `t.TempDir()`), but there should not be too much preventing that as a later enhancement as necessary. Without `TF_LOG_SDK_PROTO_DATA_DIR` set (once): ```text 2022-01-06T16:04:54.787-0500 [TRACE] provider.terraform-provider-framework: Skipping protocol data file writing because no data directory is set. Use the TF_LOG_SDK_PROTO_DATA_DIR environment variable to enable this functionality.: tf_req_id=105b9e19-be94-411d-2f52-ca84c14f951e tf_resource_type=framework_optional_types tf_rpc=ValidateResourceConfig @caller=/Users/bflad/src/github.com/hashicorp/terraform-plugin-go/internal/logging/protocol.go:21 @module=sdk.proto EXTRA_VALUE_AT_END=<nil> tf_provider_addr=registry.terraform.io/bflad/framework tf_proto_version=6 timestamp=2022-01-06T16:04:54.786-0500 ``` With `TF_LOG_SDK_PROTO_DATA_DIR` set (per `DynamicValue` field): ```text 2022-01-06T16:06:36.079-0500 [TRACE] provider.terraform-provider-framework: Writing protocol data file: EXTRA_VALUE_AT_END=[tf_proto_data_file, 1641503196_ValidateResourceConfig_Request_Config.msgpack] tf_rpc=ValidateResourceConfig @module=sdk.proto tf_proto_version=6 tf_provider_addr=registry.terraform.io/bflad/framework tf_req_id=9084bc21-f6d1-bcc8-b916-fe75595375d2 tf_resource_type=framework_optional_types @caller=/Users/bflad/src/github.com/hashicorp/terraform-plugin-go/internal/logging/protocol.go:21 timestamp=2022-01-06T16:06:36.079-0500 ``` Viewing example MessagePack file contents: ```console $ go install github.com/nokute78/msgpack-microscope/cmd/msgpack2json@latest $ msgpack2json 1641503196_PlanResourceChange_Request_Config.msgpack | jq . { "format": "fixmap", "header": "0x85", "length": 5, "raw": "0x85a26964c0b36f7074696f6e616c5f74797065735f626f6f6cc0b66f7074696f6e616c5f74797065735f666c6f61743634c0b46f7074696f6e616c5f74797065735f696e743634c0b56f7074696f6e616c5f74797065735f737472696e67c0", "value": [ { "key": { "format": "fixstr", "header": "0xa2", "raw": "0xa26964", "value": "id" }, "value": { "format": "nil", "header": "0xc0", "raw": "0xc0", "value": null } }, { "key": { "format": "fixstr", "header": "0xb3", "raw": "0xb36f7074696f6e616c5f74797065735f626f6f6c", "value": "optional_types_bool" }, "value": { "format": "nil", "header": "0xc0", "raw": "0xc0", "value": null } }, { "key": { "format": "fixstr", "header": "0xb6", "raw": "0xb66f7074696f6e616c5f74797065735f666c6f61743634", "value": "optional_types_float64" }, "value": { "format": "nil", "header": "0xc0", "raw": "0xc0", "value": null } }, { "key": { "format": "fixstr", "header": "0xb4", "raw": "0xb46f7074696f6e616c5f74797065735f696e743634", "value": "optional_types_int64" }, "value": { "format": "nil", "header": "0xc0", "raw": "0xc0", "value": null } }, { "key": { "format": "fixstr", "header": "0xb5", "raw": "0xb56f7074696f6e616c5f74797065735f737472696e67", "value": "optional_types_string" }, "value": { "format": "nil", "header": "0xc0", "raw": "0xc0", "value": null } } ] } ```
1 parent 5ad95e8 commit 930a517

File tree

12 files changed

+730
-368
lines changed

12 files changed

+730
-368
lines changed

.changelog/135.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
tfprotov5/tf5server: Added support for writing protocol data to disk by setting `TF_LOG_SDK_PROTO_DATA_DIR` environment variable
3+
```
4+
5+
```release-note:enhancement
6+
tfprotov6/tf6server: Added support for writing protocol data to disk by setting `TF_LOG_SDK_PROTO_DATA_DIR` environment variable
7+
```

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ func main() {
119119
}
120120
```
121121

122+
### Protocol Data
123+
124+
To write raw protocol MessagePack or JSON data to disk, set the `TF_LOG_SDK_PROTO_DATA_DIR` environment variable. During Terraform execution, this directory will get populated with `{TIME}_{RPC}_{MESSAGE}_{FIELD}.{EXTENSION}` named files. Tooling such as [`jq`](https://stedolan.github.io/jq/) can be used to inspect the JSON data. Tooling such as [`fq`](https://github.com/wader/fq) or [`msgpack2json`](https://pkg.go.dev/github.com/nokute78/msgpack-microscope/cmd/msgpack2json) can be used to inspect the MessagePack data.
125+
122126
## Documentation
123127

124128
Documentation is a work in progress. The GoDoc for packages, types, functions,

internal/logging/context.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/go-uuid"
7+
"github.com/hashicorp/terraform-plugin-log/tflog"
8+
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
9+
)
10+
11+
// DataSourceContext injects the data source type into logger contexts.
12+
func DataSourceContext(ctx context.Context, dataSource string) context.Context {
13+
ctx = tfsdklog.With(ctx, KeyDataSourceType, dataSource)
14+
ctx = tfsdklog.SubsystemWith(ctx, SubsystemProto, KeyDataSourceType, dataSource)
15+
ctx = tflog.With(ctx, KeyDataSourceType, dataSource)
16+
17+
return ctx
18+
}
19+
20+
// InitContext creates SDK and provider logger contexts.
21+
func InitContext(ctx context.Context, sdkOpts tfsdklog.Options, providerOpts tflog.Options) context.Context {
22+
ctx = tfsdklog.NewRootSDKLogger(ctx, append(tfsdklog.Options{
23+
tfsdklog.WithLevelFromEnv(EnvTfLogSdk),
24+
}, sdkOpts...)...)
25+
ctx = tfsdklog.NewSubsystem(ctx, SubsystemProto, append(tfsdklog.Options{
26+
tfsdklog.WithLevelFromEnv(EnvTfLogSdkProto),
27+
}, sdkOpts...)...)
28+
ctx = tfsdklog.NewRootProviderLogger(ctx, providerOpts...)
29+
30+
return ctx
31+
}
32+
33+
// ProtocolVersionContext injects the protocol version into logger contexts.
34+
func ProtocolVersionContext(ctx context.Context, protocolVersion string) context.Context {
35+
ctx = tfsdklog.SubsystemWith(ctx, SubsystemProto, KeyProtocolVersion, protocolVersion)
36+
37+
return ctx
38+
}
39+
40+
// ProviderAddressContext injects the provider address into logger contexts.
41+
func ProviderAddressContext(ctx context.Context, providerAddress string) context.Context {
42+
ctx = tfsdklog.With(ctx, KeyProviderAddress, providerAddress)
43+
ctx = tfsdklog.SubsystemWith(ctx, SubsystemProto, KeyProviderAddress, providerAddress)
44+
ctx = tflog.With(ctx, KeyProviderAddress, providerAddress)
45+
46+
return ctx
47+
}
48+
49+
// RequestIdContext injects a unique request ID into logger contexts.
50+
func RequestIdContext(ctx context.Context) context.Context {
51+
reqID, err := uuid.GenerateUUID()
52+
53+
if err != nil {
54+
reqID = "unable to assign request ID: " + err.Error()
55+
}
56+
57+
ctx = tfsdklog.With(ctx, KeyRequestID, reqID)
58+
ctx = tfsdklog.SubsystemWith(ctx, SubsystemProto, KeyRequestID, reqID)
59+
ctx = tflog.With(ctx, KeyRequestID, reqID)
60+
61+
return ctx
62+
}
63+
64+
// ResourceContext injects the resource type into logger contexts.
65+
func ResourceContext(ctx context.Context, resource string) context.Context {
66+
ctx = tfsdklog.With(ctx, KeyResourceType, resource)
67+
ctx = tfsdklog.SubsystemWith(ctx, SubsystemProto, KeyResourceType, resource)
68+
ctx = tflog.With(ctx, KeyResourceType, resource)
69+
70+
return ctx
71+
}
72+
73+
// RpcContext injects the RPC name into logger contexts.
74+
func RpcContext(ctx context.Context, rpc string) context.Context {
75+
ctx = tfsdklog.With(ctx, KeyRPC, rpc)
76+
ctx = tfsdklog.SubsystemWith(ctx, SubsystemProto, KeyRPC, rpc)
77+
ctx = tflog.With(ctx, KeyRPC, rpc)
78+
79+
return ctx
80+
}

internal/logging/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package logging contains shared environment variable and log functionality.
2+
package logging
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package logging
2+
3+
// Environment variables.
4+
const (
5+
// EnvTfLogProvider is the prefix of the environment variable that sets the
6+
// logging level of the root provider logger for the provider being served.
7+
// The suffix is an underscore and the parsed provider name. For example,
8+
// registry.terraform.io/hashicorp/example becomes TF_LOG_PROVIDER_EXAMPLE.
9+
EnvTfLogProvider = "TF_LOG_PROVIDER"
10+
11+
// EnvTfLogSdk is an environment variable that sets the root logging level
12+
// of SDK loggers.
13+
EnvTfLogSdk = "TF_LOG_SDK"
14+
15+
// EnvTfLogSdkProto is an environment variable that sets the logging level
16+
// of SDK protocol loggers. Infers root SDK logging level, if unset.
17+
EnvTfLogSdkProto = "TF_LOG_SDK_PROTO"
18+
19+
// EnvTfLogSdkProtoDataDir is an environment variable that sets the
20+
// directory to write raw protocol data files for debugging purposes.
21+
EnvTfLogSdkProtoDataDir = "TF_LOG_SDK_PROTO_DATA_DIR"
22+
)

internal/logging/keys.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package logging
2+
3+
// Global logging keys attached to all requests.
4+
//
5+
// Practitioners or tooling reading logs may be depending on these keys, so be
6+
// conscious of that when changing them.
7+
const (
8+
// A unique ID for the RPC request
9+
KeyRequestID = "tf_req_id"
10+
11+
// The full address of the provider, such as
12+
// registry.terraform.io/hashicorp/random
13+
KeyProviderAddress = "tf_provider_addr"
14+
15+
// The RPC being run, such as "ApplyResourceChange"
16+
KeyRPC = "tf_rpc"
17+
18+
// The type of resource being operated on, such as "random_pet"
19+
KeyResourceType = "tf_resource_type"
20+
21+
// The type of data source being operated on, such as "archive_file"
22+
KeyDataSourceType = "tf_data_source_type"
23+
24+
// Path to protocol data file, such as "/tmp/example.json"
25+
KeyProtocolDataFile = "tf_proto_data_file"
26+
27+
// The protocol version being used, as a string, such as "6"
28+
KeyProtocolVersion = "tf_proto_version"
29+
)

internal/logging/protocol.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-log/tfsdklog"
7+
)
8+
9+
const (
10+
// SubsystemProto is the tfsdklog subsystem name for protocol logging.
11+
SubsystemProto = "proto"
12+
)
13+
14+
// ProtocolError emits a protocol subsystem log at ERROR level.
15+
func ProtocolError(ctx context.Context, msg string, args ...interface{}) {
16+
tfsdklog.SubsystemError(ctx, SubsystemProto, msg, args)
17+
}
18+
19+
// ProtocolTrace emits a protocol subsystem log at TRACE level.
20+
func ProtocolTrace(ctx context.Context, msg string, args ...interface{}) {
21+
tfsdklog.SubsystemTrace(ctx, SubsystemProto, msg, args)
22+
}

internal/logging/protocol_data.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package logging
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path"
8+
"sync"
9+
"time"
10+
11+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
12+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
13+
)
14+
15+
const (
16+
// fileExtEmpty is the file extension for empty data.
17+
// Empty data may be expected, depending on the RPC.
18+
fileExtEmpty = "empty"
19+
20+
// fileExtJson is the file extension for JSON data.
21+
fileExtJson = "json"
22+
23+
// fileExtMsgpack is the file extension for MessagePack data.
24+
fileExtMsgpack = "msgpack"
25+
)
26+
27+
var protocolDataSkippedLog sync.Once
28+
29+
// ProtocolData emits raw protocol data to a file, if given a directory.
30+
//
31+
// The directory must exist and be writable, prior to invoking this function.
32+
//
33+
// File names are in the format: {TIME}_{RPC}_{MESSAGE}_{FIELD}.{EXT}
34+
func ProtocolData(ctx context.Context, dataDir string, rpc string, message string, field string, data interface{}) {
35+
if dataDir == "" {
36+
// Write a log, only once, that explains how to enable this functionality.
37+
protocolDataSkippedLog.Do(func() {
38+
ProtocolTrace(ctx, "Skipping protocol data file writing because no data directory is set. "+
39+
fmt.Sprintf("Use the %s environment variable to enable this functionality.", EnvTfLogSdkProtoDataDir))
40+
})
41+
42+
return
43+
}
44+
45+
var fileContents []byte
46+
var fileExtension string
47+
48+
switch data := data.(type) {
49+
case *tfprotov5.DynamicValue:
50+
fileExtension, fileContents = protocolDataDynamicValue5(ctx, data)
51+
case *tfprotov6.DynamicValue:
52+
fileExtension, fileContents = protocolDataDynamicValue6(ctx, data)
53+
default:
54+
ProtocolError(ctx, fmt.Sprintf("Skipping unknown protocol data type: %T", data))
55+
return
56+
}
57+
58+
fileName := fmt.Sprintf("%d_%s_%s_%s.%s", time.Now().Unix(), rpc, message, field, fileExtension)
59+
filePath := path.Join(dataDir, fileName)
60+
61+
ProtocolTrace(ctx, "Writing protocol data file", KeyProtocolDataFile, filePath)
62+
63+
err := os.WriteFile(filePath, fileContents, 0644)
64+
65+
if err != nil {
66+
ProtocolError(ctx, fmt.Sprintf("Unable to write protocol data file: %s", err), KeyProtocolDataFile, filePath)
67+
return
68+
}
69+
70+
ProtocolTrace(ctx, "Wrote protocol data file", KeyProtocolDataFile, filePath)
71+
}
72+
73+
func protocolDataDynamicValue5(ctx context.Context, value *tfprotov5.DynamicValue) (string, []byte) {
74+
if value == nil {
75+
return fileExtEmpty, nil
76+
}
77+
78+
// (tfprotov5.DynamicValue).Unmarshal() prefers JSON first, so prefer to
79+
// output JSON if found.
80+
if len(value.JSON) > 0 {
81+
return fileExtJson, value.JSON
82+
}
83+
84+
if len(value.MsgPack) > 0 {
85+
return fileExtMsgpack, value.MsgPack
86+
}
87+
88+
return fileExtEmpty, nil
89+
}
90+
91+
func protocolDataDynamicValue6(ctx context.Context, value *tfprotov6.DynamicValue) (string, []byte) {
92+
if value == nil {
93+
return fileExtEmpty, nil
94+
}
95+
96+
// (tfprotov6.DynamicValue).Unmarshal() prefers JSON first, so prefer to
97+
// output JSON if found.
98+
if len(value.JSON) > 0 {
99+
return fileExtJson, value.JSON
100+
}
101+
102+
if len(value.MsgPack) > 0 {
103+
return fileExtMsgpack, value.MsgPack
104+
}
105+
106+
return fileExtEmpty, nil
107+
}

internal/logging/provider.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package logging
2+
3+
import (
4+
"log"
5+
"strings"
6+
7+
tfaddr "github.com/hashicorp/terraform-registry-address"
8+
)
9+
10+
func ProviderLoggerName(providerAddress string) string {
11+
provider, err := tfaddr.ParseRawProviderSourceString(providerAddress)
12+
13+
if err != nil {
14+
log.Printf("[ERROR] Error parsing provider name %q: %s", providerAddress, err)
15+
return ""
16+
}
17+
18+
return strings.ReplaceAll(provider.Type, "-", "_")
19+
}

internal/logging/provider_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package logging
2+
3+
import "testing"
4+
5+
func TestProviderLoggerName(t *testing.T) {
6+
t.Parallel()
7+
8+
testCases := map[string]struct {
9+
providerAddress string
10+
expected string
11+
}{
12+
"unparseable": {
13+
providerAddress: "example.com/test",
14+
expected: "",
15+
},
16+
"basic": {
17+
providerAddress: "example.com/user/test",
18+
expected: "test",
19+
},
20+
"hyphenated": {
21+
providerAddress: "example.com/user/test-test",
22+
expected: "test_test",
23+
},
24+
}
25+
26+
for name, testCase := range testCases {
27+
testCase := testCase
28+
29+
t.Run(name, func(t *testing.T) {
30+
t.Parallel()
31+
32+
got := ProviderLoggerName(testCase.providerAddress)
33+
34+
if testCase.expected != got {
35+
t.Errorf("wanted: %q, got: %q", testCase.expected, got)
36+
}
37+
})
38+
}
39+
}

0 commit comments

Comments
 (0)