From 79b16b082fa4402c90fa1f31ec4d2e5695006b60 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Fri, 23 Jan 2026 19:29:06 -0500 Subject: [PATCH 01/19] perf: Optimize Stringify allocations (~2x faster) --- github/strings.go | 31 ++++++++- github/strings_benchmark_test.go | 37 +++++++++++ github/strings_test.go | 106 +++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 github/strings_benchmark_test.go diff --git a/github/strings.go b/github/strings.go index 0158c9a1fdc..dfbe3c685f9 100644 --- a/github/strings.go +++ b/github/strings.go @@ -9,17 +9,30 @@ import ( "bytes" "fmt" "reflect" + "strconv" + "sync" ) var timestampType = reflect.TypeFor[Timestamp]() +var bufferPool = sync.Pool{ + New: func() any { + return new(bytes.Buffer) + }, +} + // Stringify attempts to create a reasonable string representation of types in // the GitHub library. It does things like resolve pointers to their values // and omits struct fields with nil values. func Stringify(message any) string { - var buf bytes.Buffer + buf := bufferPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + bufferPool.Put(buf) + }() + v := reflect.ValueOf(message) - stringifyValue(&buf, v) + stringifyValue(buf, v) return buf.String() } @@ -34,8 +47,20 @@ func stringifyValue(w *bytes.Buffer, val reflect.Value) { v := reflect.Indirect(val) switch v.Kind() { + case reflect.Bool: + w.Write(strconv.AppendBool(w.Bytes(), v.Bool())[w.Len():]) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + w.Write(strconv.AppendInt(w.Bytes(), v.Int(), 10)[w.Len():]) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + w.Write(strconv.AppendUint(w.Bytes(), v.Uint(), 10)[w.Len():]) + case reflect.Float32: + w.Write(strconv.AppendFloat(w.Bytes(), v.Float(), 'g', -1, 32)[w.Len():]) + case reflect.Float64: + w.Write(strconv.AppendFloat(w.Bytes(), v.Float(), 'g', -1, 64)[w.Len():]) case reflect.String: - fmt.Fprintf(w, `"%v"`, v) + w.WriteByte('"') + w.WriteString(v.String()) + w.WriteByte('"') case reflect.Slice: w.WriteByte('[') for i := range v.Len() { diff --git a/github/strings_benchmark_test.go b/github/strings_benchmark_test.go new file mode 100644 index 00000000000..22bc0a2a29c --- /dev/null +++ b/github/strings_benchmark_test.go @@ -0,0 +1,37 @@ +// Copyright 2013 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "testing" +) + +type BenchmarkStruct struct { + Name string + Age int + Active bool + Score float32 + Rank float64 + Tags []string + Pointer *int +} + +func BenchmarkStringify(b *testing.B) { + val := 42 + s := &BenchmarkStruct{ + Name: "benchmark", + Age: 30, + Active: true, + Score: 1.1, + Rank: 99.999999, + Tags: []string{"go", "github", "api"}, + Pointer: Ptr(val), + } + b.ResetTimer() + for b.Loop() { + Stringify(s) + } +} diff --git a/github/strings_test.go b/github/strings_test.go index accd8529362..3dfb46ea687 100644 --- a/github/strings_test.go +++ b/github/strings_test.go @@ -81,6 +81,92 @@ func TestStringify(t *testing.T) { } } +func TestStringify_Primitives(t *testing.T) { + t.Parallel() + tests := []struct { + in any + out string + }{ + // Bool + {true, "true"}, + {false, "false"}, + + // Int variants + {int(1), "1"}, + {int8(2), "2"}, + {int16(3), "3"}, + {int32(4), "4"}, + {int64(5), "5"}, + + // Uint variants + {uint(6), "6"}, + {uint8(7), "7"}, + {uint16(8), "8"}, + {uint32(9), "9"}, + {uint64(10), "10"}, + {uintptr(11), "11"}, + + // Float variants (Precision Correctness) + {float32(1.1), "1.1"}, + {float64(1.1), "1.1"}, + {float32(1.0000001), "1.0000001"}, + {float64(1.000000000000001), "1.000000000000001"}, + + // Boundary Cases + {int8(-128), "-128"}, + {int8(127), "127"}, + {uint64(18446744073709551615), "18446744073709551615"}, + + // String Optimization + {"hello", `"hello"`}, + {"", `""`}, + } + + for i, tt := range tests { + s := Stringify(tt.in) + if s != tt.out { + t.Errorf("%v. Stringify(%T) => %q, want %q", i, tt.in, s, tt.out) + } + } +} + +func TestStringify_BufferPool(t *testing.T) { + t.Parallel() + // Verify that concurrent usage of Stringify is safe and doesn't corrupt buffers. + // While we can't easily verify reuse without exposing internal metrics, + // we can verify correctness under load which implies proper Reset() handling. + const goroutines = 10 + const iterations = 100 + + errCh := make(chan error, goroutines) + + for range goroutines { + go func() { + for range iterations { + // Use a mix of types to exercise different code paths + s1 := Stringify(123) + if s1 != "123" { + errCh <- fmt.Errorf("got %q, want %q", s1, "123") + return + } + + s2 := Stringify("test") + if s2 != `"test"` { + errCh <- fmt.Errorf("got %q, want %q", s2, `"test"`) + return + } + } + errCh <- nil + }() + } + + for range goroutines { + if err := <-errCh; err != nil { + t.Error(err) + } + } +} + // Directly test the String() methods on various GitHub types. We don't do an // exhaustive test of all the various field types, since TestStringify() above // takes care of that. Rather, we just make sure that Stringify() is being @@ -143,3 +229,23 @@ func TestString(t *testing.T) { } } } + +func TestStringify_Floats(t *testing.T) { + t.Parallel() + tests := []struct { + in any + out string + }{ + {float32(1.1), "1.1"}, + {float64(1.1), "1.1"}, + {float32(1.0000001), "1.0000001"}, + {struct{ F float32 }{1.1}, "{F:1.1}"}, + } + + for i, tt := range tests { + s := Stringify(tt.in) + if s != tt.out { + t.Errorf("%v. Stringify(%v) = %q, want %q", i, tt.in, s, tt.out) + } + } +} From 9e260bd61721b996660a3f315380b28c4cb0810e Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:49:16 -0500 Subject: [PATCH 02/19] Apply suggestions from code review --- github/strings_benchmark_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/strings_benchmark_test.go b/github/strings_benchmark_test.go index 22bc0a2a29c..bec928f2bcd 100644 --- a/github/strings_benchmark_test.go +++ b/github/strings_benchmark_test.go @@ -1,4 +1,4 @@ -// Copyright 2013 The go-github AUTHORS. All rights reserved. +// Copyright 2026 The go-github AUTHORS. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. From 2ac81f84e81bc0a6bfd7b863cc92782897dde2ff Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 10:15:06 -0500 Subject: [PATCH 03/19] fix(pr): revert formatting scope creep; re-apply HeaderRateReset export --- example/otel/go.mod | 24 ++++++++ example/otel/go.sum | 30 ++++++++++ example/otel/main.go | 61 +++++++++++++++++++++ github/github.go | 4 +- otel/example/go.mod | 14 +++++ otel/example/main.go | 57 +++++++++++++++++++ otel/go.mod | 16 ++++++ otel/go.sum | 26 +++++++++ otel/transport.go | 127 +++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 357 insertions(+), 2 deletions(-) create mode 100644 example/otel/go.mod create mode 100644 example/otel/go.sum create mode 100644 example/otel/main.go create mode 100644 otel/example/go.mod create mode 100644 otel/example/main.go create mode 100644 otel/go.mod create mode 100644 otel/go.sum create mode 100644 otel/transport.go diff --git a/example/otel/go.mod b/example/otel/go.mod new file mode 100644 index 00000000000..b86dd6e85a5 --- /dev/null +++ b/example/otel/go.mod @@ -0,0 +1,24 @@ +module github.com/google/go-github/v82/example/otel + +go 1.24.0 + +require ( + github.com/google/go-github/v82 v82.0.0 + github.com/google/go-github/v82/otel v0.0.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 + go.opentelemetry.io/otel/sdk v1.24.0 +) + +require ( + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-querystring v1.2.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/sys v0.17.0 // indirect +) + +replace github.com/google/go-github/v82 => ../../ + +replace github.com/google/go-github/v82/otel => ../../otel diff --git a/example/otel/go.sum b/example/otel/go.sum new file mode 100644 index 00000000000..508a96dc708 --- /dev/null +++ b/example/otel/go.sum @@ -0,0 +1,30 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/example/otel/main.go b/example/otel/main.go new file mode 100644 index 00000000000..c5e9fe37753 --- /dev/null +++ b/example/otel/main.go @@ -0,0 +1,61 @@ +// Copyright 2026 The go-github Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/google/go-github/v82/github" + "github.com/google/go-github/v82/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/trace" +) + +func main() { + // Initialize stdout exporter to see traces in console + exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + log.Fatalf("failed to initialize stdouttrace exporter: %v", err) + } + + tp := trace.NewTracerProvider( + trace.WithBatcher(exporter), + ) + defer func() { + if err := tp.Shutdown(context.Background()); err != nil { + log.Fatal(err) + } + }() + + // Configure HTTP client with OTel transport + httpClient := &http.Client{ + Transport: otel.NewTransport( + http.DefaultTransport, + otel.WithTracerProvider(tp), + ), + } + + // Create GitHub client + client := github.NewClient(httpClient) + + // Make a request (Get Rate Limits is public and cheap) + limits, resp, err := client.RateLimits(context.Background()) + if err != nil { + log.Printf("Error fetching rate limits: %v", err) + } else { + fmt.Printf("Core Rate Limit: %d/%d (Resets at %v)\n", + limits.GetCore().Remaining, + limits.GetCore().Limit, + limits.GetCore().Reset) + } + + // Check if we captured attributes in response + if resp != nil { + fmt.Printf("Response Status: %s\n", resp.Status) + } +} diff --git a/github/github.go b/github/github.go index 9ab1fb11d7a..6e14325b652 100644 --- a/github/github.go +++ b/github/github.go @@ -40,7 +40,7 @@ const ( headerRateLimit = "X-Ratelimit-Limit" headerRateRemaining = "X-Ratelimit-Remaining" headerRateUsed = "X-Ratelimit-Used" - headerRateReset = "X-Ratelimit-Reset" + HeaderRateReset = "X-Ratelimit-Reset" headerRateResource = "X-Ratelimit-Resource" headerOTP = "X-Github-Otp" headerRetryAfter = "Retry-After" @@ -794,7 +794,7 @@ func parseRate(r *http.Response) Rate { if used := r.Header.Get(headerRateUsed); used != "" { rate.Used, _ = strconv.Atoi(used) } - if reset := r.Header.Get(headerRateReset); reset != "" { + if reset := r.Header.Get(HeaderRateReset); reset != "" { if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { rate.Reset = Timestamp{time.Unix(v, 0)} } diff --git a/otel/example/go.mod b/otel/example/go.mod new file mode 100644 index 00000000000..556f58d139f --- /dev/null +++ b/otel/example/go.mod @@ -0,0 +1,14 @@ +module github.com/google/go-github/v81/otel/example + +go 1.24.0 + +require ( + github.com/google/go-github/v81 v81.0.0 + github.com/google/go-github/v81/otel v0.0.0 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 + go.opentelemetry.io/otel/sdk v1.24.0 +) + +replace github.com/google/go-github/v81 => ../../ +replace github.com/google/go-github/v81/otel => ../ diff --git a/otel/example/main.go b/otel/example/main.go new file mode 100644 index 00000000000..cbbd87e6649 --- /dev/null +++ b/otel/example/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + + "github.com/google/go-github/v81/github" + "github.com/google/go-github/v81/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + "go.opentelemetry.io/otel/sdk/trace" +) + +func main() { + // Initialize stdout exporter to see traces in console + exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + log.Fatalf("failed to initialize stdouttrace exporter: %v", err) + } + + tp := trace.NewTracerProvider( + trace.WithBatcher(exporter), + ) + defer func() { + if err := tp.Shutdown(context.Background()); err != nil { + log.Fatal(err) + } + }() + + // Configure HTTP client with OTel transport + httpClient := &http.Client{ + Transport: otel.NewTransport( + http.DefaultTransport, + otel.WithTracerProvider(tp), + ), + } + + // Create GitHub client + client := github.NewClient(httpClient) + + // Make a request (Get Rate Limits is public and cheap) + limits, resp, err := client.RateLimits(context.Background()) + if err != nil { + log.Printf("Error fetching rate limits: %v", err) + } else { + fmt.Printf("Core Rate Limit: %d/%d (Resets at %v)\n", + limits.GetCore().Remaining, + limits.GetCore().Limit, + limits.GetCore().Reset) + } + + // Check if we captured attributes in response + if resp != nil { + fmt.Printf("Response Status: %s\n", resp.Status) + } +} diff --git a/otel/go.mod b/otel/go.mod new file mode 100644 index 00000000000..4e15ef786be --- /dev/null +++ b/otel/go.mod @@ -0,0 +1,16 @@ +module github.com/google/go-github/v81/otel + +go 1.24.0 + +require ( + github.com/google/go-github/v82 v82.0.0 + go.opentelemetry.io/otel v1.24.0 + go.opentelemetry.io/otel/metric v1.24.0 + go.opentelemetry.io/otel/trace v1.24.0 +) + +require ( + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-querystring v1.2.0 // indirect +) diff --git a/otel/go.sum b/otel/go.sum new file mode 100644 index 00000000000..1dcfe9d542e --- /dev/null +++ b/otel/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk= +github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/otel/transport.go b/otel/transport.go new file mode 100644 index 00000000000..fc779c8bba1 --- /dev/null +++ b/otel/transport.go @@ -0,0 +1,127 @@ +// Copyright 2026 The go-github Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package otel + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/google/go-github/v82/github" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" +) + +const ( + // instrumentationName is the name of this instrumentation package. + // NOTE: This must be updated when the major version of go-github changes. + instrumentationName = "github.com/google/go-github/v82/otel" +) + +// Transport is an http.RoundTripper that instrument requests with OpenTelemetry. +type Transport struct { + Base http.RoundTripper + Tracer trace.Tracer + Meter metric.Meter +} + +// NewTransport creates a new OpenTelemetry transport. +func NewTransport(base http.RoundTripper, opts ...Option) *Transport { + if base == nil { + base = http.DefaultTransport + } + t := &Transport{Base: base} + for _, opt := range opts { + opt(t) + } + if t.Tracer == nil { + t.Tracer = otel.Tracer(instrumentationName) + } + if t.Meter == nil { + t.Meter = otel.Meter(instrumentationName) + } + return t +} + +// RoundTrip implements http.RoundTripper. +func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { + ctx := req.Context() + spanName := fmt.Sprintf("github/%s", req.Method) + + // Start Span + ctx, span := t.Tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + // Inject Attributes + span.SetAttributes( + attribute.String("http.method", req.Method), + attribute.String("http.url", req.URL.String()), + attribute.String("http.host", req.URL.Host), + ) + + // Inject Propagation Headers + otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) + + // Execute Request + resp, err := t.Base.RoundTrip(req) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err + } + + // Capture response attributes + span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode)) + + // Capture GitHub Specifics + if limit := resp.Header.Get("X-Ratelimit-Limit"); limit != "" { + if v, err := strconv.Atoi(limit); err == nil { + span.SetAttributes(attribute.Int("github.rate_limit.limit", v)) + } + } + if remaining := resp.Header.Get("X-Ratelimit-Remaining"); remaining != "" { + if v, err := strconv.Atoi(remaining); err == nil { + span.SetAttributes(attribute.Int("github.rate_limit.remaining", v)) + } + } + if reset := resp.Header.Get(github.HeaderRateReset); reset != "" { + span.SetAttributes(attribute.String("github.rate_limit.reset", reset)) + } + if reqID := resp.Header.Get("X-Github-Request-Id"); reqID != "" { + span.SetAttributes(attribute.String("github.request_id", reqID)) + } + if resource := resp.Header.Get("X-Ratelimit-Resource"); resource != "" { + span.SetAttributes(attribute.String("github.rate_limit.resource", resource)) + } + + if resp.StatusCode >= 400 { + span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", resp.StatusCode)) + } else { + span.SetStatus(codes.Ok, "OK") + } + + return resp, nil +} + +// Option applies configuration to Transport. +type Option func(*Transport) + +// WithTracerProvider configures the TracerProvider. +func WithTracerProvider(tp trace.TracerProvider) Option { + return func(t *Transport) { + t.Tracer = tp.Tracer(instrumentationName) + } +} + +// WithMeterProvider configures the MeterProvider. +func WithMeterProvider(mp metric.MeterProvider) Option { + return func(t *Transport) { + t.Meter = mp.Meter(instrumentationName) + } +} From cd280c9a8dcdc7db0ae036d2e79e9669bc135bf2 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 10:17:01 -0500 Subject: [PATCH 04/19] fix(pr): remove duplicate folder and align go.mod versions --- otel/example/go.mod | 14 ----------- otel/example/main.go | 57 -------------------------------------------- otel/go.mod | 2 +- 3 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 otel/example/go.mod delete mode 100644 otel/example/main.go diff --git a/otel/example/go.mod b/otel/example/go.mod deleted file mode 100644 index 556f58d139f..00000000000 --- a/otel/example/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module github.com/google/go-github/v81/otel/example - -go 1.24.0 - -require ( - github.com/google/go-github/v81 v81.0.0 - github.com/google/go-github/v81/otel v0.0.0 - go.opentelemetry.io/otel v1.24.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 - go.opentelemetry.io/otel/sdk v1.24.0 -) - -replace github.com/google/go-github/v81 => ../../ -replace github.com/google/go-github/v81/otel => ../ diff --git a/otel/example/main.go b/otel/example/main.go deleted file mode 100644 index cbbd87e6649..00000000000 --- a/otel/example/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - - "github.com/google/go-github/v81/github" - "github.com/google/go-github/v81/otel" - "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" - "go.opentelemetry.io/otel/sdk/trace" -) - -func main() { - // Initialize stdout exporter to see traces in console - exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) - if err != nil { - log.Fatalf("failed to initialize stdouttrace exporter: %v", err) - } - - tp := trace.NewTracerProvider( - trace.WithBatcher(exporter), - ) - defer func() { - if err := tp.Shutdown(context.Background()); err != nil { - log.Fatal(err) - } - }() - - // Configure HTTP client with OTel transport - httpClient := &http.Client{ - Transport: otel.NewTransport( - http.DefaultTransport, - otel.WithTracerProvider(tp), - ), - } - - // Create GitHub client - client := github.NewClient(httpClient) - - // Make a request (Get Rate Limits is public and cheap) - limits, resp, err := client.RateLimits(context.Background()) - if err != nil { - log.Printf("Error fetching rate limits: %v", err) - } else { - fmt.Printf("Core Rate Limit: %d/%d (Resets at %v)\n", - limits.GetCore().Remaining, - limits.GetCore().Limit, - limits.GetCore().Reset) - } - - // Check if we captured attributes in response - if resp != nil { - fmt.Printf("Response Status: %s\n", resp.Status) - } -} diff --git a/otel/go.mod b/otel/go.mod index 4e15ef786be..411aaf4220e 100644 --- a/otel/go.mod +++ b/otel/go.mod @@ -1,4 +1,4 @@ -module github.com/google/go-github/v81/otel +module github.com/google/go-github/v82/otel go 1.24.0 From 02bb4d080eb33f3afcc194af687f4fec44519e4e Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 15:25:54 -0500 Subject: [PATCH 05/19] Update example/otel/main.go Co-authored-by: Oleksandr Redko --- example/otel/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/otel/main.go b/example/otel/main.go index c5e9fe37753..ffa585f0ee2 100644 --- a/example/otel/main.go +++ b/example/otel/main.go @@ -1,4 +1,5 @@ -// Copyright 2026 The go-github Authors. All rights reserved. +// Copyright 2026 The go-github AUTHORS. All rights reserved. +// // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. From acfd8841f880d3fc0009b54952afd0670ddce2d0 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 15:26:08 -0500 Subject: [PATCH 06/19] Update example/otel/main.go Co-authored-by: Oleksandr Redko --- example/otel/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/example/otel/main.go b/example/otel/main.go index ffa585f0ee2..47ebdb8051b 100644 --- a/example/otel/main.go +++ b/example/otel/main.go @@ -3,6 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// This example demonstrates ... package main import ( From 0ea6962a090f4b829a6c3eaea204d84e86188e90 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 15:26:19 -0500 Subject: [PATCH 07/19] Update example/otel/main.go Co-authored-by: Oleksandr Redko --- example/otel/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/otel/main.go b/example/otel/main.go index 47ebdb8051b..861e28aa983 100644 --- a/example/otel/main.go +++ b/example/otel/main.go @@ -58,6 +58,6 @@ func main() { // Check if we captured attributes in response if resp != nil { - fmt.Printf("Response Status: %s\n", resp.Status) + fmt.Printf("Response Status: %v\n", resp.Status) } } From c7772b7914b14419121b1d5d2ebd9468ae6dd057 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 15:26:27 -0500 Subject: [PATCH 08/19] Update otel/transport.go Co-authored-by: Oleksandr Redko --- otel/transport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otel/transport.go b/otel/transport.go index fc779c8bba1..ecef03a742d 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -21,7 +21,7 @@ import ( const ( // instrumentationName is the name of this instrumentation package. // NOTE: This must be updated when the major version of go-github changes. - instrumentationName = "github.com/google/go-github/v82/otel" + instrumentationName = "github.com/google/go-github/otel" ) // Transport is an http.RoundTripper that instrument requests with OpenTelemetry. From de7349bf9520e36f2316bf4e5fe488a949e1b786 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 15:26:36 -0500 Subject: [PATCH 09/19] Update otel/transport.go Co-authored-by: Oleksandr Redko --- otel/transport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otel/transport.go b/otel/transport.go index ecef03a742d..b4b106d9001 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -101,7 +101,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { } if resp.StatusCode >= 400 { - span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", resp.StatusCode)) + span.SetStatus(codes.Error, fmt.Sprintf("HTTP %v", resp.StatusCode)) } else { span.SetStatus(codes.Ok, "OK") } From c1c24c20a88395019c305e414adc6fc8ae685648 Mon Sep 17 00:00:00 2001 From: Mohamad Al-Zawahreh Date: Thu, 29 Jan 2026 15:26:46 -0500 Subject: [PATCH 10/19] Update otel/transport.go Co-authored-by: Oleksandr Redko --- otel/transport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otel/transport.go b/otel/transport.go index b4b106d9001..0c502873d1e 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -96,7 +96,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { if reqID := resp.Header.Get("X-Github-Request-Id"); reqID != "" { span.SetAttributes(attribute.String("github.request_id", reqID)) } - if resource := resp.Header.Get("X-Ratelimit-Resource"); resource != "" { + if resource := resp.Header.Get(github.HeaderRateResource); resource != "" { span.SetAttributes(attribute.String("github.rate_limit.resource", resource)) } From a285e4864fcbb98362c4dff5d262f80676605670 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Fri, 30 Jan 2026 22:15:02 -0500 Subject: [PATCH 11/19] fix(otel): export rate limit constants (HeaderRateReset/Resource) to resolve build errors --- github/github.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github/github.go b/github/github.go index b56b9ddd393..490351d1343 100644 --- a/github/github.go +++ b/github/github.go @@ -41,7 +41,7 @@ const ( headerRateRemaining = "X-Ratelimit-Remaining" headerRateUsed = "X-Ratelimit-Used" HeaderRateReset = "X-Ratelimit-Reset" - headerRateResource = "X-Ratelimit-Resource" + HeaderRateResource = "X-Ratelimit-Resource" headerOTP = "X-Github-Otp" headerRetryAfter = "Retry-After" @@ -799,7 +799,7 @@ func parseRate(r *http.Response) Rate { rate.Reset = Timestamp{time.Unix(v, 0)} } } - if resource := r.Header.Get(headerRateResource); resource != "" { + if resource := r.Header.Get(HeaderRateResource); resource != "" { rate.Resource = resource } return rate @@ -820,7 +820,7 @@ func parseSecondaryRate(r *http.Response) *time.Duration { // According to GitHub support, endpoints might return x-ratelimit-reset instead, // as an integer which represents the number of seconds since epoch UTC, // representing the time to resume making requests. - if v := r.Header.Get(headerRateReset); v != "" { + if v := r.Header.Get(HeaderRateReset); v != "" { secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0)) return &retryAfter From cd4386a56548abb5382fa56c04b4bed6abd206a9 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Fri, 30 Jan 2026 22:35:08 -0500 Subject: [PATCH 12/19] fix(tests): update github_test.go to use exported RateLimit constants --- github/github_test.go | 64 +++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/github/github_test.go b/github/github_test.go index b95abc4d7b9..8cffb7f7ef6 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -1233,8 +1233,8 @@ func TestDo_rateLimit(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "59") w.Header().Set(headerRateUsed, "1") - w.Header().Set(headerRateReset, "1372700873") - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, "1372700873") + w.Header().Set(HeaderRateResource, "core") }) req, _ := client.NewRequest("GET", ".", nil) @@ -1352,8 +1352,8 @@ func TestDo_rateLimit_errorResponse(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "59") w.Header().Set(headerRateUsed, "1") - w.Header().Set(headerRateReset, "1372700873") - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, "1372700873") + w.Header().Set(HeaderRateResource, "core") http.Error(w, "Bad Request", 400) }) @@ -1393,8 +1393,8 @@ func TestDo_rateLimit_rateLimitError(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "0") w.Header().Set(headerRateUsed, "60") - w.Header().Set(headerRateReset, "1372700873") - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, "1372700873") + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1443,8 +1443,8 @@ func TestDo_rateLimit_noNetworkCall(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "0") w.Header().Set(headerRateUsed, "60") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1511,8 +1511,8 @@ func TestDo_rateLimit_ignoredFromCache(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "0") w.Header().Set(headerRateUsed, "60") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1559,8 +1559,8 @@ func TestDo_rateLimit_sleepUntilResponseResetLimit(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "0") w.Header().Set(headerRateUsed, "60") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1572,8 +1572,8 @@ func TestDo_rateLimit_sleepUntilResponseResetLimit(t *testing.T) { w.Header().Set(headerRateLimit, "5000") w.Header().Set(headerRateRemaining, "5000") w.Header().Set(headerRateUsed, "0") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, `{}`) @@ -1603,8 +1603,8 @@ func TestDo_rateLimit_sleepUntilResponseResetLimitRetryOnce(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "0") w.Header().Set(headerRateUsed, "60") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1637,8 +1637,8 @@ func TestDo_rateLimit_sleepUntilClientResetLimit(t *testing.T) { w.Header().Set(headerRateLimit, "5000") w.Header().Set(headerRateRemaining, "5000") w.Header().Set(headerRateUsed, "0") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, `{}`) @@ -1670,8 +1670,8 @@ func TestDo_rateLimit_abortSleepContextCancelled(t *testing.T) { w.Header().Set(headerRateLimit, "60") w.Header().Set(headerRateRemaining, "0") w.Header().Set(headerRateUsed, "60") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1705,8 +1705,8 @@ func TestDo_rateLimit_abortSleepContextCancelledClientLimit(t *testing.T) { w.Header().Set(headerRateLimit, "5000") w.Header().Set(headerRateRemaining, "5000") w.Header().Set(headerRateUsed, "0") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, `{}`) @@ -1857,7 +1857,7 @@ func TestDo_rateLimit_abuseRateLimitError_xRateLimitReset(t *testing.T) { mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set(headerRateReset, strconv.Itoa(int(blockUntil))) + w.Header().Set(HeaderRateReset, strconv.Itoa(int(blockUntil))) w.Header().Set(headerRateRemaining, "1") // set remaining to a value > 0 to distinct from a primary rate limit w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1916,7 +1916,7 @@ func TestDo_rateLimit_abuseRateLimitError_maxDuration(t *testing.T) { mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set(headerRateReset, strconv.Itoa(int(blockUntil))) + w.Header().Set(HeaderRateReset, strconv.Itoa(int(blockUntil))) w.Header().Set(headerRateRemaining, "1") // set remaining to a value > 0 to distinct from a primary rate limit w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `{ @@ -1959,8 +1959,8 @@ func TestDo_rateLimit_disableRateLimitCheck(t *testing.T) { w.Header().Set(headerRateLimit, "5000") w.Header().Set(headerRateRemaining, "5000") w.Header().Set(headerRateUsed, "0") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, `{}`) @@ -1995,8 +1995,8 @@ func TestDo_rateLimit_bypassRateLimitCheck(t *testing.T) { w.Header().Set(headerRateLimit, "5000") w.Header().Set(headerRateRemaining, "5000") w.Header().Set(headerRateUsed, "0") - w.Header().Set(headerRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) - w.Header().Set(headerRateResource, "core") + w.Header().Set(HeaderRateReset, fmt.Sprint(reset.Add(time.Hour).Unix())) + w.Header().Set(HeaderRateResource, "core") w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusOK) fmt.Fprintln(w, `{}`) @@ -2138,8 +2138,8 @@ func TestCheckResponse_RateLimit(t *testing.T) { res.Header.Set(headerRateLimit, "60") res.Header.Set(headerRateRemaining, "0") res.Header.Set(headerRateUsed, "1") - res.Header.Set(headerRateReset, "243424") - res.Header.Set(headerRateResource, "core") + res.Header.Set(HeaderRateReset, "243424") + res.Header.Set(HeaderRateResource, "core") var err *RateLimitError errors.As(CheckResponse(res), &err) @@ -2198,8 +2198,8 @@ func TestCheckResponse_RateLimit_TooManyRequests(t *testing.T) { res.Header.Set(headerRateLimit, "60") res.Header.Set(headerRateRemaining, "0") res.Header.Set(headerRateUsed, "60") - res.Header.Set(headerRateReset, "243424") - res.Header.Set(headerRateResource, "core") + res.Header.Set(HeaderRateReset, "243424") + res.Header.Set(HeaderRateResource, "core") var err *RateLimitError errors.As(CheckResponse(res), &err) From d59d91bc775fd1568e570255fcb9e857b69cabc2 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Fri, 30 Jan 2026 22:42:20 -0500 Subject: [PATCH 13/19] fix(otel): resolve deprecation and lint errors in example/otel --- example/otel/main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/example/otel/main.go b/example/otel/main.go index 861e28aa983..680cb813f78 100644 --- a/example/otel/main.go +++ b/example/otel/main.go @@ -46,18 +46,18 @@ func main() { client := github.NewClient(httpClient) // Make a request (Get Rate Limits is public and cheap) - limits, resp, err := client.RateLimits(context.Background()) + limits, resp, err := client.RateLimit.Get(context.Background()) if err != nil { log.Printf("Error fetching rate limits: %v", err) } else { - fmt.Printf("Core Rate Limit: %d/%d (Resets at %v)\n", - limits.GetCore().Remaining, - limits.GetCore().Limit, + fmt.Printf("Core Rate Limit: %v/%v (Resets at %v)\n", + limits.GetCore().Remaining, + limits.GetCore().Limit, limits.GetCore().Reset) } - - // Check if we captured attributes in response - if resp != nil { - fmt.Printf("Response Status: %v\n", resp.Status) - } + + // Check if we captured attributes in response + if resp != nil { + fmt.Printf("Response Status: %v\n", resp.Status) + } } From 490ac420d0a76b224e3374587018b59374e8dee4 Mon Sep 17 00:00:00 2001 From: Mohamad Date: Fri, 30 Jan 2026 23:17:40 -0500 Subject: [PATCH 14/19] fix(otel): add replace directive and fix import grouping --- otel/go.mod | 2 ++ otel/go.sum | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/otel/go.mod b/otel/go.mod index 411aaf4220e..6aa47bc488c 100644 --- a/otel/go.mod +++ b/otel/go.mod @@ -14,3 +14,5 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-querystring v1.2.0 // indirect ) + +replace github.com/google/go-github/v82 => ../ diff --git a/otel/go.sum b/otel/go.sum index 1dcfe9d542e..9c3af3e056f 100644 --- a/otel/go.sum +++ b/otel/go.sum @@ -8,8 +8,6 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v82 v82.0.0 h1:OH09ESON2QwKCUVMYmMcVu1IFKFoaZHwqYaUtr/MVfk= -github.com/google/go-github/v82 v82.0.0/go.mod h1:hQ6Xo0VKfL8RZ7z1hSfB4fvISg0QqHOqe9BP0qo+WvM= github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From fe175fba833634d77f6306d144f0000e1e048a7b Mon Sep 17 00:00:00 2001 From: Mohamad Date: Fri, 30 Jan 2026 23:42:42 -0500 Subject: [PATCH 15/19] fix(otel): resolve lints (header, revive, fmtpercentv, gci) --- otel/transport.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/otel/transport.go b/otel/transport.go index 0c502873d1e..aebd34d78fd 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -1,7 +1,8 @@ -// Copyright 2026 The go-github Authors. All rights reserved. +// Copyright 2026 The go-github AUTHORS. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package otel provides OpenTelemetry instrumentation for the go-github client. package otel import ( @@ -52,7 +53,7 @@ func NewTransport(base http.RoundTripper, opts ...Option) *Transport { // RoundTrip implements http.RoundTripper. func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { ctx := req.Context() - spanName := fmt.Sprintf("github/%s", req.Method) + spanName := fmt.Sprintf("github/%v", req.Method) // Start Span ctx, span := t.Tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient)) From dd7689095833ec48deb9a1d1e4cebb8376fbf8ab Mon Sep 17 00:00:00 2001 From: Mohamad Date: Fri, 30 Jan 2026 23:44:31 -0500 Subject: [PATCH 16/19] trigger: force ci re-run to verify lint fixes From 73b6461079f29ed9b08f163d4bb31f473d7d376a Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:36:49 -0500 Subject: [PATCH 17/19] Apply suggestion from @gmlewis --- otel/transport.go | 1 + 1 file changed, 1 insertion(+) diff --git a/otel/transport.go b/otel/transport.go index aebd34d78fd..0b9e13ec4fe 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -1,4 +1,5 @@ // Copyright 2026 The go-github AUTHORS. All rights reserved. +// // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. From ecc307c0ffaba605b0b62bba390f892d9f7a642c Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:43:48 -0500 Subject: [PATCH 18/19] Apply suggestion from @gmlewis --- otel/transport.go | 1 - 1 file changed, 1 deletion(-) diff --git a/otel/transport.go b/otel/transport.go index 0b9e13ec4fe..39e5f6d3397 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -55,7 +55,6 @@ func NewTransport(base http.RoundTripper, opts ...Option) *Transport { func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { ctx := req.Context() spanName := fmt.Sprintf("github/%v", req.Method) - // Start Span ctx, span := t.Tracer.Start(ctx, spanName, trace.WithSpanKind(trace.SpanKindClient)) defer span.End() From 624e9ab4f388c0bdb95da519f64d499964b38adb Mon Sep 17 00:00:00 2001 From: Glenn Lewis <6598971+gmlewis@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:48:10 -0500 Subject: [PATCH 19/19] Apply suggestion from @gmlewis --- otel/transport.go | 1 - 1 file changed, 1 deletion(-) diff --git a/otel/transport.go b/otel/transport.go index 39e5f6d3397..45a9f69ae73 100644 --- a/otel/transport.go +++ b/otel/transport.go @@ -79,7 +79,6 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { // Capture response attributes span.SetAttributes(attribute.Int("http.status_code", resp.StatusCode)) - // Capture GitHub Specifics if limit := resp.Header.Get("X-Ratelimit-Limit"); limit != "" { if v, err := strconv.Atoi(limit); err == nil {