Skip to content

Commit

Permalink
feat: request context setup (#300)
Browse files Browse the repository at this point in the history
  • Loading branch information
sethyates committed Sep 12, 2023
1 parent 292f4d2 commit 767d88b
Show file tree
Hide file tree
Showing 16 changed files with 514 additions and 222 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module go.ketch.com/lib/orlop/v2
go 1.20

require (
github.com/google/uuid v1.3.0
github.com/joho/godotenv v1.5.1
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Expand Down
39 changes: 39 additions & 0 deletions log/publisher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package log

import (
"context"
"encoding/json"
"github.com/sirupsen/logrus"
)

// Publisher is an implementation of a Publisher using Log
type Publisher struct{}

// NewPublisher returns a new Publisher
func NewPublisher() *Publisher {
return &Publisher{}
}

func (p *Publisher) PublishEvent(ctx context.Context, subject string, event any) error {
if event == nil {
return nil
}

fields := make(logrus.Fields)
b, err := json.Marshal(event)
if err != nil {
return err
}

err = json.Unmarshal(b, &fields)
if err != nil {
return err
}

WithContext(ctx).WithFields(logrus.Fields{
"subject": subject,
"data": fields,
}).Info("published")

return nil
}
25 changes: 25 additions & 0 deletions log/publisher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package log_test

import (
"context"
"github.com/stretchr/testify/require"
"go.ketch.com/lib/orlop/v2/log"
"testing"
)

type SomeEvent struct {
ID string
Name string
}

func TestLog(t *testing.T) {
ctx := context.Background()

pub := log.NewPublisher()

err := pub.PublishEvent(ctx, "SomeEvent", &SomeEvent{
ID: "123",
Name: "one two three",
})
require.NoError(t, err)
}
35 changes: 35 additions & 0 deletions request/connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package request

import (
"context"
"go.ketch.com/lib/orlop/v2/errors"
)

// WithConnection returns a new context with the given request Connection
func WithConnection(parent context.Context, connectionID string) context.Context {
if len(connectionID) == 0 {
return parent
}

return WithValue(parent, ConnectionKey, connectionID)
}

// WithConnectionOption returns a function that sets the given Connection on a context
func WithConnectionOption(connection string) func(ctx context.Context) context.Context {
return func(ctx context.Context) context.Context {
if len(Connection(ctx)) == 0 {
ctx = WithConnection(ctx, connection)
}
return ctx
}
}

// Connection returns the request ID or an empty string
func Connection(ctx Context) string {
return Value[string](ctx, ConnectionKey)
}

// RequireConnection returns the request ID or an error if not set
func RequireConnection(ctx Context) (string, error) {
return RequireValue[string](ctx, ConnectionKey, errors.Invalid)
}
241 changes: 19 additions & 222 deletions request/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ package request

import (
"context"
"strings"
"github.com/google/uuid"
"time"

"go.ketch.com/lib/orlop/v2/errors"
)

// Key is the key of a request property in context
Expand Down Expand Up @@ -109,235 +107,34 @@ type Context interface {
Value(key any) any
}

// Value returns the value of a request context key, or a default value if the value is not set
func Value[T any](ctx Context, key Key) T {
if v := ctx.Value(key); v != nil {
if r, ok := v.(T); ok {
return r
}
}

var v T
return v
}
// Clone clones the given context into a blank context
func Clone(ctx context.Context) context.Context {
newCtx := context.Background()

// RequireValue returns the value of a request context key or returns an error if the value is not set
func RequireValue[T any](ctx Context, key Key, errDecorator func(error) error) (T, error) {
if v := ctx.Value(key); v != nil {
if r, ok := v.(T); ok {
return r, nil
}
for _, k := range AllKeys {
newCtx = context.WithValue(newCtx, k, ctx.Value(k))
}

var v T
return v, errDecorator(errors.Errorf("%v not specified", key))
}

// WithValue returns a new context with the given request value
func WithValue[T any](parent context.Context, key Key, v T) context.Context {
return context.WithValue(parent, key, v)
}

// WithConnection returns a new context with the given request Connection
func WithConnection(parent context.Context, connectionID string) context.Context {
return WithValue(parent, ConnectionKey, connectionID)
}

// Connection returns the request ID or an empty string
func Connection(ctx Context) string {
return Value[string](ctx, ConnectionKey)
}

// RequireConnection returns the request ID or an error if not set
func RequireConnection(ctx Context) (string, error) {
return RequireValue[string](ctx, ConnectionKey, errors.Invalid)
}

// WithIntegration returns a new context with the given request integration
func WithIntegration(parent context.Context, integration string) context.Context {
return WithValue(parent, IntegrationKey, integration)
}

// Integration returns the request ID or an empty string
func Integration(ctx Context) string {
return Value[string](ctx, IntegrationKey)
}

// RequireIntegration returns the request ID or an error if not set
func RequireIntegration(ctx Context) (string, error) {
return RequireValue[string](ctx, IntegrationKey, errors.Invalid)
}

// WithID returns a new context with the given request ID
func WithID(parent context.Context, requestID string) context.Context {
return WithValue(parent, IDKey, requestID)
}

// ID returns the request ID or an empty string
func ID(ctx Context) string {
return Value[string](ctx, IDKey)
}

// RequireID returns the request ID or an error if not set
func RequireID(ctx Context) (string, error) {
return RequireValue[string](ctx, IDKey, errors.Invalid)
}

// WithUser returns a new context with the given user ID
func WithUser(parent context.Context, userID string) context.Context {
return WithValue(parent, UserKey, userID)
}

// User returns the User ID or an empty string
func User(ctx Context) string {
return Value[string](ctx, UserKey)
}

// RequireUser returns the request User or an error if not set
func RequireUser(ctx Context) (string, error) {
return RequireValue[string](ctx, UserKey, errors.Forbidden)
}

// Originator returns the request originator or an empty string
func Originator(ctx Context) string {
return Value[string](ctx, OriginatorKey)
}

// RequireOriginator returns the request originator or an error if not set
func RequireOriginator(ctx Context) (string, error) {
return RequireValue[string](ctx, OriginatorKey, errors.Invalid)
}

// WithOriginator returns a new context with the given request originator
func WithOriginator(parent context.Context, originator string) context.Context {
return WithValue(parent, OriginatorKey, originator)
}

// URL returns the request URL or an empty string
func URL(ctx Context) string {
return Value[string](ctx, URLKey)
}

// RequireURL returns the request URL or an error if not set
func RequireURL(ctx Context) (string, error) {
return RequireValue[string](ctx, URLKey, errors.Invalid)
}

// WithURL returns a new context with the given request URL
func WithURL(parent context.Context, requestURL string) context.Context {
return WithValue(parent, URLKey, requestURL)
}

// Timestamp returns the request timestamp or an empty time.Time
func Timestamp(ctx Context) time.Time {
return Value[time.Time](ctx, TimestampKey)
}

// RequireTimestamp returns the request timestamp or an error if not set
func RequireTimestamp(ctx Context) (time.Time, error) {
return RequireValue[time.Time](ctx, TimestampKey, errors.Invalid)
}

// WithTimestamp returns a new context with the given request timestamp
func WithTimestamp(parent context.Context, requestTimestamp time.Time) context.Context {
return WithValue(parent, TimestampKey, requestTimestamp)
}

// Tenant returns the request Tenant or an empty string
func Tenant(ctx Context) string {
tenant := Value[string](ctx, TenantKey)
parts := strings.Split(tenant, ";")
return parts[0]
}

// RequireTenant returns the request Tenant or an error if not set
func RequireTenant(ctx Context) (string, error) {
return RequireValue[string](ctx, TenantKey, errors.Forbidden)
}

// WithTenant returns a new context with the given request tenant
func WithTenant(parent context.Context, requestTenant string) context.Context {
return WithValue(parent, TenantKey, requestTenant)
}

// Operation returns the Operation or an empty string
func Operation(ctx Context) string {
return Value[string](ctx, OperationKey)
}

// RequireOperation returns the Operation or an error if not set
func RequireOperation(ctx Context) (string, error) {
return RequireValue[string](ctx, OperationKey, errors.Invalid)
}

// WithOperation returns a new context with the given Operation
func WithOperation(parent context.Context, operation string) context.Context {
return WithValue(parent, OperationKey, operation)
return newCtx
}

// Values returns a map of the request values from the context
func Values(ctx Context, opts ...Option) map[string]string {
var o options

for _, opt := range opts {
opt(&o)
}

out := make(map[string]string)

for k, getter := range Getters {
if s := getter(ctx); len(s) > 0 {
skip := false

for _, filter := range o.filters {
if !filter(k) {
skip = true
}
}

if !skip {
out[string(k)] = s
}
// Setup sets up a context with the given options
func Setup(ctx context.Context, opts ...func(ctx context.Context) context.Context) context.Context {
// Check to see if we have a request ID and if not, populate it and some other of the request fields
if len(ID(ctx)) == 0 {
u, err := uuid.NewRandom()
if err == nil {
ctx = WithID(ctx, u.String())
}
}

return out
}

// Option is a function that sets values on the options structure
type Option func(o *options)

type options struct {
filters []Filter
}

// Filter is a function that returns true if the given key should be included
type Filter func(k Key) bool

// WithFilter returns a filter that filters request values
func WithFilter(f Filter) Option {
return func(o *options) {
o.filters = append(o.filters, f)
}
}

// SkipHighCardinalityKeysFilter returns true if the key is low cardinality
func SkipHighCardinalityKeysFilter(k Key) bool {
if v, ok := LowCardinalityKeys[k]; ok {
return v
if Timestamp(ctx).IsZero() {
ctx = WithTimestamp(ctx, time.Now().UTC())
}

// If we don't know about the key, assume it is "high cardinality"
return false
}

func Clone(ctx context.Context) context.Context {
newCtx := context.Background()

for _, k := range AllKeys {
v := ctx.Value(k)
newCtx = context.WithValue(newCtx, k, v)
for _, opt := range opts {
ctx = opt(ctx)
}

return newCtx
return ctx
}

0 comments on commit 767d88b

Please sign in to comment.