+2
-13
cmd/app/main.go
+2
-13
cmd/app/main.go
···
2
2
3
3
import (
4
4
"context"
5
-
"errors"
6
5
"os"
7
6
"os/signal"
8
7
9
-
"github.com/Tulkdan/payment-gateway/internal/lib"
10
8
"github.com/Tulkdan/payment-gateway/internal/providers"
11
9
"github.com/Tulkdan/payment-gateway/internal/service"
12
10
"github.com/Tulkdan/payment-gateway/internal/web"
···
23
21
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
24
22
defer stop()
25
23
26
-
otelShutdown, err := lib.SetupOTelSDK(ctx)
27
-
if err != nil {
28
-
return
29
-
}
30
-
31
-
defer func() {
32
-
err = errors.Join(err, otelShutdown(context.Background()))
33
-
}()
34
-
35
24
providers := providers.NewUseProviders([]providers.Provider{
36
25
providers.NewBraintreeProvider(getEnv("BRAINTREE_URL", "localhost:8001")),
37
26
providers.NewStripeProvider(getEnv("STRIPE_URL", "localhost:8002")),
···
48
37
}()
49
38
50
39
select {
51
-
case err = <-srvErr:
40
+
case <-srvErr:
52
41
return
53
42
case <-ctx.Done():
54
43
stop()
55
44
}
56
45
57
-
err = server.Shutdown()
46
+
server.Shutdown()
58
47
}
-20
go.mod
-20
go.mod
···
3
3
go 1.24.4
4
4
5
5
require github.com/google/uuid v1.6.0
6
-
7
-
require (
8
-
github.com/felixge/httpsnoop v1.0.4 // indirect
9
-
github.com/go-logr/logr v1.4.3 // indirect
10
-
github.com/go-logr/stdr v1.2.2 // indirect
11
-
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
12
-
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 // indirect
13
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
14
-
go.opentelemetry.io/otel v1.37.0 // indirect
15
-
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect
16
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 // indirect
17
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 // indirect
18
-
go.opentelemetry.io/otel/log v0.13.0 // indirect
19
-
go.opentelemetry.io/otel/metric v1.37.0 // indirect
20
-
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
21
-
go.opentelemetry.io/otel/sdk/log v0.13.0 // indirect
22
-
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
23
-
go.opentelemetry.io/otel/trace v1.37.0 // indirect
24
-
golang.org/x/sys v0.33.0 // indirect
25
-
)
-35
go.sum
-35
go.sum
···
1
-
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
2
-
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
3
-
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
4
-
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
5
-
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
6
-
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
7
-
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
8
1
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
9
2
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
10
-
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
11
-
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
12
-
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0 h1:lFM7SZo8Ce01RzRfnUFQZEYeWRf/MtOA3A5MobOqk2g=
13
-
go.opentelemetry.io/contrib/bridges/otelslog v0.12.0/go.mod h1:Dw05mhFtrKAYu72Tkb3YBYeQpRUJ4quDgo2DQw3No5A=
14
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
15
-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
16
-
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
17
-
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
18
-
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc=
19
-
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ=
20
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0 h1:6VjV6Et+1Hd2iLZEPtdV7vie80Yyqf7oikJLjQ/myi0=
21
-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.37.0/go.mod h1:u8hcp8ji5gaM/RfcOo8z9NMnf1pVLfVY7lBY2VOGuUU=
22
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0 h1:SNhVp/9q4Go/XHBkQ1/d5u9P/U+L1yaGPoi0x+mStaI=
23
-
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.37.0/go.mod h1:tx8OOlGH6R4kLV67YaYO44GFXloEjGPZuMjEkaaqIp4=
24
-
go.opentelemetry.io/otel/log v0.13.0 h1:yoxRoIZcohB6Xf0lNv9QIyCzQvrtGZklVbdCoyb7dls=
25
-
go.opentelemetry.io/otel/log v0.13.0/go.mod h1:INKfG4k1O9CL25BaM1qLe0zIedOpvlS5Z7XgSbmN83E=
26
-
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
27
-
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
28
-
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
29
-
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
30
-
go.opentelemetry.io/otel/sdk/log v0.13.0 h1:I3CGUszjM926OphK8ZdzF+kLqFvfRY/IIoFq/TjwfaQ=
31
-
go.opentelemetry.io/otel/sdk/log v0.13.0/go.mod h1:lOrQyCCXmpZdN7NchXb6DOZZa1N5G1R2tm5GMMTpDBw=
32
-
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
33
-
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
34
-
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
35
-
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
36
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
37
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-96
internal/lib/openTelemetry.go
-96
internal/lib/openTelemetry.go
···
1
-
package lib
2
-
3
-
import (
4
-
"context"
5
-
"errors"
6
-
"time"
7
-
8
-
"go.opentelemetry.io/otel"
9
-
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
10
-
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
11
-
"go.opentelemetry.io/otel/log/global"
12
-
"go.opentelemetry.io/otel/propagation"
13
-
"go.opentelemetry.io/otel/sdk/log"
14
-
"go.opentelemetry.io/otel/sdk/trace"
15
-
)
16
-
17
-
// setupOTelSDK bootstraps the OpenTelemetry pipeline.
18
-
// If it does not return an error, make sure to call shutdown for proper cleanup.
19
-
func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
20
-
var shutdownFuncs []func(context.Context) error
21
-
22
-
// shutdown calls cleanup functions registered via shutdownFuncs.
23
-
// The errors from the calls are joined.
24
-
// Each registered cleanup will be invoked once.
25
-
shutdown = func(ctx context.Context) error {
26
-
var err error
27
-
for _, fn := range shutdownFuncs {
28
-
err = errors.Join(err, fn(ctx))
29
-
}
30
-
shutdownFuncs = nil
31
-
return err
32
-
}
33
-
34
-
// handleErr calls shutdown for cleanup and makes sure that all errors are returned.
35
-
handleErr := func(inErr error) {
36
-
err = errors.Join(inErr, shutdown(ctx))
37
-
}
38
-
39
-
// Set up propagator.
40
-
prop := newPropagator()
41
-
otel.SetTextMapPropagator(prop)
42
-
43
-
// Set up trace provider.
44
-
tracerProvider, err := newTracerProvider()
45
-
if err != nil {
46
-
handleErr(err)
47
-
return
48
-
}
49
-
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
50
-
otel.SetTracerProvider(tracerProvider)
51
-
52
-
// Set up logger provider.
53
-
loggerProvider, err := newLoggerProvider()
54
-
if err != nil {
55
-
handleErr(err)
56
-
return
57
-
}
58
-
shutdownFuncs = append(shutdownFuncs, loggerProvider.Shutdown)
59
-
global.SetLoggerProvider(loggerProvider)
60
-
61
-
return
62
-
}
63
-
64
-
func newPropagator() propagation.TextMapPropagator {
65
-
return propagation.NewCompositeTextMapPropagator(
66
-
propagation.TraceContext{},
67
-
propagation.Baggage{},
68
-
)
69
-
}
70
-
71
-
func newTracerProvider() (*trace.TracerProvider, error) {
72
-
traceExporter, err := stdouttrace.New(
73
-
stdouttrace.WithPrettyPrint())
74
-
if err != nil {
75
-
return nil, err
76
-
}
77
-
78
-
tracerProvider := trace.NewTracerProvider(
79
-
trace.WithBatcher(traceExporter,
80
-
// Default is 5s. Set to 1s for demonstrative purposes.
81
-
trace.WithBatchTimeout(time.Second)),
82
-
)
83
-
return tracerProvider, nil
84
-
}
85
-
86
-
func newLoggerProvider() (*log.LoggerProvider, error) {
87
-
logExporter, err := stdoutlog.New()
88
-
if err != nil {
89
-
return nil, err
90
-
}
91
-
92
-
loggerProvider := log.NewLoggerProvider(
93
-
log.WithProcessor(log.NewBatchProcessor(logExporter)),
94
-
)
95
-
return loggerProvider, nil
96
-
}
+10
-2
internal/providers/braintree.go
+10
-2
internal/providers/braintree.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"context"
5
6
"encoding/json"
6
7
"net/http"
7
8
···
37
38
PaymentMethod BraintreeChargePaymentMethod `json:"paymentMethod"`
38
39
}
39
40
40
-
func (b BraintreeProvider) Charge(request *domain.Payment) (*domain.Provider, error) {
41
+
func (b BraintreeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
41
42
body := b.createChargeBody(request)
42
-
response, err := http.Post(b.Url+"/charges", "application/json", bytes.NewBuffer(body))
43
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/charges", bytes.NewBuffer(body))
43
44
if err != nil {
44
45
return nil, err
45
46
}
46
47
48
+
req.Header.Set("Content-Type", "application/json")
49
+
req.Header.Set("transaction-id", ctx.Value("request-id").(string))
50
+
51
+
response, err := http.DefaultClient.Do(req)
52
+
if err != nil {
53
+
return nil, err
54
+
}
47
55
return b.responseCharge(response)
48
56
}
49
57
+4
-1
internal/providers/braintree_test.go
+4
-1
internal/providers/braintree_test.go
···
1
1
package providers_test
2
2
3
3
import (
4
+
"context"
4
5
"encoding/json"
5
6
"net/http"
6
7
"net/http/httptest"
···
17
18
t.Run("should make request to url", func(t *testing.T) {
18
19
id, _ := uuid.Parse("2ee70bcb-5cb9-4412-a35f-c2a15fb88ef1")
19
20
cardId, _ := uuid.Parse("ed6ecd4c-81d5-4e63-bb12-99439ae559e7")
21
+
ctx := context.WithValue(t.Context(), "request-id", uuid.New().String())
22
+
20
23
serverResponse := &providers.BraintreeChargeResponse{
21
24
Id: id,
22
25
CreatedAt: time.Now().Format("YYYY-MM-DD"),
···
54
57
}
55
58
56
59
provider := providers.NewBraintreeProvider(server.URL)
57
-
response, err := provider.Charge(charge)
60
+
response, err := provider.Charge(ctx, charge)
58
61
59
62
if err != nil {
60
63
t.Fatalf("got an error but didn't want one %q", err)
+6
-5
internal/providers/provider.go
+6
-5
internal/providers/provider.go
···
1
1
package providers
2
2
3
3
import (
4
+
"context"
4
5
"errors"
5
6
"time"
6
7
···
10
11
var thirtySecondTimout = 30 * time.Second
11
12
12
13
type Provider interface {
13
-
Charge(request *domain.Payment) (*domain.Provider, error)
14
+
Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error)
14
15
}
15
16
16
17
type UseProviders struct {
···
29
30
}
30
31
}
31
32
32
-
func (p *UseProviders) Payment(payment *domain.Payment) (*domain.Provider, error) {
33
+
func (p *UseProviders) Payment(ctx context.Context, payment *domain.Payment) (*domain.Provider, error) {
33
34
var err error = nil
34
35
35
36
for _, provider := range p.providers {
36
37
select {
37
-
case data := <-charge(payment, provider):
38
+
case data := <-charge(ctx, payment, provider):
38
39
return data, nil
39
40
case <-time.After(p.timeout):
40
41
err = errors.New("Timeout")
···
45
46
return nil, err
46
47
}
47
48
48
-
func charge(charge *domain.Payment, provider Provider) chan *domain.Provider {
49
+
func charge(ctx context.Context, charge *domain.Payment, provider Provider) chan *domain.Provider {
49
50
ch := make(chan *domain.Provider)
50
51
51
52
go func() {
52
-
response, err := provider.Charge(charge)
53
+
response, err := provider.Charge(ctx, charge)
53
54
if err != nil {
54
55
close(ch)
55
56
return
+5
-4
internal/providers/provider_test.go
+5
-4
internal/providers/provider_test.go
···
1
1
package providers_test
2
2
3
3
import (
4
+
"context"
4
5
"testing"
5
6
"time"
6
7
···
14
15
Response *domain.Provider
15
16
}
16
17
17
-
func (s *SpyProvider) Charge(request *domain.Payment) (*domain.Provider, error) {
18
+
func (s *SpyProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
18
19
time.Sleep(s.Timeout)
19
20
s.Calls++
20
21
···
29
30
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
30
31
31
32
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 15*time.Millisecond)
32
-
data, err := useProvider.Payment(payment)
33
+
data, err := useProvider.Payment(context.Background(), payment)
33
34
34
35
if err != nil {
35
36
t.Fatal("Got error. didn't want one")
···
50
51
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
51
52
52
53
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 15*time.Millisecond)
53
-
data, err := useProvider.Payment(payment)
54
+
data, err := useProvider.Payment(context.Background(), payment)
54
55
55
56
if err != nil {
56
57
t.Fatal("Got error. didn't want one")
···
71
72
payment, _ := domain.NewPayment(1000, "R$", "", "card", domain.PaymentCard{Number: "", HolderName: "", CVV: "", ExpirationDate: "02/2025", Installments: 1})
72
73
73
74
useProvider := providers.ConfigurableUseProvider([]providers.Provider{spyFirst, spySecond}, 5*time.Millisecond)
74
-
data, err := useProvider.Payment(payment)
75
+
data, err := useProvider.Payment(context.Background(), payment)
75
76
76
77
if data != nil {
77
78
t.Fatalf("Got data but didn't expected one, got %q", data)
+11
-2
internal/providers/stripe.go
+11
-2
internal/providers/stripe.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"context"
5
6
"encoding/json"
6
7
"net/http"
7
8
···
33
34
Card StripeChargeCard `json:"card"`
34
35
}
35
36
36
-
func (b StripeProvider) Charge(request *domain.Payment) (*domain.Provider, error) {
37
+
func (b StripeProvider) Charge(ctx context.Context, request *domain.Payment) (*domain.Provider, error) {
37
38
body := b.createChargeBody(request)
38
-
response, err := http.Post(b.Url+"/transactions", "application/json", bytes.NewBuffer(body))
39
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, b.Url+"/transactions", bytes.NewBuffer(body))
40
+
if err != nil {
41
+
return nil, err
42
+
}
43
+
44
+
req.Header.Set("Content-Type", "application/json")
45
+
req.Header.Set("transaction-id", ctx.Value("request-id").(string))
46
+
47
+
response, err := http.DefaultClient.Do(req)
39
48
if err != nil {
40
49
return nil, err
41
50
}
+4
-1
internal/providers/stripe_test.go
+4
-1
internal/providers/stripe_test.go
···
1
1
package providers_test
2
2
3
3
import (
4
+
"context"
4
5
"encoding/json"
5
6
"net/http"
6
7
"net/http/httptest"
···
17
18
t.Run("should make request to url", func(t *testing.T) {
18
19
id, _ := uuid.Parse("2ee70bcb-5cb9-4412-a35f-c2a15fb88ef1")
19
20
cardId, _ := uuid.Parse("ed6ecd4c-81d5-4e63-bb12-99439ae559e7")
21
+
ctx := context.WithValue(t.Context(), "request-id", uuid.New().String())
22
+
20
23
serverResponse := &providers.StripeChargeResponse{
21
24
Id: id,
22
25
CreatedAt: time.Now().Format("YYYY-MM-DD"),
···
54
57
}
55
58
56
59
provider := providers.NewStripeProvider(server.URL)
57
-
response, err := provider.Charge(charge)
60
+
response, err := provider.Charge(ctx, charge)
58
61
59
62
if err != nil {
60
63
t.Fatalf("got an error but didn't want one %q", err)
+1
-1
internal/service/payment_service.go
+1
-1
internal/service/payment_service.go
+15
internal/web/middleware/transactionId.go
+15
internal/web/middleware/transactionId.go
···
1
+
package middleware
2
+
3
+
import (
4
+
"context"
5
+
"net/http"
6
+
7
+
"github.com/google/uuid"
8
+
)
9
+
10
+
func WithRequestId(next http.HandlerFunc) http.HandlerFunc {
11
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12
+
ctx := context.WithValue(r.Context(), "request-id", uuid.New().String())
13
+
next.ServeHTTP(w, r.WithContext(ctx))
14
+
})
15
+
}
+4
-9
internal/web/server.go
+4
-9
internal/web/server.go
···
8
8
9
9
"github.com/Tulkdan/payment-gateway/internal/service"
10
10
"github.com/Tulkdan/payment-gateway/internal/web/handler"
11
-
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
11
+
"github.com/Tulkdan/payment-gateway/internal/web/middleware"
12
12
)
13
13
14
14
type Server struct {
15
15
port string
16
-
router http.Handler
16
+
router *http.ServeMux
17
17
server *http.Server
18
18
19
19
paymentsService *service.PaymentService
···
29
29
func (s *Server) ConfigureRouter() {
30
30
mux := http.NewServeMux()
31
31
32
-
handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
33
-
handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
34
-
mux.Handle(pattern, handler)
35
-
}
36
-
37
32
paymentsHandler := handler.NewPaymentsHandler(s.paymentsService)
38
33
39
-
handleFunc("POST /payments", paymentsHandler.Create)
34
+
mux.HandleFunc("POST /payments", middleware.WithRequestId(paymentsHandler.Create))
40
35
// r.HandleFunc("POST /refunds", func(http.ResponseWriter, *http.Request) {})
41
36
// r.HandleFunc("GET /payments/{id}", func(w http.ResponseWriter, r *http.Request) {
42
37
// id := r.PathValue("id")
43
38
// })
44
39
45
-
s.router = otelhttp.NewHandler(mux, "/")
40
+
s.router = mux
46
41
}
47
42
48
43
func (s *Server) Start(ctx context.Context) error {