package service import ( "context" "reflect" "regexp" "time" "gitlab.com/uafrica/go-utils/audit" "gitlab.com/uafrica/go-utils/errors" "gitlab.com/uafrica/go-utils/logger" "gitlab.com/uafrica/go-utils/string_utils" ) // Ctx stores lambda-wide context e.g. claims, request ID etc. var Ctx Context type Context interface { context.Context logger.Logger Producer audit.Auditor RequestID() string MillisecondsSinceStart() int64 StartTime() time.Time //set claim values - things that cannot change - typically UserID, AccountID, Username, ... //the fieldName must be public, i.e. start with uppercase A-Z, no underscores, its not a tag name :-) //once set, it will override any field in params/body struct recursively with this Golang field name (before validation) //Set will fail if the value is already set on this context, i.e. the value cannot change //Call this in your app Start(), before params/body is extracted //value may only be of type int64 or string for now... ClaimSet(fieldName string, value interface{}) error //Get can retrieve any claim value ClaimGet(fieldName string) (interface{}, bool) //Claim() return all values so you can iterate over them, but not change them... //note: a context.Context also support its own set of values, which you can use as you like //but the idea is that you use this instead, which we apply to params/body Claim() map[string]interface{} //context data (names must be snake_case) //unlike claim, any type of value may be stored //but like claims, value can never change WithValue(name string, value interface{}) Context Set(name string, value interface{}) error Get(name string) (interface{}, bool) //Value(name string) interface{} //return nil if not set, inherited from context.Context and overloaded to retrieve local first, just like Get() ValueOrDefault(name string, defaultValue interface{}) interface{} Data() map[string]interface{} //write an audit event AuditChange(eventType string, orgValue, newValue interface{}) } //values: are added to context and logger //these values are logged for every log event in this context //values can be added later using with value, but won't be logged // they are just for retrieval between unrelated packages, e.g. // authentication may set the user_id etc... and other package may retrieve it but not change it type valueKey string func (s service) NewContext(base context.Context, requestID string, values map[string]interface{}) (Context, error) { if values == nil { values = map[string]interface{}{} } values["request-id"] = requestID for n, v := range values { base = context.WithValue(base, valueKey(n), v) } l := logger.New().WithFields(values) l.IFormatter = l.IFormatter.NextColor() Ctx = &serviceContext{ Context: base, Logger: l, Producer: s.Producer, Auditor: s.Auditor, startTime: time.Now(), requestID: requestID, data: map[string]interface{}{}, claim: map[string]interface{}{}, } for starterName, starter := range s.starters { var starterData interface{} starterData, err := starter.Start(Ctx) if err != nil { Ctx.Errorf("Start(%s) failed: %+v ...", starterName, err) return nil, errors.Wrapf(err, "%s", starterName) } if err = Ctx.Set(starterName, starterData); err != nil { Ctx.Errorf("Start(%s) failed to set (%T)%+v: %+v ...", starterName, starterData, starterData, err) return nil, errors.Wrapf(err, "failed to set starter(%s) data=(%T)%+v", starterName, starterData, starterData) } Ctx.Debugf("Start(%s)=(%T)%+v", starterName, starterData, starterData) } return Ctx, nil } type serviceContext struct { context.Context logger.Logger Producer audit.Auditor startTime time.Time requestID string claim map[string]interface{} data map[string]interface{} } func (ctx serviceContext) RequestID() string { return ctx.requestID } const claimFieldNamePattern = `[A-Z][a-zA-Z0-9]*` var claimFieldNameRegex = regexp.MustCompile("^" + claimFieldNamePattern + "$") func (ctx *serviceContext) ClaimSet(fieldName string, value interface{}) error { if !claimFieldNameRegex.MatchString(fieldName) { return errors.Errorf("invalid claim field name \"%s\"", fieldName) } if oldValue, exists := ctx.claim[fieldName]; exists { return errors.Errorf("ClaimSet(%s=(%T)%v) failed because already set to (%T)%v", fieldName, value, value, oldValue, oldValue) } switch reflect.TypeOf(value).Kind() { case reflect.Int64: case reflect.String: default: panic(errors.Errorf("claim(%s)=(%T)%v is neither sting nor int64", fieldName, value, value)) } ctx.claim[fieldName] = value return nil } func (ctx serviceContext) ClaimGet(fieldName string) (interface{}, bool) { if cv, ok := ctx.claim[fieldName]; ok { return cv, true } return nil, false } func (ctx serviceContext) Claim() map[string]interface{} { claim := map[string]interface{}{} for n, v := range ctx.claim { claim[n] = v } return claim } func (ctx *serviceContext) WithValue(name string, value interface{}) Context { ctx.Context = context.WithValue(ctx.Context, valueKey(name), value) return ctx } func (ctx *serviceContext) Set(name string, value interface{}) error { if !string_utils.IsSnakeCase(name) { return errors.Errorf("invalid context value name \"%s\"", name) } if oldValue, exists := ctx.data[name]; exists { return errors.Errorf("Set(%s=(%T)%v) failed because already set to (%T)%v", name, value, value, oldValue, oldValue) } ctx.data[name] = value return nil } func (ctx *serviceContext) Get(name string) (interface{}, bool) { if cv, ok := ctx.data[name]; ok { return cv, true } //alternative: try value from context.Context if cv := ctx.Context.Value(name); cv != nil { return cv, true } return nil, false } //Value override context.Context.Value to retrieve first from our own data func (ctx *serviceContext) Value(key interface{}) interface{} { if name, ok := key.(string); ok { if cv, ok := ctx.data[name]; ok { return cv } } //alternative: try value from context.Context if cv := ctx.Context.Value(key); cv != nil { return cv } return nil } func (ctx *serviceContext) Data() map[string]interface{} { data := map[string]interface{}{} for n, v := range ctx.data { data[n] = v } return data } func (ctx *serviceContext) MillisecondsSinceStart() int64 { return time.Since(ctx.startTime).Milliseconds() } func (ctx *serviceContext) StartTime() time.Time { return ctx.startTime } func (ctx *serviceContext) ValueOrDefault(name string, defaultValue interface{}) interface{} { if value := ctx.Value(valueKey(name)); value != nil { return value } return defaultValue } func (ctx *serviceContext) AuditChange(eventType string, orgValue, newValue interface{}) { username, _ := ctx.Claim()["username"].(string) event, err := audit.NewEvent( username, //use username as source (will default to "SYSTEM" if undefined) eventType, orgValue, newValue, ) if err != nil { ctx.Errorf("failed to define audit event: %+v", err) return } if err := ctx.Auditor.WriteEvent(ctx.requestID, event); err != nil { ctx.Errorf("failed to audit change: %+v", err) } }