├── aws-sdk-shim ├── go.mod ├── aws │ ├── session │ │ └── session.go │ └── ec2metadata │ │ └── service.go └── README.md ├── README.md ├── gmutex ├── nocopy.go ├── locker.go ├── README.md ├── init.go ├── backoff.go ├── json.go ├── gmutex_test.go └── gmutex.go ├── gtrace ├── README.md ├── gtace_test.go └── gtace.go ├── glog ├── README.md ├── std_test.go ├── std.go ├── glog_test.go ├── util.go ├── util_test.go └── glog.go ├── .gitignore ├── LICENSE ├── go.mod └── go.sum /aws-sdk-shim/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/aws/aws-sdk-go 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /aws-sdk-shim/aws/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | func NewSession() (*struct{}, error) { 4 | return nil, nil 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Google Cloud Platform](https://cloud.google.com/) utilities in Go 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-gcp) -------------------------------------------------------------------------------- /gmutex/nocopy.go: -------------------------------------------------------------------------------- 1 | package gmutex 2 | 3 | import "sync" 4 | 5 | type noCopy struct{} 6 | 7 | func (*noCopy) Lock() {} 8 | func (*noCopy) Unlock() {} 9 | 10 | var _ sync.Locker = (*noCopy)(nil) 11 | -------------------------------------------------------------------------------- /gtrace/README.md: -------------------------------------------------------------------------------- 1 | # [Google Cloud Trace](https://cloud.google.com/trace) in Go for Cloud Run and Cloud Functions 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-gcp/gtrace) -------------------------------------------------------------------------------- /aws-sdk-shim/README.md: -------------------------------------------------------------------------------- 1 | This is a shim to help ensure Google Cloud Trace libraries don't depend on AWS SDK. 2 | 3 | To use it, add the following to your `go.mod`: 4 | 5 | replace github.com/aws/aws-sdk-go => github.com/ncruces/go-gcp/aws-sdk-shim v1.0.0 -------------------------------------------------------------------------------- /glog/README.md: -------------------------------------------------------------------------------- 1 | # [Google Cloud Logging](https://cloud.google.com/logging) in Go for App Engine, Kubernetes Engine, Cloud Run, and Cloud Functions 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-gcp/glog) 4 | -------------------------------------------------------------------------------- /glog/std_test.go: -------------------------------------------------------------------------------- 1 | package glog_test 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/ncruces/go-gcp/glog" 7 | ) 8 | 9 | func ExampleSetupLogger() { 10 | glog.SetupLogger(log.Default()) 11 | log.Print("Test") 12 | // Output: 13 | // {"message":"Test"} 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /gmutex/locker.go: -------------------------------------------------------------------------------- 1 | package gmutex 2 | 3 | import "context" 4 | 5 | type locker struct { 6 | *Mutex 7 | } 8 | 9 | func (m locker) Lock() { 10 | if err := m.Mutex.LockData(context.Background(), nil); err != nil { 11 | panic(err) 12 | } 13 | } 14 | 15 | func (m locker) Unlock() { 16 | if err := m.Mutex.Unlock(context.Background()); err != nil { 17 | panic(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /glog/std.go: -------------------------------------------------------------------------------- 1 | package glog 2 | 3 | import "log" 4 | 5 | type stdLogger struct{} 6 | 7 | func (s stdLogger) Write(p []byte) (int, error) { 8 | logs(defaultsv, std, string(p)) 9 | return len(p), nil 10 | } 11 | 12 | // SetupLogger sets up a log.Logger to output structured logs at the default severity level. 13 | func SetupLogger(l *log.Logger) { 14 | l.SetFlags(0) 15 | l.SetOutput(stdLogger{}) 16 | } 17 | -------------------------------------------------------------------------------- /gtrace/gtace_test.go: -------------------------------------------------------------------------------- 1 | package gtrace_test 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/ncruces/go-gcp/glog" 8 | "github.com/ncruces/go-gcp/gtrace" 9 | ) 10 | 11 | func Example() { 12 | go gtrace.Init() 13 | glog.Notice("Starting server...") 14 | 15 | port := os.Getenv("PORT") 16 | if port == "" { 17 | port = "8080" 18 | } 19 | 20 | http.HandleFunc("/", http.NotFound) 21 | 22 | glog.Critical(http.ListenAndServe(":"+port, gtrace.NewHTTPHandler())) 23 | } 24 | -------------------------------------------------------------------------------- /aws-sdk-shim/aws/ec2metadata/service.go: -------------------------------------------------------------------------------- 1 | package ec2metadata 2 | 3 | type EC2Metadata struct{} 4 | 5 | func New(p *struct{}) *EC2Metadata { 6 | return nil 7 | } 8 | 9 | func (c *EC2Metadata) Available() bool { 10 | return false 11 | } 12 | 13 | func (c *EC2Metadata) GetInstanceIdentityDocument() (d EC2InstanceIdentityDocument, err error) { 14 | return 15 | } 16 | 17 | type EC2InstanceIdentityDocument struct { 18 | Region string 19 | InstanceID string 20 | AccountID string 21 | } 22 | -------------------------------------------------------------------------------- /gmutex/README.md: -------------------------------------------------------------------------------- 1 | # A global mutex using [Google Cloud Storage](https://cloud.google.com/storage) 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/image)](https://pkg.go.dev/github.com/ncruces/go-gcp/gmutex) 4 | 5 | Based on _[“A robust distributed locking algorithm based on Google Cloud Storage”][1]_ and 6 | [github.com/marcacohen/gcslock](https://github.com/marcacohen/gcslock). 7 | 8 | [1]: https://www.joyfulbikeshedding.com/blog/2021-05-19-robust-distributed-locking-algorithm-based-on-google-cloud-storage.html -------------------------------------------------------------------------------- /gmutex/init.go: -------------------------------------------------------------------------------- 1 | // Package gmutex implements a global mutex using Google Cloud Storage. 2 | package gmutex 3 | 4 | import ( 5 | "context" 6 | "net/http" 7 | "sync" 8 | 9 | "golang.org/x/oauth2/google" 10 | ) 11 | 12 | // HTTPClient should be set to an http.Client before first use. 13 | // If unset google.DefaultClient will be used. 14 | var HTTPClient *http.Client 15 | 16 | var initMtx sync.Mutex 17 | 18 | func initClient(ctx context.Context) (err error) { 19 | initMtx.Lock() 20 | defer initMtx.Unlock() 21 | if HTTPClient == nil { 22 | const scope = "https://www.googleapis.com/auth/devstorage.read_write" 23 | HTTPClient, err = google.DefaultClient(ctx, scope) 24 | } 25 | return err 26 | } 27 | -------------------------------------------------------------------------------- /glog/glog_test.go: -------------------------------------------------------------------------------- 1 | package glog_test 2 | 3 | import "github.com/ncruces/go-gcp/glog" 4 | 5 | func init() { 6 | glog.LogSourceLocation = false 7 | } 8 | 9 | func ExamplePrint() { 10 | glog.Print("Test") 11 | // Output: 12 | // {"message":"Test"} 13 | } 14 | 15 | func ExampleInfof() { 16 | glog.Infof("Hello %q!", "Google") 17 | // Output: 18 | // {"message":"Hello \"Google\"!","severity":"INFO"} 19 | } 20 | 21 | func ExampleWarningj() { 22 | glog.Warningj("Warning", map[string]string{ 23 | "component": "app", 24 | }) 25 | // Output: 26 | // {"component":"app","message":"Warning","severity":"WARNING"} 27 | } 28 | 29 | func ExampleWarningw() { 30 | glog.Warningw("Warning", 31 | "component", "app") 32 | // Output: 33 | // {"component":"app","message":"Warning","severity":"WARNING"} 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nuno Cruces 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gmutex/backoff.go: -------------------------------------------------------------------------------- 1 | package gmutex 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // Full Jitter from: 10 | // https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ 11 | 12 | const ( 13 | backOffMin = 50 * time.Millisecond 14 | backOffMax = 30 * time.Second 15 | ) 16 | 17 | type expBackOff struct { 18 | time time.Duration 19 | } 20 | 21 | type linBackOff struct { 22 | time time.Duration 23 | } 24 | 25 | func (b *linBackOff) wait(ctx context.Context) error { 26 | b.time += backOffMin 27 | if b.time < backOffMin { 28 | b.time = backOffMin 29 | } 30 | if b.time > backOffMax { 31 | b.time = backOffMax 32 | } 33 | return wait(ctx, time.Duration(rand.Int63n(int64(b.time)))) 34 | } 35 | 36 | func (b *expBackOff) wait(ctx context.Context) error { 37 | b.time += b.time / 2 38 | if b.time < backOffMin { 39 | b.time = backOffMin 40 | } 41 | if b.time > backOffMax { 42 | b.time = backOffMax 43 | } 44 | return wait(ctx, time.Duration(rand.Int63n(int64(b.time)))) 45 | } 46 | 47 | func wait(ctx context.Context, delay time.Duration) error { 48 | timer := time.NewTimer(delay) 49 | select { 50 | case <-timer.C: 51 | return nil 52 | case <-ctx.Done(): 53 | timer.Stop() 54 | return ctx.Err() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /glog/util.go: -------------------------------------------------------------------------------- 1 | package glog 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | 9 | "go.opencensus.io/trace" 10 | ) 11 | 12 | type any = interface{} 13 | 14 | func location(skip int) *sourceLocation { 15 | if !LogSourceLocation { 16 | return nil 17 | } 18 | if pc, file, line, ok := runtime.Caller(skip); ok { 19 | loc := &sourceLocation{ 20 | File: file, 21 | Line: strconv.Itoa(line), 22 | } 23 | if f := runtime.FuncForPC(pc); f != nil { 24 | loc.Function = f.Name() 25 | } 26 | return loc 27 | } 28 | return nil 29 | } 30 | 31 | func fromSpanContext(spanContext trace.SpanContext) (trace, spanID string) { 32 | if ProjectID == "" { 33 | return 34 | } 35 | 36 | trace = fmt.Sprintf("projects/%s/traces/%s", ProjectID, spanContext.TraceID) 37 | spanID = spanContext.SpanID.String() 38 | return 39 | } 40 | 41 | func parseTraceContext(traceContext string) (trace, spanID string) { 42 | if traceContext == "" || ProjectID == "" { 43 | return 44 | } 45 | 46 | t, rest, ok := cut(traceContext, "/") 47 | if !ok { 48 | return 49 | } 50 | trace = fmt.Sprintf("projects/%s/traces/%s", ProjectID, t) 51 | 52 | s, _, ok := cut(rest, ";") 53 | if !ok { 54 | return 55 | } 56 | if s, _ := strconv.ParseUint(s, 10, 64); s > 0 { 57 | spanID = fmt.Sprintf("%016x", s) 58 | } 59 | 60 | return 61 | } 62 | 63 | // TODO: replace with strings.Cut. 64 | func cut(s, sep string) (before, after string, found bool) { 65 | if i := strings.Index(s, sep); i >= 0 { 66 | return s[:i], s[i+len(sep):], true 67 | } 68 | return s, "", false 69 | } 70 | -------------------------------------------------------------------------------- /glog/util_test.go: -------------------------------------------------------------------------------- 1 | package glog 2 | 3 | import ( 4 | "testing" 5 | 6 | "go.opencensus.io/trace" 7 | ) 8 | 9 | func Test_fromSpanContext(t *testing.T) { 10 | ProjectID = "my-projectid" 11 | 12 | tests := []struct { 13 | name string 14 | span trace.SpanContext 15 | trace string 16 | spanID string 17 | }{ 18 | { 19 | "span", 20 | trace.SpanContext{ 21 | TraceID: [16]byte{0x01}, 22 | SpanID: [8]byte{0x02}, 23 | }, 24 | "projects/my-projectid/traces/01000000000000000000000000000000", 25 | "0200000000000000", 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | trace, spanID := fromSpanContext(tt.span) 31 | if trace != tt.trace { 32 | t.Errorf("fromSpanContext() trace = %q, want %q", trace, tt.trace) 33 | } 34 | if spanID != tt.spanID { 35 | t.Errorf("fromSpanContext() spanID = %q, want %q", spanID, tt.spanID) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func Test_parseTraceContext(t *testing.T) { 42 | ProjectID = "my-projectid" 43 | 44 | tests := []struct { 45 | name string 46 | header string 47 | trace string 48 | spanID string 49 | }{ 50 | {"no header", "", "", ""}, 51 | {"no span", "06796866738c859f2f19b7cfb3214824/0;o=1", "projects/my-projectid/traces/06796866738c859f2f19b7cfb3214824", ""}, 52 | {"hex span", "06796866738c859f2f19b7cfb3214824/74;o=1", "projects/my-projectid/traces/06796866738c859f2f19b7cfb3214824", "000000000000004a"}, 53 | {"with span", "06796866738c859f2f19b7cfb3214824/1;o=1", "projects/my-projectid/traces/06796866738c859f2f19b7cfb3214824", "0000000000000001"}, 54 | } 55 | for _, tt := range tests { 56 | t.Run(tt.name, func(t *testing.T) { 57 | trace, spanID := parseTraceContext(tt.header) 58 | if trace != tt.trace { 59 | t.Errorf("parseTraceContext() trace = %q, want %q", trace, tt.trace) 60 | } 61 | if spanID != tt.spanID { 62 | t.Errorf("parseTraceContext() spanID = %q, want %q", spanID, tt.spanID) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gmutex/json.go: -------------------------------------------------------------------------------- 1 | package gmutex 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "reflect" 8 | ) 9 | 10 | type any = interface{} 11 | 12 | // LockJSON calls LockData with the JSON encoding of v. 13 | func (m *Mutex) LockJSON(ctx context.Context, v any) error { 14 | b, err := json.Marshal(v) 15 | if err != nil { 16 | return err 17 | } 18 | return m.LockData(ctx, bytes.NewReader(b)) 19 | } 20 | 21 | // TryLockJSON calls TryLockData with the JSON encoding of v. 22 | // Parses JSON-encoded data into the value pointed to by v, 23 | // if the lock is already in use and v is a pointer. 24 | func (m *Mutex) TryLockJSON(ctx context.Context, v any) (bool, error) { 25 | b, err := json.Marshal(v) 26 | if err != nil { 27 | return false, err 28 | } 29 | 30 | if rv := reflect.ValueOf(v); rv.Kind() != reflect.Ptr || rv.IsNil() { 31 | return m.TryLockData(ctx, bytes.NewReader(b)) 32 | } 33 | 34 | buf := bytes.NewBuffer(b) 35 | locked, err := m.TryLockData(ctx, buf) 36 | if locked || err != nil { 37 | return locked, err 38 | } 39 | return false, json.Unmarshal(buf.Bytes(), v) 40 | } 41 | 42 | // UpdateJSON calls UpdateData with the JSON encoding of v. 43 | func (m *Mutex) UpdateJSON(ctx context.Context, v any) error { 44 | b, err := json.Marshal(v) 45 | if err != nil { 46 | return err 47 | } 48 | return m.UpdateData(ctx, bytes.NewReader(b)) 49 | } 50 | 51 | // AdoptJSON calls AdoptData with the JSON encoding of v. 52 | func (m *Mutex) AdoptJSON(ctx context.Context, id string, v any) error { 53 | b, err := json.Marshal(v) 54 | if err != nil { 55 | return err 56 | } 57 | return m.AdoptData(ctx, id, bytes.NewReader(b)) 58 | } 59 | 60 | // InspectJSON calls InspectData. 61 | // Parses JSON-encoded data into the value pointed to by v. 62 | func (m *Mutex) InspectJSON(ctx context.Context, v any) (bool, error) { 63 | var buf bytes.Buffer 64 | locked, err := m.InspectData(ctx, &buf) 65 | if err == nil { 66 | err = json.Unmarshal(buf.Bytes(), v) 67 | } 68 | return locked, err 69 | } 70 | -------------------------------------------------------------------------------- /gtrace/gtace.go: -------------------------------------------------------------------------------- 1 | // Package gtrace implements tracing for Google Cloud Run and Cloud Functions. 2 | package gtrace 3 | 4 | import ( 5 | "encoding/json" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | "sync" 10 | 11 | "contrib.go.opencensus.io/exporter/stackdriver" 12 | "contrib.go.opencensus.io/exporter/stackdriver/propagation" 13 | "go.opencensus.io/plugin/ochttp" 14 | "go.opencensus.io/trace" 15 | ) 16 | 17 | var once sync.Once 18 | 19 | // ProjectID should be set to the Google Cloud project ID. 20 | var ProjectID string = os.Getenv("GOOGLE_CLOUD_PROJECT") 21 | 22 | // Init initializes Cloud Trace. 23 | // Can be called multiple times. 24 | // Logs the error if called asynchronously. 25 | func Init() (err error) { 26 | callers := runtime.Callers(3, make([]uintptr, 1)) 27 | 28 | once.Do(func() { 29 | exporter, ierr := stackdriver.NewExporter(stackdriver.Options{ 30 | ProjectID: ProjectID, 31 | }) 32 | if ierr == nil { 33 | trace.RegisterExporter(exporter) 34 | return 35 | } 36 | if callers == 0 { 37 | json.NewEncoder(os.Stderr).Encode(map[string]string{ 38 | "message": ierr.Error(), 39 | "severity": "CRITICAL", 40 | }) 41 | } 42 | err = ierr 43 | }) 44 | 45 | return 46 | } 47 | 48 | // HTTPFormat implements propagation.HTTPFormat to propagate traces in 49 | // HTTP headers for Cloud Trace. 50 | type HTTPFormat struct { 51 | propagation.HTTPFormat 52 | } 53 | 54 | // NewHTTPClient returns a tracing http.Client. 55 | func NewHTTPClient() *http.Client { 56 | return &http.Client{ 57 | Transport: &ochttp.Transport{ 58 | // Use Google Cloud propagation format. 59 | Propagation: &propagation.HTTPFormat{}, 60 | }, 61 | } 62 | } 63 | 64 | // NewHTTPTransport returns a tracing http.RoundTripper. 65 | func NewHTTPTransport() http.RoundTripper { 66 | return &ochttp.Transport{ 67 | // Use Google Cloud propagation format. 68 | Propagation: &propagation.HTTPFormat{}, 69 | } 70 | } 71 | 72 | // NewHTTPHandler returns a tracing http.Handler. 73 | func NewHTTPHandler() http.Handler { 74 | return &ochttp.Handler{ 75 | // Use the Google Cloud propagation format. 76 | Propagation: &propagation.HTTPFormat{}, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ncruces/go-gcp 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | cloud.google.com/go/functions v1.19.3 7 | contrib.go.opencensus.io/exporter/stackdriver v0.13.14 8 | go.opencensus.io v0.24.0 9 | golang.org/x/oauth2 v0.28.0 10 | ) 11 | 12 | require ( 13 | cloud.google.com/go/auth v0.15.0 // indirect 14 | cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect 15 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 16 | cloud.google.com/go/monitoring v1.24.0 // indirect 17 | cloud.google.com/go/trace v1.11.4 // indirect 18 | github.com/aws/aws-sdk-go v1.55.6 // indirect 19 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 20 | github.com/felixge/httpsnoop v1.0.4 // indirect 21 | github.com/go-logr/logr v1.4.2 // indirect 22 | github.com/go-logr/stdr v1.2.2 // indirect 23 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 24 | github.com/golang/protobuf v1.5.4 // indirect 25 | github.com/google/s2a-go v0.1.9 // indirect 26 | github.com/googleapis/enterprise-certificate-proxy v0.3.5 // indirect 27 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 28 | github.com/prometheus/prometheus v0.302.1 // indirect 29 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 30 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 31 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 32 | go.opentelemetry.io/otel v1.35.0 // indirect 33 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 34 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 35 | golang.org/x/crypto v0.45.0 // indirect 36 | golang.org/x/net v0.47.0 // indirect 37 | golang.org/x/sync v0.18.0 // indirect 38 | golang.org/x/sys v0.38.0 // indirect 39 | golang.org/x/text v0.31.0 // indirect 40 | golang.org/x/time v0.11.0 // indirect 41 | google.golang.org/api v0.225.0 // indirect 42 | google.golang.org/genproto v0.0.0-20250311190419-81fb87f6b8bf // indirect 43 | google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf // indirect 44 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect 45 | google.golang.org/grpc v1.71.0 // indirect 46 | google.golang.org/protobuf v1.36.5 // indirect 47 | ) 48 | 49 | replace github.com/aws/aws-sdk-go => github.com/ncruces/go-gcp/aws-sdk-shim v1.0.0 50 | -------------------------------------------------------------------------------- /gmutex/gmutex_test.go: -------------------------------------------------------------------------------- 1 | package gmutex_test 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | "os" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/ncruces/go-gcp/gmutex" 13 | ) 14 | 15 | var bucket = os.Getenv("BUCKET") 16 | var object = os.Getenv("OBJECT") 17 | 18 | func TestMain(m *testing.M) { 19 | gmutex.HTTPClient = http.DefaultClient 20 | if os.Getenv("STORAGE_EMULATOR_HOST") != "" { 21 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 22 | } 23 | if bucket != "" && object != "" { 24 | os.Exit(m.Run()) 25 | } 26 | } 27 | 28 | func TestMutex_contention(t *testing.T) { 29 | ctx := context.Background() 30 | 31 | var failed bool 32 | var running bool 33 | var wg sync.WaitGroup 34 | for i := 0; i < 8; i++ { 35 | wg.Add(1) 36 | go func(i int) { 37 | defer wg.Done() 38 | 39 | mtx, err := gmutex.New(ctx, bucket, object, 5*time.Minute) 40 | if err != nil { 41 | t.Error(err) 42 | return 43 | } 44 | 45 | t.Log("locking", i) 46 | if err := mtx.Lock(ctx); err != nil { 47 | t.Error(err) 48 | return 49 | } 50 | t.Log("locked", i) 51 | 52 | if running || failed { 53 | failed = true 54 | } else { 55 | running = true 56 | time.Sleep(time.Second) 57 | running = false 58 | } 59 | 60 | t.Log("unlocking", i) 61 | if err := mtx.Unlock(ctx); err != nil { 62 | t.Error(err) 63 | return 64 | } 65 | t.Log("unlocked", i) 66 | }(i) 67 | } 68 | wg.Wait() 69 | 70 | if failed { 71 | t.Fail() 72 | } 73 | } 74 | 75 | func TestMutex_expiration(t *testing.T) { 76 | ctx := context.Background() 77 | mtx, err := gmutex.New(ctx, bucket, object, 5*time.Second) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | t.Log("locking") 83 | if err := mtx.Lock(ctx); err != nil { 84 | t.Fatal(err) 85 | } 86 | t.Log("locked") 87 | mtx.Abandon() 88 | t.Log("abandoned") 89 | 90 | t.Log("locking") 91 | if err := mtx.Lock(ctx); err != nil { 92 | t.Fatal(err) 93 | } 94 | t.Log("locked") 95 | 96 | t.Log("unlocking") 97 | if err := mtx.Unlock(ctx); err != nil { 98 | t.Fatal(err) 99 | } 100 | t.Log("unlocked") 101 | } 102 | 103 | func TestMutex_extension(t *testing.T) { 104 | ctx := context.Background() 105 | mtx, err := gmutex.New(ctx, bucket, object, 5*time.Second) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | t.Log("locking") 111 | if err := mtx.Lock(ctx); err != nil { 112 | t.Fatal(err) 113 | } 114 | t.Log("locked") 115 | 116 | for i := 0; i < 5; i++ { 117 | time.Sleep(time.Second) 118 | 119 | t.Log("extending") 120 | if err := mtx.Extend(ctx); err != nil { 121 | t.Fatal(err) 122 | } 123 | t.Log("extended") 124 | } 125 | 126 | mtx.Abandon() 127 | t.Log("abandoned") 128 | 129 | t.Log("locking") 130 | if err := mtx.Lock(ctx); err != nil { 131 | t.Fatal(err) 132 | } 133 | t.Log("locked") 134 | 135 | t.Log("unlocking") 136 | if err := mtx.Unlock(ctx); err != nil { 137 | t.Fatal(err) 138 | } 139 | t.Log("unlocked") 140 | } 141 | 142 | func TestMutex_SetTTL(t *testing.T) { 143 | tests := []struct { 144 | name string 145 | ttl time.Duration 146 | want time.Duration 147 | }{ 148 | {"zero", 0, 0}, 149 | {"zero", -1, 0}, 150 | {"zero", +1, time.Second}, 151 | {"nano", time.Nanosecond, time.Second}, 152 | {"micro", time.Microsecond, time.Second}, 153 | {"milli", time.Millisecond, time.Second}, 154 | {"one", time.Second, time.Second}, 155 | {"one", time.Second - 1, time.Second}, 156 | {"one", time.Second + 1, 2 * time.Second}, 157 | {"negative", -time.Hour, 0}, 158 | } 159 | 160 | ctx := context.Background() 161 | mtx, err := gmutex.New(ctx, bucket, object, 0) 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | 166 | for _, tt := range tests { 167 | t.Run(tt.name, func(t *testing.T) { 168 | mtx.SetTTL(tt.ttl) 169 | got := mtx.TTL() 170 | if got != tt.want { 171 | t.Errorf("New() = %v, want %v", got, tt.want) 172 | } 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= 3 | cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= 4 | cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= 6 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 7 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 8 | cloud.google.com/go/functions v1.19.3 h1:V0vCHSgFTUqKn57+PUXp1UfQY0/aMkveAw7wXeM3Lq0= 9 | cloud.google.com/go/functions v1.19.3/go.mod h1:nOZ34tGWMmwfiSJjoH/16+Ko5106x+1Iji29wzrBeOo= 10 | cloud.google.com/go/monitoring v1.24.0 h1:csSKiCJ+WVRgNkRzzz3BPoGjFhjPY23ZTcaenToJxMM= 11 | cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc= 12 | cloud.google.com/go/trace v1.11.4 h1:LKlhVyX6I4+heP31sWvERSKZZ9cPPEZumt7b4SKVK18= 13 | cloud.google.com/go/trace v1.11.4/go.mod h1:lCSHzSPZC1TPwto7zhaRt3KtGYsXFyaErPQ18AUUeUE= 14 | contrib.go.opencensus.io/exporter/stackdriver v0.13.14 h1:zBakwHardp9Jcb8sQHcHpXy/0+JIb1M8KjigCJzx7+4= 15 | contrib.go.opencensus.io/exporter/stackdriver v0.13.14/go.mod h1:5pSSGY0Bhuk7waTHuDf4aQ8D2DrhgETRo9fy6k3Xlzc= 16 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 17 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 18 | github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= 19 | github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= 20 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 21 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 22 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 27 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 28 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 29 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 30 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 31 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 32 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 33 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 34 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 35 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 36 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 37 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 38 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 39 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 40 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 41 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 42 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 44 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 45 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 46 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 47 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 48 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 49 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 50 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 51 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 52 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 53 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 54 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 55 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 56 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 58 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 59 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 60 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 61 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 62 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 63 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 64 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 65 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 66 | github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= 67 | github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 68 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 69 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 70 | github.com/ncruces/go-gcp/aws-sdk-shim v1.0.0 h1:m9BoEV5nwCwNjW3XRiwhjm3eW3OjjByvPo1q6iBQKPw= 71 | github.com/ncruces/go-gcp/aws-sdk-shim v1.0.0/go.mod h1:9ZVk9C7xhk4yVR4LFoWOIw8WivQR44oc8NpL3aUOabk= 72 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 73 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 74 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 76 | github.com/prometheus/prometheus v0.302.1 h1:xqVdrwrB4WNpdgJqxsz5loqFWNUZitsK8myqLuSZ6Ag= 77 | github.com/prometheus/prometheus v0.302.1/go.mod h1:YcyCoTbUR/TM8rY3Aoeqr0AWTu/pu1Ehh+trpX3eRzg= 78 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 79 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 80 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 81 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 83 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 84 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 85 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 86 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 87 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 88 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 89 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 90 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= 91 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= 92 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 93 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 94 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 95 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 96 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 97 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 98 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 99 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 100 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 101 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 102 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 103 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 104 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 105 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 106 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 107 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 108 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 109 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 110 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 111 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 112 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 113 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 114 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 115 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 116 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 117 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 118 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 119 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 120 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 121 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 122 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 123 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 124 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 125 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 126 | golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 127 | golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 128 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 129 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 130 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 131 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 133 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 134 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 135 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 136 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 137 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 138 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 139 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 140 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 141 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 142 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 143 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 144 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 145 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | google.golang.org/api v0.225.0 h1:+4/IVqBQm0MV5S+JW3kdEGC1WtOmM2mXN1LKH1LdNlw= 147 | google.golang.org/api v0.225.0/go.mod h1:WP/0Xm4LVvMOCldfvOISnWquSRWbG2kArDZcg+W2DbY= 148 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 149 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 150 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 151 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 152 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 153 | google.golang.org/genproto v0.0.0-20250311190419-81fb87f6b8bf h1:114fkUG+I9ba4UmaoNZt0UtiRmBng3KJIB/E0avfYII= 154 | google.golang.org/genproto v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= 155 | google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf h1:BdIVRm+fyDUn8lrZLPSlBCfM/YKDwUBYgDoLv9+DYo0= 156 | google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= 157 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY= 158 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 159 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 160 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 161 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 162 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 163 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 164 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 165 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 166 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 167 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 168 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 169 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 170 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 171 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 172 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 173 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 174 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 175 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 176 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 177 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 180 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 182 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 183 | -------------------------------------------------------------------------------- /gmutex/gmutex.go: -------------------------------------------------------------------------------- 1 | package gmutex 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/xml" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strconv" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | // A Mutex is a global, mutual exclusion lock 20 | // that uses an object in Google Cloud Storage 21 | // to serialize computations across the internet. 22 | // 23 | // A Mutex can optionally have data attached to it while it is held. 24 | // While there is no limit to the size of this data, 25 | // it is best kept small. 26 | // Provided data must be of type *bytes.Buffer, *bytes.Reader, 27 | // or *strings.Reader. 28 | // 29 | // Given the latency and scalability properties of Google Cloud Storage, 30 | // a Mutex is best used to serialize long-running, high-latency 31 | // compute processes. 32 | // Critical sections should span seconds. 33 | // Expect an uncontended mutex to take tens to hundreds of milliseconds 34 | // to acquire, and a contended one multiple seconds after release. 35 | // 36 | // An instance of Mutex is not associated with a particular goroutine 37 | // (it is allowed for one goroutine to lock a Mutex 38 | // and then arrange for another goroutine to unlock it), 39 | // but it is not safe for concurrent use by multiple goroutines. 40 | // 41 | // To use an API-compatible alternative to Google Cloud Storage 42 | // (such as fake-gcs-server or similar), provide the endpoint 43 | // by setting the environment variable STORAGE_EMULATOR_HOST 44 | // prior to creating the Mutex. 45 | type Mutex struct { 46 | _ noCopy 47 | bucket string 48 | object string 49 | generation string 50 | ttl int64 51 | baseUrl *url.URL 52 | } 53 | 54 | // New creates a new Mutex at the given bucket and object, 55 | // with the given time-to-live. 56 | func New(ctx context.Context, bucket, object string, ttl time.Duration) (*Mutex, error) { 57 | if err := initClient(ctx); err != nil { 58 | return nil, err 59 | } 60 | 61 | var baseUrl *url.URL 62 | if host := os.Getenv("STORAGE_EMULATOR_HOST"); host == "" { 63 | baseUrl = &url.URL{Scheme: "https", Host: "storage.googleapis.com"} 64 | } else if strings.Contains(host, "://") { 65 | h, err := url.Parse(host) 66 | if err != nil { 67 | return nil, err 68 | } 69 | baseUrl = h 70 | } else { 71 | baseUrl = &url.URL{Scheme: "http", Host: host} 72 | } 73 | 74 | m := Mutex{ 75 | bucket: bucket, 76 | object: object, 77 | baseUrl: baseUrl, 78 | } 79 | m.SetTTL(ttl) 80 | return &m, nil 81 | } 82 | 83 | // TTL gets the time-to-live to use when the mutex is 84 | // locked, extended, or updated. 85 | func (m *Mutex) TTL() time.Duration { 86 | return time.Duration(m.ttl) * time.Second 87 | } 88 | 89 | // SetTTL sets the time-to-live to use when the mutex is 90 | // locked, extended, or updated. 91 | // The time-to-live is rounded up to the nearest second. 92 | // Negative or zero time-to-live means the lock never expires. 93 | func (m *Mutex) SetTTL(ttl time.Duration) { 94 | ttl += time.Second - time.Nanosecond 95 | if ttl > 0 { 96 | m.ttl = int64(ttl / time.Second) 97 | } else { 98 | m.ttl = 0 99 | } 100 | } 101 | 102 | // Locker gets a Locker that uses context.Background to call Lock and Unlock, 103 | // and panics on error. 104 | func (m *Mutex) Locker() sync.Locker { 105 | return locker{m} 106 | } 107 | 108 | // Lock locks m. 109 | // If the lock is already in use, 110 | // the calling goroutine blocks until the mutex is available, 111 | // or the context expires. 112 | // Returns nil if the lock was taken successfully. 113 | func (m *Mutex) Lock(ctx context.Context) error { 114 | return m.LockData(ctx, nil) 115 | } 116 | 117 | // LockData locks m with attached data. 118 | // If the lock is already in use, 119 | // the calling goroutine blocks until the mutex is available, 120 | // or the context expires. 121 | // Returns nil if the lock was taken successfully 122 | // (and the attached data stored). 123 | func (m *Mutex) LockData(ctx context.Context, data io.Reader) error { 124 | if m.generation != "" { 125 | panic("gmutex: lock of locked mutex") 126 | } 127 | if !rewindable(data) { 128 | panic("gmutex: data not rewindable") 129 | } 130 | 131 | generation := "" // Initially, we expect the lock not to exist. 132 | var backoff expBackOff // Exponential backoff because we don't hold the lock. 133 | 134 | for { 135 | // Create the lock object, at the expected generation. 136 | status, gen, err := m.createObject(ctx, generation, data) 137 | if status == http.StatusOK { 138 | // Acquired. 139 | m.generation = gen 140 | return nil 141 | } 142 | if status == http.StatusNotFound { 143 | return errors.New("lock mutex: bucket does not exist") 144 | } 145 | 146 | if status == http.StatusPreconditionFailed { 147 | // The lock object exists at another generation, inspect it. 148 | status, gen, err = m.inspectObject(ctx, nil) 149 | } 150 | // While the lock object exists, and for transient errors, backoff and retry. 151 | for status == http.StatusOK || retriable(status, err) { 152 | if err := backoff.wait(ctx); err != nil { 153 | return err 154 | } 155 | status, gen, err = m.inspectObject(ctx, nil) 156 | } 157 | if status == http.StatusNotFound { 158 | // The lock object no longer exists, or has expired, acquire it. 159 | generation = gen 160 | continue 161 | } 162 | 163 | // Can't recover, give up. 164 | if err != nil { 165 | return fmt.Errorf("lock mutex: %w", err) 166 | } 167 | return fmt.Errorf("lock mutex: http status %d: %s", status, http.StatusText(status)) 168 | } 169 | } 170 | 171 | // TryLock tries to lock m. 172 | // Returns true if the lock was taken successfully, 173 | // false if the lock is already in use. 174 | func (m *Mutex) TryLock(ctx context.Context) (bool, error) { 175 | return m.TryLockData(ctx, nil) 176 | } 177 | 178 | // TryLockData tries to lock m with attached data. 179 | // Returns true if the lock was taken successfully 180 | // (and the attached data stored). 181 | // Returns false if the lock is already in use, 182 | // fetching attached data if data satisfies io.Writer. 183 | func (m *Mutex) TryLockData(ctx context.Context, data io.Reader) (bool, error) { 184 | if m.generation != "" { 185 | panic("gmutex: lock of locked mutex") 186 | } 187 | if !rewindable(data) { 188 | panic("gmutex: data not rewindable") 189 | } 190 | 191 | buffer, _ := data.(io.Writer) 192 | var backoff expBackOff // Exponential backoff because we don't hold the lock. 193 | 194 | for { 195 | // Inspect the lock object. 196 | status, gen, err := m.inspectObject(ctx, buffer) 197 | if status == http.StatusOK { 198 | return false, nil 199 | } 200 | 201 | if status == http.StatusNotFound { 202 | // The lock object doesn't exist, or has expired, acquire it. 203 | status, gen, err = m.createObject(ctx, gen, data) 204 | if status == http.StatusOK { 205 | // Acquired. 206 | m.generation = gen 207 | return true, nil 208 | } 209 | if status == http.StatusNotFound { 210 | return false, errors.New("lock mutex: bucket does not exist") 211 | } 212 | if status == http.StatusPreconditionFailed { 213 | // The lock object was recreated at another generation, inspect it. 214 | continue 215 | } 216 | } 217 | 218 | // For transient errors, backoff and retry. 219 | if retriable(status, err) { 220 | if err := backoff.wait(ctx); err != nil { 221 | return false, err 222 | } 223 | continue 224 | } 225 | 226 | // Can't recover, give up. 227 | if err != nil { 228 | return false, fmt.Errorf("lock mutex: %w", err) 229 | } 230 | return false, fmt.Errorf("lock mutex: http status %d: %s", status, http.StatusText(status)) 231 | } 232 | } 233 | 234 | // Unlock unlocks m, deleting any attached data. 235 | // Returns an error if the lock had already expired, 236 | // and mutual exclusion was not ensured. 237 | func (m *Mutex) Unlock(ctx context.Context) error { 238 | if m.generation == "" { 239 | panic("gmutex: unlock of unlocked mutex") 240 | } 241 | 242 | var backoff linBackOff // Linear backoff because we hold the lock. 243 | 244 | for { 245 | // Delete the lock object, at the expected generation. 246 | status, err := m.deleteObject(ctx, m.generation) 247 | if status == http.StatusOK || status == http.StatusNoContent { 248 | m.generation = "" 249 | return nil 250 | } 251 | 252 | if status == http.StatusPreconditionFailed || status == http.StatusNotFound { 253 | // The lock object exists at another generation, or no longer exists, it's stale. 254 | return errors.New("unlock mutex: stale lock") 255 | } 256 | 257 | // For transient errors, backoff and retry. 258 | if retriable(status, err) { 259 | if err := backoff.wait(ctx); err != nil { 260 | return err 261 | } 262 | continue 263 | } 264 | 265 | // Can't recover, give up. 266 | if err != nil { 267 | return fmt.Errorf("unlock mutex: %w", err) 268 | } 269 | return fmt.Errorf("unlock mutex: http status %d: %s", status, http.StatusText(status)) 270 | } 271 | } 272 | 273 | // Extend extends the expiration time of m, keeping any attached data. 274 | // Returns an error if the lock has already expired, 275 | // and mutual exclusion can not be ensured. 276 | func (m *Mutex) Extend(ctx context.Context) error { 277 | if m.generation == "" { 278 | panic("gmutex: extend of unlocked mutex") 279 | } 280 | 281 | var backoff linBackOff // Linear backoff because we hold the lock. 282 | 283 | for { 284 | // Extend the lock object, at the expected generation. 285 | status, gen, err := m.extendObject(ctx, m.generation) 286 | if status == http.StatusOK { 287 | // Extended. 288 | m.generation = gen 289 | return nil 290 | } 291 | if status == http.StatusNotFound { 292 | return errors.New("mutex: bucket does not exist") 293 | } 294 | 295 | if status == http.StatusPreconditionFailed || status == http.StatusNotFound { 296 | // The lock object exists at another generation, or no longer exists, it's stale. 297 | return errors.New("extend mutex: stale lock, abort") 298 | } 299 | 300 | // For transient errors, backoff and retry. 301 | if retriable(status, err) { 302 | if err := backoff.wait(ctx); err != nil { 303 | return err 304 | } 305 | continue 306 | } 307 | 308 | // Can't recover, give up. 309 | if err != nil { 310 | return fmt.Errorf("extend mutex: %w", err) 311 | } 312 | return fmt.Errorf("extend mutex: http status %d: %s", status, http.StatusText(status)) 313 | } 314 | } 315 | 316 | // UpdateData updates attached data, extending the expiration time of m. 317 | // Returns an error if the lock has already expired, 318 | // and mutual exclusion can not be ensured. 319 | func (m *Mutex) UpdateData(ctx context.Context, data io.Reader) error { 320 | if m.generation == "" { 321 | panic("gmutex: update of unlocked mutex") 322 | } 323 | if !rewindable(data) { 324 | panic("gmutex: data not rewindable") 325 | } 326 | 327 | var backoff linBackOff // Linear backoff because we hold the lock. 328 | 329 | for { 330 | // Update the lock object, at the expected generation. 331 | status, gen, err := m.createObject(ctx, m.generation, data) 332 | if status == http.StatusOK { 333 | // Updated. 334 | m.generation = gen 335 | return nil 336 | } 337 | if status == http.StatusNotFound { 338 | return errors.New("lock mutex: bucket does not exist") 339 | } 340 | 341 | if status == http.StatusPreconditionFailed || status == http.StatusNotFound { 342 | // The lock object exists at another generation, or no longer exists, it's stale. 343 | return errors.New("update mutex: stale lock, abort") 344 | } 345 | 346 | // For transient errors, backoff and retry. 347 | if retriable(status, err) { 348 | if err := backoff.wait(ctx); err != nil { 349 | return err 350 | } 351 | continue 352 | } 353 | 354 | // Can't recover, give up. 355 | if err != nil { 356 | return fmt.Errorf("update mutex: %w", err) 357 | } 358 | return fmt.Errorf("update mutex: http status %d: %s", status, http.StatusText(status)) 359 | } 360 | } 361 | 362 | // InspectData inspects m, returning its locked state and fetching attached data. 363 | func (m *Mutex) InspectData(ctx context.Context, data io.Writer) (bool, error) { 364 | var backoff expBackOff // Exponential backoff because we don't hold the lock. 365 | 366 | for { 367 | // Inspect the lock object. 368 | status, _, err := m.inspectObject(ctx, data) 369 | if status == http.StatusOK { 370 | return true, nil 371 | } 372 | if status == http.StatusNotFound { 373 | return false, nil 374 | } 375 | 376 | // For transient errors, backoff and retry. 377 | if retriable(status, err) { 378 | if err := backoff.wait(ctx); err != nil { 379 | return false, err 380 | } 381 | continue 382 | } 383 | 384 | // Can't recover, give up. 385 | if err != nil { 386 | return false, fmt.Errorf("inspect mutex: %w", err) 387 | } 388 | return false, fmt.Errorf("inspect mutex: http status %d: %s", status, http.StatusText(status)) 389 | } 390 | } 391 | 392 | // Abandon abandons m, returning a lock id that can be used to call Adopt. 393 | func (m *Mutex) Abandon() string { 394 | if m.generation == "" { 395 | panic("gmutex: abandon of unlocked mutex") 396 | } 397 | 398 | gen := m.generation 399 | m.generation = "" 400 | return gen 401 | } 402 | 403 | // Adopt adopts an abandoned lock into m, 404 | // and calls Extend to ensure mutual exclusion. 405 | func (m *Mutex) Adopt(ctx context.Context, id string) error { 406 | if m.generation != "" { 407 | panic("gmutex: adopt on locked mutex") 408 | } 409 | if id == "" || id == "0" { 410 | panic("gmutex: adopt of invalid lock") 411 | } 412 | 413 | m.generation = id 414 | return m.Extend(ctx) 415 | } 416 | 417 | // AdoptData adopts an abandoned lock into m, 418 | // and calls UpdateData to ensure mutual exclusion. 419 | func (m *Mutex) AdoptData(ctx context.Context, id string, data io.Reader) error { 420 | if m.generation != "" { 421 | panic("gmutex: adopt on locked mutex") 422 | } 423 | if id == "" || id == "0" { 424 | panic("gmutex: adopt of invalid lock") 425 | } 426 | 427 | m.generation = id 428 | return m.UpdateData(ctx, data) 429 | } 430 | 431 | func (m *Mutex) createObject(ctx context.Context, generation string, data io.Reader) (int, string, error) { 432 | if generation == "" { 433 | generation = "0" 434 | } 435 | 436 | // Create/update the lock object if the generation matches. 437 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, m.url(), data) 438 | if err != nil { 439 | panic(err) 440 | } 441 | req.Header.Set("Cache-Control", "no-store") 442 | req.Header.Set("x-goog-if-generation-match", generation) 443 | req.Header.Set("x-goog-meta-ttl", strconv.FormatInt(m.ttl, 10)) 444 | 445 | res, err := HTTPClient.Do(req) 446 | if err != nil { 447 | return 0, "", err 448 | } 449 | res.Body.Close() 450 | return res.StatusCode, res.Header.Get("x-goog-generation"), nil 451 | } 452 | 453 | func (m *Mutex) extendObject(ctx context.Context, generation string) (int, string, error) { 454 | // Copy object doesn't update the generation, only the metageneration. 455 | // Compose allows us to update the generation in a single request. 456 | var buf bytes.Buffer 457 | buf.WriteString("") 458 | xml.EscapeText(&buf, []byte(m.object)) 459 | buf.WriteString("") 460 | 461 | // Extend the lock object if the generation matches. 462 | req, err := http.NewRequestWithContext(ctx, http.MethodPut, m.url()+"?compose", &buf) 463 | if err != nil { 464 | panic(err) 465 | } 466 | req.Header.Set("Cache-Control", "no-store") 467 | req.Header.Set("x-goog-if-generation-match", generation) 468 | req.Header.Set("x-goog-meta-ttl", strconv.FormatInt(m.ttl, 10)) 469 | 470 | res, err := HTTPClient.Do(req) 471 | if err != nil { 472 | return 0, "", err 473 | } 474 | res.Body.Close() 475 | return res.StatusCode, res.Header.Get("x-goog-generation"), nil 476 | } 477 | 478 | func (m *Mutex) deleteObject(ctx context.Context, generation string) (int, error) { 479 | // Delete the lock object if the generation matches. 480 | req, err := http.NewRequestWithContext(ctx, http.MethodDelete, m.url(), nil) 481 | if err != nil { 482 | panic(err) 483 | } 484 | req.Header.Set("x-goog-if-generation-match", generation) 485 | 486 | res, err := HTTPClient.Do(req) 487 | if err != nil { 488 | return 0, err 489 | } 490 | res.Body.Close() 491 | return res.StatusCode, nil 492 | } 493 | 494 | func (m *Mutex) inspectObject(ctx context.Context, data io.Writer) (int, string, error) { 495 | var method string 496 | if data == nil { 497 | method = http.MethodHead 498 | } 499 | 500 | // Get the lock object's status. 501 | req, err := http.NewRequestWithContext(ctx, method, m.url(), nil) 502 | if err != nil { 503 | panic(err) 504 | } 505 | req.Header.Set("Cache-Control", "no-cache") 506 | 507 | res, err := HTTPClient.Do(req) 508 | if err != nil { 509 | return 0, "", err 510 | } 511 | defer res.Body.Close() 512 | 513 | // If it exists, but is expired, act as if it didn't. 514 | if res.StatusCode == http.StatusOK && expired(res) { 515 | res.StatusCode = http.StatusNotFound 516 | } 517 | if res.StatusCode == http.StatusOK && data != nil { 518 | switch b := data.(type) { 519 | case *strings.Builder: 520 | b.Reset() 521 | case *bytes.Buffer: 522 | b.Reset() 523 | } 524 | _, err = io.Copy(data, res.Body) 525 | } 526 | return res.StatusCode, res.Header.Get("x-goog-generation"), err 527 | } 528 | 529 | func (m *Mutex) url() string { 530 | url := url.URL{ 531 | Scheme: m.baseUrl.Scheme, 532 | Host: m.baseUrl.Host, 533 | Path: m.bucket + "/" + m.object, 534 | } 535 | return url.String() 536 | } 537 | 538 | func retriable(status int, err error) bool { 539 | // Retry on temporary errors and timeouts. 540 | if err != nil { 541 | uerr := url.Error{Err: err} 542 | return uerr.Temporary() || uerr.Timeout() 543 | } 544 | return status == http.StatusTooManyRequests || 545 | status == http.StatusRequestTimeout || 546 | status == http.StatusInternalServerError || 547 | status == http.StatusServiceUnavailable || 548 | status == http.StatusBadGateway || 549 | status == http.StatusGatewayTimeout 550 | } 551 | 552 | func rewindable(body io.Reader) bool { 553 | switch body.(type) { 554 | case nil, *bytes.Buffer, *bytes.Reader, *strings.Reader: 555 | return true 556 | default: 557 | return body == http.NoBody 558 | } 559 | } 560 | 561 | func expired(res *http.Response) bool { 562 | // Check for expiration using server date. 563 | now, err := http.ParseTime(res.Header.Get("Date")) 564 | if err != nil { 565 | return false 566 | } 567 | modified, err := http.ParseTime(res.Header.Get("Last-Modified")) 568 | if err != nil { 569 | return false 570 | } 571 | lifecycle, err := http.ParseTime(res.Header.Get("x-goog-expiration")) 572 | if err == nil && lifecycle.Before(now) { 573 | return true 574 | } 575 | ttl, err := strconv.ParseInt(res.Header.Get("x-goog-meta-ttl"), 10, 64) 576 | if err != nil || ttl <= 0 { 577 | return false 578 | } 579 | expires := modified.Add(time.Duration(ttl) * time.Second) 580 | return expires.Before(now) 581 | } 582 | -------------------------------------------------------------------------------- /glog/glog.go: -------------------------------------------------------------------------------- 1 | // Package glog implements leveled, structured logging for Google App Engine, 2 | // Kubernetes Engine, Cloud Run, and Cloud Functions. 3 | // 4 | // Structured logs are written to stdout or stderr as JSON objects 5 | // serialized on a single line. 6 | // The Logging agent then sends the structured logs to Cloud Logging 7 | // as the jsonPayload of the LogEntry structure. 8 | package glog 9 | 10 | import ( 11 | "context" 12 | "encoding/json" 13 | "fmt" 14 | "net/http" 15 | "os" 16 | "strings" 17 | 18 | "cloud.google.com/go/functions/metadata" 19 | "go.opencensus.io/trace" 20 | ) 21 | 22 | var std Logger = Logger{callers: 1} 23 | 24 | // ProjectID should be set to the Google Cloud project ID. 25 | var ProjectID string = os.Getenv("GOOGLE_CLOUD_PROJECT") 26 | 27 | // LogSourceLocation should be set to false to avoid associating 28 | // source code location information with the entry. 29 | var LogSourceLocation bool = true 30 | 31 | // Print logs an entry with no assigned severity level. 32 | // Arguments are handled in the manner of fmt.Print. 33 | func Print(v ...any) { 34 | std.Print(v...) 35 | } 36 | 37 | // Println logs an entry with no assigned severity level. 38 | // Arguments are handled in the manner of fmt.Println. 39 | func Println(v ...any) { 40 | std.Println(v...) 41 | } 42 | 43 | // Printf logs an entry with no assigned severity level. 44 | // Arguments are handled in the manner of fmt.Printf. 45 | func Printf(format string, v ...any) { 46 | std.Printf(format, v...) 47 | } 48 | 49 | // Printj logs an entry with no assigned severity level. 50 | // Arguments populate jsonPayload in the log entry. 51 | func Printj(msg string, v any) { 52 | std.Printj(msg, v) 53 | } 54 | 55 | // Printw logs an entry with no assigned severity level. 56 | // Arguments populate jsonPayload in the log entry. 57 | func Printw(msg string, kvs ...any) { 58 | std.Printw(msg, kvs...) 59 | } 60 | 61 | // Debug logs debug or trace information. 62 | // Arguments are handled in the manner of fmt.Print. 63 | func Debug(v ...any) { 64 | std.Debug(v...) 65 | } 66 | 67 | // Debugln logs debug or trace information. 68 | // Arguments are handled in the manner of fmt.Println. 69 | func Debugln(v ...any) { 70 | std.Debugln(v...) 71 | } 72 | 73 | // Debugf logs debug or trace information. 74 | // Arguments are handled in the manner of fmt.Printf. 75 | func Debugf(format string, v ...any) { 76 | std.Debugf(format, v...) 77 | } 78 | 79 | // Debugj logs debug or trace information. 80 | // Arguments populate jsonPayload in the log entry. 81 | func Debugj(msg string, v any) { 82 | std.Debugj(msg, v) 83 | } 84 | 85 | // Debugw logs debug or trace information. 86 | // Arguments populate jsonPayload in the log entry. 87 | func Debugw(msg string, kvs ...any) { 88 | std.Debugw(msg, kvs...) 89 | } 90 | 91 | // Info logs routine information, such as ongoing status or performance. 92 | // Arguments are handled in the manner of fmt.Print. 93 | func Info(v ...any) { 94 | std.Info(v...) 95 | } 96 | 97 | // Infoln logs routine information, such as ongoing status or performance. 98 | // Arguments are handled in the manner of fmt.Println. 99 | func Infoln(v ...any) { 100 | std.Infoln(v...) 101 | } 102 | 103 | // Infof logs routine information, such as ongoing status or performance. 104 | // Arguments are handled in the manner of fmt.Printf. 105 | func Infof(format string, v ...any) { 106 | std.Infof(format, v...) 107 | } 108 | 109 | // Infoj logs routine information, such as ongoing status or performance. 110 | // Arguments populate jsonPayload in the log entry. 111 | func Infoj(msg string, v any) { 112 | std.Infoj(msg, v) 113 | } 114 | 115 | // Infow logs routine information, such as ongoing status or performance. 116 | // Arguments populate jsonPayload in the log entry. 117 | func Infow(msg string, kvs ...any) { 118 | std.Infow(msg, kvs...) 119 | } 120 | 121 | // Notice logs normal but significant events, such as start up, shut down, or configuration. 122 | // Arguments are handled in the manner of fmt.Print. 123 | func Notice(v ...any) { 124 | std.Notice(v...) 125 | } 126 | 127 | // Noticeln logs normal but significant events, such as start up, shut down, or configuration. 128 | // Arguments are handled in the manner of fmt.Println. 129 | func Noticeln(v ...any) { 130 | std.Noticeln(v...) 131 | } 132 | 133 | // Noticef logs normal but significant events, such as start up, shut down, or configuration. 134 | // Arguments are handled in the manner of fmt.Printf. 135 | func Noticef(format string, v ...any) { 136 | std.Noticef(format, v...) 137 | } 138 | 139 | // Noticej logs normal but significant events, such as start up, shut down, or configuration. 140 | // Arguments populate jsonPayload in the log entry. 141 | func Noticej(msg string, v any) { 142 | std.Noticej(msg, v) 143 | } 144 | 145 | // Noticew logs normal but significant events, such as start up, shut down, or configuration. 146 | // Arguments populate jsonPayload in the log entry. 147 | func Noticew(msg string, kvs ...any) { 148 | std.Noticew(msg, kvs...) 149 | } 150 | 151 | // Warning logs events that might cause problems. 152 | // Arguments are handled in the manner of fmt.Print. 153 | func Warning(v ...any) { 154 | std.Warning(v...) 155 | } 156 | 157 | // Warningln logs events that might cause problems. 158 | // Arguments are handled in the manner of fmt.Println. 159 | func Warningln(v ...any) { 160 | std.Warningln(v...) 161 | } 162 | 163 | // Warningf logs events that might cause problems. 164 | // Arguments are handled in the manner of fmt.Printf. 165 | func Warningf(format string, v ...any) { 166 | std.Warningf(format, v...) 167 | } 168 | 169 | // Warningj logs events that might cause problems. 170 | // Arguments populate jsonPayload in the log entry. 171 | func Warningj(msg string, v any) { 172 | std.Warningj(msg, v) 173 | } 174 | 175 | // Warningw logs events that might cause problems. 176 | // Arguments populate jsonPayload in the log entry. 177 | func Warningw(msg string, kvs ...any) { 178 | std.Warningw(msg, kvs...) 179 | } 180 | 181 | // Error logs events likely to cause problems. 182 | // Arguments are handled in the manner of fmt.Print. 183 | func Error(v ...any) { 184 | std.Error(v...) 185 | } 186 | 187 | // Errorln logs events likely to cause problems. 188 | // Arguments are handled in the manner of fmt.Println. 189 | func Errorln(v ...any) { 190 | std.Errorln(v...) 191 | } 192 | 193 | // Errorf logs events likely to cause problems. 194 | // Arguments are handled in the manner of fmt.Printf. 195 | func Errorf(format string, v ...any) { 196 | std.Errorf(format, v...) 197 | } 198 | 199 | // Errorj logs events likely to cause problems. 200 | // Arguments populate jsonPayload in the log entry. 201 | func Errorj(msg string, v any) { 202 | std.Errorj(msg, v) 203 | } 204 | 205 | // Errorw logs events likely to cause problems. 206 | // Arguments populate jsonPayload in the log entry. 207 | func Errorw(msg string, kvs ...any) { 208 | std.Errorw(msg, kvs...) 209 | } 210 | 211 | // Critical logs events that cause more severe problems or outages. 212 | // Arguments are handled in the manner of fmt.Print. 213 | func Critical(v ...any) { 214 | std.Critical(v...) 215 | } 216 | 217 | // Criticalln logs events that cause more severe problems or outages. 218 | // Arguments are handled in the manner of fmt.Println. 219 | func Criticalln(v ...any) { 220 | std.Criticalln(v...) 221 | } 222 | 223 | // Criticalf logs events that cause more severe problems or outages. 224 | // Arguments are handled in the manner of fmt.Printf. 225 | func Criticalf(format string, v ...any) { 226 | std.Criticalf(format, v...) 227 | } 228 | 229 | // Criticalj logs events that cause more severe problems or outages. 230 | // Arguments populate jsonPayload in the log entry. 231 | func Criticalj(msg string, v any) { 232 | std.Criticalj(msg, v) 233 | } 234 | 235 | // Criticalw logs events that cause more severe problems or outages. 236 | // Arguments populate jsonPayload in the log entry. 237 | func Criticalw(msg string, kvs ...any) { 238 | std.Criticalw(msg, kvs...) 239 | } 240 | 241 | // Alert logs when a person must take an action immediately. 242 | // Arguments are handled in the manner of fmt.Print. 243 | func Alert(v ...any) { 244 | std.Alert(v...) 245 | } 246 | 247 | // Alertln logs when a person must take an action immediately. 248 | // Arguments are handled in the manner of fmt.Println. 249 | func Alertln(v ...any) { 250 | std.Alertln(v...) 251 | } 252 | 253 | // Alertf logs when a person must take an action immediately. 254 | // Arguments are handled in the manner of fmt.Printf. 255 | func Alertf(format string, v ...any) { 256 | std.Alertf(format, v...) 257 | } 258 | 259 | // Alertj logs when a person must take an action immediately. 260 | // Arguments populate jsonPayload in the log entry. 261 | func Alertj(msg string, v any) { 262 | std.Alertj(msg, v) 263 | } 264 | 265 | // Alertw logs when a person must take an action immediately. 266 | // Arguments populate jsonPayload in the log entry. 267 | func Alertw(msg string, kvs ...any) { 268 | std.Alertw(msg, kvs...) 269 | } 270 | 271 | // Emergency logs when one or more systems are unusable. 272 | // Arguments are handled in the manner of fmt.Print. 273 | func Emergency(v ...any) { 274 | std.Emergency(v...) 275 | } 276 | 277 | // Emergencyln logs when one or more systems are unusable. 278 | // Arguments are handled in the manner of fmt.Println. 279 | func Emergencyln(v ...any) { 280 | std.Emergencyln(v...) 281 | } 282 | 283 | // Emergencyf logs when one or more systems are unusable. 284 | // Arguments are handled in the manner of fmt.Printf. 285 | func Emergencyf(format string, v ...any) { 286 | std.Emergencyf(format, v...) 287 | } 288 | 289 | // Emergencyj logs when one or more systems are unusable. 290 | // Arguments populate jsonPayload in the log entry. 291 | func Emergencyj(msg string, v any) { 292 | std.Emergencyj(msg, v) 293 | } 294 | 295 | // Emergencyw logs when one or more systems are unusable. 296 | // Arguments populate jsonPayload in the log entry. 297 | func Emergencyw(msg string, kvs ...any) { 298 | std.Emergencyw(msg, kvs...) 299 | } 300 | 301 | // A Logger that logs entries with additional context. 302 | type Logger struct { 303 | callers int 304 | trace string 305 | spanID string 306 | executionID string 307 | request *httpRequest 308 | } 309 | 310 | // ForRequest creates a Logger with metadata from an http.Request. 311 | func ForRequest(r *http.Request) (l Logger) { 312 | l.trace, l.spanID = parseTraceContext(r.Header.Get("X-Cloud-Trace-Context")) 313 | l.executionID = r.Header.Get("Function-Execution-Id") 314 | l.request = &httpRequest{ 315 | RequestMethod: r.Method, 316 | RequestUrl: r.RequestURI, 317 | UserAgent: r.UserAgent(), 318 | RemoteIp: r.RemoteAddr, 319 | Referer: r.Referer(), 320 | Protocol: r.Proto, 321 | } 322 | return l 323 | } 324 | 325 | // ForContext creates a Logger with metadata from a context.Context. 326 | func ForContext(ctx context.Context) (l Logger) { 327 | l.SetContext(ctx) 328 | return l 329 | } 330 | 331 | // SetContext updates a Logger with metadata from a context.Context. 332 | func (l *Logger) SetContext(ctx context.Context) { 333 | if span := trace.FromContext(ctx); span != nil { 334 | l.trace, l.spanID = fromSpanContext(span.SpanContext()) 335 | } 336 | if meta, _ := metadata.FromContext(ctx); meta != nil { 337 | l.executionID = meta.EventID 338 | } 339 | } 340 | 341 | // Print logs an entry with no assigned severity level. 342 | // Arguments are handled in the manner of fmt.Print. 343 | func (l Logger) Print(v ...any) { 344 | logm(defaultsv, l, v...) 345 | } 346 | 347 | // Println logs an entry with no assigned severity level. 348 | // Arguments are handled in the manner of fmt.Println. 349 | func (l Logger) Println(v ...any) { 350 | logn(defaultsv, l, v...) 351 | } 352 | 353 | // Printf logs an entry with no assigned severity level. 354 | // Arguments are handled in the manner of fmt.Printf. 355 | func (l Logger) Printf(format string, v ...any) { 356 | logf(defaultsv, l, format, v...) 357 | } 358 | 359 | // Printj logs an entry with no assigned severity level. 360 | // Arguments populate jsonPayload in the log entry. 361 | func (l Logger) Printj(msg string, v any) { 362 | logj(defaultsv, l, msg, v) 363 | } 364 | 365 | // Printw logs an entry with no assigned severity level. 366 | // Arguments populate jsonPayload in the log entry. 367 | func (l Logger) Printw(msg string, kvs ...any) { 368 | logw(defaultsv, l, msg, kvs) 369 | } 370 | 371 | // Debug logs debug or trace information. 372 | // Arguments are handled in the manner of fmt.Print. 373 | func (l Logger) Debug(v ...any) { 374 | logm(debugsv, l, v...) 375 | } 376 | 377 | // Debugln logs debug or trace information. 378 | // Arguments are handled in the manner of fmt.Println. 379 | func (l Logger) Debugln(v ...any) { 380 | logn(debugsv, l, v...) 381 | } 382 | 383 | // Debugf logs debug or trace information. 384 | // Arguments are handled in the manner of fmt.Printf. 385 | func (l Logger) Debugf(format string, v ...any) { 386 | logf(debugsv, l, format, v...) 387 | } 388 | 389 | // Debugj logs debug or trace information. 390 | // Arguments populate jsonPayload in the log entry. 391 | func (l Logger) Debugj(msg string, v any) { 392 | logj(debugsv, l, msg, v) 393 | } 394 | 395 | // Debugw logs debug or trace information. 396 | // Arguments populate jsonPayload in the log entry. 397 | func (l Logger) Debugw(msg string, kvs ...any) { 398 | logw(debugsv, l, msg, kvs) 399 | } 400 | 401 | // Info logs routine information, such as ongoing status or performance. 402 | // Arguments are handled in the manner of fmt.Print. 403 | func (l Logger) Info(v ...any) { 404 | logm(infosv, l, v...) 405 | } 406 | 407 | // Infoln logs routine information, such as ongoing status or performance. 408 | // Arguments are handled in the manner of fmt.Println. 409 | func (l Logger) Infoln(v ...any) { 410 | logn(infosv, l, v...) 411 | } 412 | 413 | // Infof logs routine information, such as ongoing status or performance. 414 | // Arguments are handled in the manner of fmt.Printf. 415 | func (l Logger) Infof(format string, v ...any) { 416 | logf(infosv, l, format, v...) 417 | } 418 | 419 | // Infoj logs routine information, such as ongoing status or performance. 420 | // Arguments populate jsonPayload in the log entry. 421 | func (l Logger) Infoj(msg string, v any) { 422 | logj(infosv, l, msg, v) 423 | } 424 | 425 | // Infow logs routine information, such as ongoing status or performance. 426 | // Arguments populate jsonPayload in the log entry. 427 | func (l Logger) Infow(msg string, kvs ...any) { 428 | logw(infosv, l, msg, kvs) 429 | } 430 | 431 | // Notice logs normal but significant events, such as start up, shut down, or configuration. 432 | // Arguments are handled in the manner of fmt.Print. 433 | func (l Logger) Notice(v ...any) { 434 | logm(noticesv, l, v...) 435 | } 436 | 437 | // Noticeln logs normal but significant events, such as start up, shut down, or configuration. 438 | // Arguments are handled in the manner of fmt.Println. 439 | func (l Logger) Noticeln(v ...any) { 440 | logn(noticesv, l, v...) 441 | } 442 | 443 | // Noticef logs normal but significant events, such as start up, shut down, or configuration. 444 | // Arguments are handled in the manner of fmt.Printf. 445 | func (l Logger) Noticef(format string, v ...any) { 446 | logf(noticesv, l, format, v...) 447 | } 448 | 449 | // Noticej logs normal but significant events, such as start up, shut down, or configuration. 450 | // Arguments populate jsonPayload in the log entry. 451 | func (l Logger) Noticej(msg string, v any) { 452 | logj(noticesv, l, msg, v) 453 | } 454 | 455 | // Noticew logs normal but significant events, such as start up, shut down, or configuration. 456 | // Arguments populate jsonPayload in the log entry. 457 | func (l Logger) Noticew(msg string, kvs ...any) { 458 | logw(noticesv, l, msg, kvs) 459 | } 460 | 461 | // Warning logs events that might cause problems. 462 | // Arguments are handled in the manner of fmt.Print. 463 | func (l Logger) Warning(v ...any) { 464 | logm(warningsv, l, v...) 465 | } 466 | 467 | // Warningln logs events that might cause problems. 468 | // Arguments are handled in the manner of fmt.Println. 469 | func (l Logger) Warningln(v ...any) { 470 | logn(warningsv, l, v...) 471 | } 472 | 473 | // Warningf logs events that might cause problems. 474 | // Arguments are handled in the manner of fmt.Printf. 475 | func (l Logger) Warningf(format string, v ...any) { 476 | logf(warningsv, l, format, v...) 477 | } 478 | 479 | // Warningj logs events that might cause problems. 480 | // Arguments populate jsonPayload in the log entry. 481 | func (l Logger) Warningj(msg string, v any) { 482 | logj(warningsv, l, msg, v) 483 | } 484 | 485 | // Warningw logs events that might cause problems. 486 | // Arguments populate jsonPayload in the log entry. 487 | func (l Logger) Warningw(msg string, kvs ...any) { 488 | logw(warningsv, l, msg, kvs) 489 | } 490 | 491 | // Error logs events likely to cause problems. 492 | // Arguments are handled in the manner of fmt.Print. 493 | func (l Logger) Error(v ...any) { 494 | logm(errorsv, l, v...) 495 | } 496 | 497 | // Errorln logs events likely to cause problems. 498 | // Arguments are handled in the manner of fmt.Println. 499 | func (l Logger) Errorln(v ...any) { 500 | logn(errorsv, l, v...) 501 | } 502 | 503 | // Errorf logs events likely to cause problems. 504 | // Arguments are handled in the manner of fmt.Printf. 505 | func (l Logger) Errorf(format string, v ...any) { 506 | logf(errorsv, l, format, v...) 507 | } 508 | 509 | // Errorj logs events likely to cause problems. 510 | // Arguments populate jsonPayload in the log entry. 511 | func (l Logger) Errorj(msg string, v any) { 512 | logj(errorsv, l, msg, v) 513 | } 514 | 515 | // Errorw logs events likely to cause problems. 516 | // Arguments populate jsonPayload in the log entry. 517 | func (l Logger) Errorw(msg string, kvs ...any) { 518 | logw(errorsv, l, msg, kvs) 519 | } 520 | 521 | // Critical logs events that cause more severe problems or outages. 522 | // Arguments are handled in the manner of fmt.Print. 523 | func (l Logger) Critical(v ...any) { 524 | logm(criticalsv, l, v...) 525 | } 526 | 527 | // Criticalln logs events that cause more severe problems or outages. 528 | // Arguments are handled in the manner of fmt.Println. 529 | func (l Logger) Criticalln(v ...any) { 530 | logn(criticalsv, l, v...) 531 | } 532 | 533 | // Criticalf logs events that cause more severe problems or outages. 534 | // Arguments are handled in the manner of fmt.Printf. 535 | func (l Logger) Criticalf(format string, v ...any) { 536 | logf(criticalsv, l, format, v...) 537 | } 538 | 539 | // Criticalj logs events that cause more severe problems or outages. 540 | // Arguments populate jsonPayload in the log entry. 541 | func (l Logger) Criticalj(msg string, v any) { 542 | logj(criticalsv, l, msg, v) 543 | } 544 | 545 | // Criticalw logs events that cause more severe problems or outages. 546 | // Arguments populate jsonPayload in the log entry. 547 | func (l Logger) Criticalw(msg string, kvs ...any) { 548 | logw(criticalsv, l, msg, kvs) 549 | } 550 | 551 | // Alert logs when a person must take an action immediately. 552 | // Arguments are handled in the manner of fmt.Print. 553 | func (l Logger) Alert(v ...any) { 554 | logm(alertsv, l, v...) 555 | } 556 | 557 | // Alertln logs when a person must take an action immediately. 558 | // Arguments are handled in the manner of fmt.Println. 559 | func (l Logger) Alertln(v ...any) { 560 | logn(alertsv, l, v...) 561 | } 562 | 563 | // Alertf logs when a person must take an action immediately. 564 | // Arguments are handled in the manner of fmt.Printf. 565 | func (l Logger) Alertf(format string, v ...any) { 566 | logf(alertsv, l, format, v...) 567 | } 568 | 569 | // Alertj logs when a person must take an action immediately. 570 | // Arguments populate jsonPayload in the log entry. 571 | func (l Logger) Alertj(msg string, v any) { 572 | logj(alertsv, l, msg, v) 573 | } 574 | 575 | // Alertw logs when a person must take an action immediately. 576 | // Arguments populate jsonPayload in the log entry. 577 | func (l Logger) Alertw(msg string, kvs ...any) { 578 | logw(alertsv, l, msg, kvs) 579 | } 580 | 581 | // Emergency logs when one or more systems are unusable. 582 | // Arguments are handled in the manner of fmt.Print. 583 | func (l Logger) Emergency(v ...any) { 584 | logm(emergencysv, l, v...) 585 | } 586 | 587 | // Emergencyln logs when one or more systems are unusable. 588 | // Arguments are handled in the manner of fmt.Println. 589 | func (l Logger) Emergencyln(v ...any) { 590 | logn(emergencysv, l, v...) 591 | } 592 | 593 | // Emergencyf logs when one or more systems are unusable. 594 | // Arguments are handled in the manner of fmt.Printf. 595 | func (l Logger) Emergencyf(format string, v ...any) { 596 | logf(emergencysv, l, format, v...) 597 | } 598 | 599 | // Emergencyj logs when one or more systems are unusable. 600 | // Arguments populate jsonPayload in the log entry. 601 | func (l Logger) Emergencyj(msg string, v any) { 602 | logj(emergencysv, l, msg, v) 603 | } 604 | 605 | // Emergencyw logs when one or more systems are unusable. 606 | // Arguments populate jsonPayload in the log entry. 607 | func (l Logger) Emergencyw(msg string, kvs ...any) { 608 | logw(emergencysv, l, msg, kvs) 609 | } 610 | 611 | type severity int32 612 | 613 | const ( 614 | defaultsv severity = iota * 100 615 | debugsv 616 | infosv 617 | noticesv 618 | warningsv 619 | errorsv 620 | criticalsv 621 | alertsv 622 | emergencysv 623 | ) 624 | 625 | func (s severity) String() string { 626 | switch s { 627 | default: 628 | return "" 629 | case debugsv: 630 | return "DEBUG" 631 | case infosv: 632 | return "INFO" 633 | case noticesv: 634 | return "NOTICE" 635 | case warningsv: 636 | return "WARNING" 637 | case errorsv: 638 | return "ERROR" 639 | case criticalsv: 640 | return "CRITICAL" 641 | case alertsv: 642 | return "ALERT" 643 | case emergencysv: 644 | return "EMERGENCY" 645 | } 646 | } 647 | 648 | func (s severity) File() *os.File { 649 | if s >= errorsv { 650 | return os.Stderr 651 | } else { 652 | return os.Stdout 653 | } 654 | } 655 | 656 | func logm(s severity, l Logger, v ...any) { 657 | logs(s, l, fmt.Sprint(v...)) 658 | } 659 | 660 | func logn(s severity, l Logger, v ...any) { 661 | logs(s, l, fmt.Sprintln(v...)) 662 | } 663 | 664 | func logf(s severity, l Logger, format string, v ...any) { 665 | logs(s, l, fmt.Sprintf(format, v...)) 666 | } 667 | 668 | func logs(s severity, l Logger, msg string) { 669 | entry := entry{ 670 | Message: strings.TrimSuffix(msg, "\n"), 671 | Severity: s.String(), 672 | Trace: l.trace, 673 | SpanID: l.spanID, 674 | HttpRequest: l.request, 675 | SourceLocation: location(4 + l.callers), 676 | Labels: executionLabels(l.executionID), 677 | } 678 | json.NewEncoder(s.File()).Encode(entry) 679 | } 680 | 681 | func logj(s severity, l Logger, msg string, j any) { 682 | entry := make(map[string]json.RawMessage) 683 | if buf, err := json.Marshal(j); err != nil { 684 | panic(err) 685 | } else if err := json.Unmarshal(buf, &entry); err != nil { 686 | panic(err) 687 | } 688 | 689 | loge(s, l, msg, entry) 690 | } 691 | 692 | func logw(s severity, l Logger, msg string, kvs []any) { 693 | entry := make(map[string]json.RawMessage, len(kvs)/2) 694 | for i := 0; i < len(kvs); i += 2 { 695 | var err error 696 | k, v := kvs[i].(string), kvs[i+1] 697 | entry[k], err = json.Marshal(v) 698 | if err != nil { 699 | panic(err) 700 | } 701 | } 702 | 703 | loge(s, l, msg, entry) 704 | } 705 | 706 | func loge(s severity, l Logger, msg string, entry map[string]json.RawMessage) { 707 | if v := msg; v != "" { 708 | entry["message"], _ = json.Marshal(v) 709 | } 710 | if v := s; v != 0 { 711 | entry["severity"], _ = json.Marshal(v.String()) 712 | } 713 | if v := l.trace; v != "" { 714 | entry["logging.googleapis.com/trace"], _ = json.Marshal(v) 715 | } 716 | if v := l.spanID; v != "" { 717 | entry["logging.googleapis.com/spanId"], _ = json.Marshal(v) 718 | } 719 | if v := l.request; v != nil { 720 | entry["httpRequest"], _ = json.Marshal(v) 721 | } 722 | if v := l.executionID; v != "" { 723 | entry["labels"], _ = json.Marshal(executionLabels(l.executionID)) 724 | } 725 | if v := location(4 + l.callers); v != nil { 726 | entry["logging.googleapis.com/sourceLocation"], _ = json.Marshal(v) 727 | } 728 | 729 | json.NewEncoder(s.File()).Encode(entry) 730 | } 731 | 732 | type entry struct { 733 | Message string `json:"message"` 734 | Severity string `json:"severity,omitempty"` 735 | Trace string `json:"logging.googleapis.com/trace,omitempty"` 736 | SpanID string `json:"logging.googleapis.com/spanId,omitempty"` 737 | 738 | HttpRequest *httpRequest `json:"httpRequest,omitempty"` 739 | SourceLocation *sourceLocation `json:"logging.googleapis.com/sourceLocation,omitempty"` 740 | Labels executionLabels `json:"logging.googleapis.com/labels,omitempty"` 741 | } 742 | 743 | type httpRequest struct { 744 | RequestMethod string `json:"requestMethod,omitempty"` 745 | RequestUrl string `json:"requestUrl,omitempty"` 746 | UserAgent string `json:"userAgent,omitempty"` 747 | RemoteIp string `json:"remoteIp,omitempty"` 748 | Referer string `json:"referer,omitempty"` 749 | Protocol string `json:"protocol,omitempty"` 750 | } 751 | 752 | type executionLabels string 753 | 754 | func (e executionLabels) MarshalJSON() ([]byte, error) { 755 | return json.Marshal(struct { 756 | ExecutionID string `json:"execution_id"` 757 | }{ 758 | string(e), 759 | }) 760 | } 761 | 762 | type sourceLocation struct { 763 | File string `json:"file,omitempty"` 764 | Line string `json:"line,omitempty"` 765 | Function string `json:"function,omitempty"` 766 | } 767 | --------------------------------------------------------------------------------