Skip to content

Commit 0e52ad6

Browse files
authored
s3store: Add option to log API calls (#1260)
1 parent e11e360 commit 0e52ad6

File tree

6 files changed

+536
-1
lines changed

6 files changed

+536
-1
lines changed

cmd/tusd/cli/composer.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"path/filepath"
88
"strings"
99

10+
"golang.org/x/exp/slog"
11+
12+
"github.com/tus/tusd/v2/internal/s3log"
1013
"github.com/tus/tusd/v2/pkg/azurestore"
1114
"github.com/tus/tusd/v2/pkg/filelocker"
1215
"github.com/tus/tusd/v2/pkg/filestore"
@@ -45,7 +48,8 @@ func CreateComposer() {
4548
printStartupLog("Using '%s/%s' as S3 endpoint and bucket for storage.\n", Flags.S3Endpoint, Flags.S3Bucket)
4649
}
4750

48-
s3Client := s3.NewFromConfig(s3Config, func(o *s3.Options) {
51+
var s3Client s3store.S3API
52+
s3Client = s3.NewFromConfig(s3Config, func(o *s3.Options) {
4953
o.UseAccelerate = Flags.S3TransferAcceleration
5054

5155
// Disable HTTPS and only use HTTP (helpful for debugging requests).
@@ -57,6 +61,13 @@ func CreateComposer() {
5761
}
5862
})
5963

64+
if Flags.S3LogAPICalls {
65+
if !Flags.VerboseOutput {
66+
stderr.Fatalf("The -s3-log-api-calls flag requires verbose mode (-verbose) to be enabled")
67+
}
68+
s3Client = s3log.New(s3Client, slog.Default())
69+
}
70+
6071
store := s3store.New(Flags.S3Bucket, s3Client)
6172
store.ObjectPrefix = Flags.S3ObjectPrefix
6273
store.PreferredPartSize = Flags.S3PartSize

cmd/tusd/cli/flags.go

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var Flags struct {
3939
S3DisableContentHashes bool
4040
S3DisableSSL bool
4141
S3ConcurrentPartUploads int
42+
S3LogAPICalls bool
4243
GCSBucket string
4344
GCSObjectPrefix string
4445
AzStorage string
@@ -138,6 +139,7 @@ func ParseFlags() {
138139
f.BoolVar(&Flags.S3DisableSSL, "s3-disable-ssl", false, "Disable SSL and only use HTTP for communication with S3 (experimental and may be removed in the future)")
139140
f.IntVar(&Flags.S3ConcurrentPartUploads, "s3-concurrent-part-uploads", 10, "Number of concurrent part uploads to S3 (experimental and may be removed in the future)")
140141
f.BoolVar(&Flags.S3TransferAcceleration, "s3-transfer-acceleration", false, "Use AWS S3 transfer acceleration endpoint (requires -s3-bucket option and Transfer Acceleration property on S3 bucket to be set)")
142+
f.BoolVar(&Flags.S3LogAPICalls, "s3-log-api-calls", false, "Log all S3 API calls for debugging purposes")
141143
})
142144

143145
fs.AddGroup("Google Cloud Storage options", func(f *flag.FlagSet) {

docs/_storage-backends/aws-s3.md

+10
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,13 @@ $ AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7M
117117
```
118118

119119
Tusd is then usable at `http://localhost:8080/files/` and saves the uploads to the local MinIO instance.
120+
121+
## Debugging
122+
123+
If you are experiencing problems with the S3 storage, such as files not appearing in the bucket or uploads not resuming, it's helpful to inspect the individual calls that are made to S3. This is in particular useful when issues appear with S3-compatible services other than AWS S3. Small implementation differences can cause problems with resuming uploads, which can be spotted by inspecting the calls. To log all calls, enable the `-s3-log-api-calls` flag:
124+
125+
```sh
126+
$ tusd -s3-bucket=my-test-bucket.com -s3-log-api-calls
127+
```
128+
129+
The output may contain sensitive information, such as bucket names, IAM users, and account names. The content of the uploaded files is not logged. If a tusd maintainer asks for the logs and you don't want to share the logs publicly, you can ask to send them via e-mail.

internal/s3log/s3log.go

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Package s3log provides a logging wrapper for the AWS S3 API.
2+
package s3log
3+
4+
import (
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"time"
9+
10+
"golang.org/x/exp/slog"
11+
12+
"github.com/aws/aws-sdk-go-v2/service/s3"
13+
"github.com/tus/tusd/v2/pkg/s3store"
14+
)
15+
16+
var _ s3store.S3API = &loggingS3API{}
17+
18+
type loggingS3API struct {
19+
// Wrapped is the underlying s3store.S3API implementation
20+
Wrapped s3store.S3API
21+
Logger *slog.Logger
22+
}
23+
24+
// New creates a wrapper around the provided S3 API that logs all calls to `logger`
25+
func New(wrapped s3store.S3API, logger *slog.Logger) s3store.S3API {
26+
return &loggingS3API{
27+
Wrapped: wrapped,
28+
Logger: logger,
29+
}
30+
}
31+
32+
// sanitizeForLogging creates a copy of the input with large values removed that
33+
// we don't want to print in the logs.
34+
func sanitizeForLogging(v interface{}) interface{} {
35+
switch input := v.(type) {
36+
case *s3.PutObjectInput:
37+
sanitized := *input
38+
sanitized.Body = nil
39+
return sanitized
40+
case *s3.UploadPartInput:
41+
sanitized := *input
42+
sanitized.Body = nil
43+
return sanitized
44+
case *s3.GetObjectOutput:
45+
sanitized := *input
46+
sanitized.Body = nil
47+
return sanitized
48+
default:
49+
return v
50+
}
51+
}
52+
53+
// jsonEncode converts a value to a JSON string, handling errors gracefully
54+
func jsonEncode(v interface{}) string {
55+
data, err := json.Marshal(v)
56+
if err != nil {
57+
return fmt.Sprintf("{\"error\":\"failed to marshal: %v\"}", err)
58+
}
59+
60+
return string(data)
61+
}
62+
63+
// logCall logs an API call with its input, output, and error
64+
func (l *loggingS3API) logCall(operation string, input, output interface{}, err error, duration time.Duration) {
65+
sanitizedInput := sanitizeForLogging(input)
66+
sanitizedOutput := sanitizeForLogging(output)
67+
68+
// Convert to JSON strings for structured logging
69+
inputJSON := jsonEncode(sanitizedInput)
70+
outputJSON := jsonEncode(sanitizedOutput)
71+
72+
attrs := []any{
73+
"operation", operation,
74+
"input", inputJSON,
75+
"duration_ms", duration.Milliseconds(),
76+
}
77+
78+
if err != nil {
79+
attrs = append(attrs, "error", err.Error())
80+
} else {
81+
attrs = append(attrs, "output", outputJSON)
82+
}
83+
84+
l.Logger.Debug("S3APICall", attrs...)
85+
}
86+
87+
// PutObject implements the s3store.S3API interface
88+
func (l *loggingS3API) PutObject(ctx context.Context, input *s3.PutObjectInput, opt ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
89+
start := time.Now()
90+
output, err := l.Wrapped.PutObject(ctx, input, opt...)
91+
l.logCall("PutObject", input, output, err, time.Since(start))
92+
return output, err
93+
}
94+
95+
// ListParts implements the s3store.S3API interface
96+
func (l *loggingS3API) ListParts(ctx context.Context, input *s3.ListPartsInput, opt ...func(*s3.Options)) (*s3.ListPartsOutput, error) {
97+
start := time.Now()
98+
output, err := l.Wrapped.ListParts(ctx, input, opt...)
99+
l.logCall("ListParts", input, output, err, time.Since(start))
100+
return output, err
101+
}
102+
103+
// UploadPart implements the s3store.S3API interface
104+
func (l *loggingS3API) UploadPart(ctx context.Context, input *s3.UploadPartInput, opt ...func(*s3.Options)) (*s3.UploadPartOutput, error) {
105+
start := time.Now()
106+
output, err := l.Wrapped.UploadPart(ctx, input, opt...)
107+
l.logCall("UploadPart", input, output, err, time.Since(start))
108+
return output, err
109+
}
110+
111+
// GetObject implements the s3store.S3API interface
112+
func (l *loggingS3API) GetObject(ctx context.Context, input *s3.GetObjectInput, opt ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
113+
start := time.Now()
114+
output, err := l.Wrapped.GetObject(ctx, input, opt...)
115+
l.logCall("GetObject", input, output, err, time.Since(start))
116+
return output, err
117+
}
118+
119+
// HeadObject implements the s3store.S3API interface
120+
func (l *loggingS3API) HeadObject(ctx context.Context, input *s3.HeadObjectInput, opt ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
121+
start := time.Now()
122+
output, err := l.Wrapped.HeadObject(ctx, input, opt...)
123+
l.logCall("HeadObject", input, output, err, time.Since(start))
124+
return output, err
125+
}
126+
127+
// CreateMultipartUpload implements the s3store.S3API interface
128+
func (l *loggingS3API) CreateMultipartUpload(ctx context.Context, input *s3.CreateMultipartUploadInput, opt ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) {
129+
start := time.Now()
130+
output, err := l.Wrapped.CreateMultipartUpload(ctx, input, opt...)
131+
l.logCall("CreateMultipartUpload", input, output, err, time.Since(start))
132+
return output, err
133+
}
134+
135+
// AbortMultipartUpload implements the s3store.S3API interface
136+
func (l *loggingS3API) AbortMultipartUpload(ctx context.Context, input *s3.AbortMultipartUploadInput, opt ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) {
137+
start := time.Now()
138+
output, err := l.Wrapped.AbortMultipartUpload(ctx, input, opt...)
139+
l.logCall("AbortMultipartUpload", input, output, err, time.Since(start))
140+
return output, err
141+
}
142+
143+
// DeleteObject implements the s3store.S3API interface
144+
func (l *loggingS3API) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput, opt ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
145+
start := time.Now()
146+
output, err := l.Wrapped.DeleteObject(ctx, input, opt...)
147+
l.logCall("DeleteObject", input, output, err, time.Since(start))
148+
return output, err
149+
}
150+
151+
// DeleteObjects implements the s3store.S3API interface
152+
func (l *loggingS3API) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput, opt ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) {
153+
start := time.Now()
154+
output, err := l.Wrapped.DeleteObjects(ctx, input, opt...)
155+
l.logCall("DeleteObjects", input, output, err, time.Since(start))
156+
return output, err
157+
}
158+
159+
// CompleteMultipartUpload implements the s3store.S3API interface
160+
func (l *loggingS3API) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteMultipartUploadInput, opt ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) {
161+
start := time.Now()
162+
output, err := l.Wrapped.CompleteMultipartUpload(ctx, input, opt...)
163+
l.logCall("CompleteMultipartUpload", input, output, err, time.Since(start))
164+
return output, err
165+
}
166+
167+
// UploadPartCopy implements the s3store.S3API interface
168+
func (l *loggingS3API) UploadPartCopy(ctx context.Context, input *s3.UploadPartCopyInput, opt ...func(*s3.Options)) (*s3.UploadPartCopyOutput, error) {
169+
start := time.Now()
170+
output, err := l.Wrapped.UploadPartCopy(ctx, input, opt...)
171+
l.logCall("UploadPartCopy", input, output, err, time.Since(start))
172+
return output, err
173+
}

0 commit comments

Comments
 (0)