diff --git a/database/migrations/014_request_traces.up.sql b/database/migrations/014_request_traces.up.sql new file mode 100644 index 00000000..cad2ac63 --- /dev/null +++ b/database/migrations/014_request_traces.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE getstronger.traces +( + id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + request TEXT NOT NULL, + status_code INT NOT NULL, + duration_ms INT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT (NOW() AT TIME ZONE 'UTC') +); diff --git a/server/cmd/main.go b/server/cmd/main.go index 6bf7f892..2e7c13c8 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -13,6 +13,7 @@ import ( "github.com/crlssn/getstronger/server/pkg/db" "github.com/crlssn/getstronger/server/pkg/jwt" "github.com/crlssn/getstronger/server/pkg/repo" + "github.com/crlssn/getstronger/server/pkg/trace" "github.com/crlssn/getstronger/server/rpc" ) @@ -33,6 +34,7 @@ func options() []fx.Option { zap.NewDevelopment, repo.New, grpc.NewServer, + trace.NewTracer, config.New, protovalidate.New, ), diff --git a/server/pkg/orm/boil_table_names.go b/server/pkg/orm/boil_table_names.go index 304795fb..e4a7d00b 100644 --- a/server/pkg/orm/boil_table_names.go +++ b/server/pkg/orm/boil_table_names.go @@ -10,6 +10,7 @@ var TableNames = struct { Followers string Routines string Sets string + Traces string Users string WorkoutComments string Workouts string @@ -20,6 +21,7 @@ var TableNames = struct { Followers: "followers", Routines: "routines", Sets: "sets", + Traces: "traces", Users: "users", WorkoutComments: "workout_comments", Workouts: "workouts", diff --git a/server/pkg/orm/traces.go b/server/pkg/orm/traces.go new file mode 100644 index 00000000..5f97bc68 --- /dev/null +++ b/server/pkg/orm/traces.go @@ -0,0 +1,963 @@ +// Code generated by SQLBoiler 4.17.1 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package orm + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/friendsofgo/errors" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + "github.com/volatiletech/sqlboiler/v4/queries/qmhelper" + "github.com/volatiletech/strmangle" +) + +// Trace is an object representing the database table. +type Trace struct { + ID string `boil:"id" json:"id" toml:"id" yaml:"id"` + Request string `boil:"request" json:"request" toml:"request" yaml:"request"` + DurationMS int `boil:"duration_ms" json:"duration_ms" toml:"duration_ms" yaml:"duration_ms"` + StatusCode int `boil:"status_code" json:"status_code" toml:"status_code" yaml:"status_code"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + + R *traceR `boil:"-" json:"-" toml:"-" yaml:"-"` + L traceL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var TraceColumns = struct { + ID string + Request string + DurationMS string + StatusCode string + CreatedAt string +}{ + ID: "id", + Request: "request", + DurationMS: "duration_ms", + StatusCode: "status_code", + CreatedAt: "created_at", +} + +var TraceTableColumns = struct { + ID string + Request string + DurationMS string + StatusCode string + CreatedAt string +}{ + ID: "traces.id", + Request: "traces.request", + DurationMS: "traces.duration_ms", + StatusCode: "traces.status_code", + CreatedAt: "traces.created_at", +} + +// Generated where + +var TraceWhere = struct { + ID whereHelperstring + Request whereHelperstring + DurationMS whereHelperint + StatusCode whereHelperint + CreatedAt whereHelpertime_Time +}{ + ID: whereHelperstring{field: "\"getstronger\".\"traces\".\"id\""}, + Request: whereHelperstring{field: "\"getstronger\".\"traces\".\"request\""}, + DurationMS: whereHelperint{field: "\"getstronger\".\"traces\".\"duration_ms\""}, + StatusCode: whereHelperint{field: "\"getstronger\".\"traces\".\"status_code\""}, + CreatedAt: whereHelpertime_Time{field: "\"getstronger\".\"traces\".\"created_at\""}, +} + +// TraceRels is where relationship names are stored. +var TraceRels = struct { +}{} + +// traceR is where relationships are stored. +type traceR struct { +} + +// NewStruct creates a new relationship struct +func (*traceR) NewStruct() *traceR { + return &traceR{} +} + +// traceL is where Load methods for each relationship are stored. +type traceL struct{} + +var ( + traceAllColumns = []string{"id", "request", "duration_ms", "status_code", "created_at"} + traceColumnsWithoutDefault = []string{"request", "duration_ms", "status_code"} + traceColumnsWithDefault = []string{"id", "created_at"} + tracePrimaryKeyColumns = []string{"id"} + traceGeneratedColumns = []string{} +) + +type ( + // TraceSlice is an alias for a slice of pointers to Trace. + // This should almost always be used instead of []Trace. + TraceSlice []*Trace + // TraceHook is the signature for custom Trace hook methods + TraceHook func(context.Context, boil.ContextExecutor, *Trace) error + + traceQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + traceType = reflect.TypeOf(&Trace{}) + traceMapping = queries.MakeStructMapping(traceType) + tracePrimaryKeyMapping, _ = queries.BindMapping(traceType, traceMapping, tracePrimaryKeyColumns) + traceInsertCacheMut sync.RWMutex + traceInsertCache = make(map[string]insertCache) + traceUpdateCacheMut sync.RWMutex + traceUpdateCache = make(map[string]updateCache) + traceUpsertCacheMut sync.RWMutex + traceUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +var traceAfterSelectMu sync.Mutex +var traceAfterSelectHooks []TraceHook + +var traceBeforeInsertMu sync.Mutex +var traceBeforeInsertHooks []TraceHook +var traceAfterInsertMu sync.Mutex +var traceAfterInsertHooks []TraceHook + +var traceBeforeUpdateMu sync.Mutex +var traceBeforeUpdateHooks []TraceHook +var traceAfterUpdateMu sync.Mutex +var traceAfterUpdateHooks []TraceHook + +var traceBeforeDeleteMu sync.Mutex +var traceBeforeDeleteHooks []TraceHook +var traceAfterDeleteMu sync.Mutex +var traceAfterDeleteHooks []TraceHook + +var traceBeforeUpsertMu sync.Mutex +var traceBeforeUpsertHooks []TraceHook +var traceAfterUpsertMu sync.Mutex +var traceAfterUpsertHooks []TraceHook + +// doAfterSelectHooks executes all "after Select" hooks. +func (o *Trace) doAfterSelectHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceAfterSelectHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeInsertHooks executes all "before insert" hooks. +func (o *Trace) doBeforeInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceBeforeInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterInsertHooks executes all "after Insert" hooks. +func (o *Trace) doAfterInsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceAfterInsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpdateHooks executes all "before Update" hooks. +func (o *Trace) doBeforeUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceBeforeUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpdateHooks executes all "after Update" hooks. +func (o *Trace) doAfterUpdateHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceAfterUpdateHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeDeleteHooks executes all "before Delete" hooks. +func (o *Trace) doBeforeDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceBeforeDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterDeleteHooks executes all "after Delete" hooks. +func (o *Trace) doAfterDeleteHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceAfterDeleteHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doBeforeUpsertHooks executes all "before Upsert" hooks. +func (o *Trace) doBeforeUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceBeforeUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// doAfterUpsertHooks executes all "after Upsert" hooks. +func (o *Trace) doAfterUpsertHooks(ctx context.Context, exec boil.ContextExecutor) (err error) { + if boil.HooksAreSkipped(ctx) { + return nil + } + + for _, hook := range traceAfterUpsertHooks { + if err := hook(ctx, exec, o); err != nil { + return err + } + } + + return nil +} + +// AddTraceHook registers your hook function for all future operations. +func AddTraceHook(hookPoint boil.HookPoint, traceHook TraceHook) { + switch hookPoint { + case boil.AfterSelectHook: + traceAfterSelectMu.Lock() + traceAfterSelectHooks = append(traceAfterSelectHooks, traceHook) + traceAfterSelectMu.Unlock() + case boil.BeforeInsertHook: + traceBeforeInsertMu.Lock() + traceBeforeInsertHooks = append(traceBeforeInsertHooks, traceHook) + traceBeforeInsertMu.Unlock() + case boil.AfterInsertHook: + traceAfterInsertMu.Lock() + traceAfterInsertHooks = append(traceAfterInsertHooks, traceHook) + traceAfterInsertMu.Unlock() + case boil.BeforeUpdateHook: + traceBeforeUpdateMu.Lock() + traceBeforeUpdateHooks = append(traceBeforeUpdateHooks, traceHook) + traceBeforeUpdateMu.Unlock() + case boil.AfterUpdateHook: + traceAfterUpdateMu.Lock() + traceAfterUpdateHooks = append(traceAfterUpdateHooks, traceHook) + traceAfterUpdateMu.Unlock() + case boil.BeforeDeleteHook: + traceBeforeDeleteMu.Lock() + traceBeforeDeleteHooks = append(traceBeforeDeleteHooks, traceHook) + traceBeforeDeleteMu.Unlock() + case boil.AfterDeleteHook: + traceAfterDeleteMu.Lock() + traceAfterDeleteHooks = append(traceAfterDeleteHooks, traceHook) + traceAfterDeleteMu.Unlock() + case boil.BeforeUpsertHook: + traceBeforeUpsertMu.Lock() + traceBeforeUpsertHooks = append(traceBeforeUpsertHooks, traceHook) + traceBeforeUpsertMu.Unlock() + case boil.AfterUpsertHook: + traceAfterUpsertMu.Lock() + traceAfterUpsertHooks = append(traceAfterUpsertHooks, traceHook) + traceAfterUpsertMu.Unlock() + } +} + +// One returns a single trace record from the query. +func (q traceQuery) One(ctx context.Context, exec boil.ContextExecutor) (*Trace, error) { + o := &Trace{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "orm: failed to execute a one query for traces") + } + + if err := o.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + + return o, nil +} + +// All returns all Trace records from the query. +func (q traceQuery) All(ctx context.Context, exec boil.ContextExecutor) (TraceSlice, error) { + var o []*Trace + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "orm: failed to assign all query results to Trace slice") + } + + if len(traceAfterSelectHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterSelectHooks(ctx, exec); err != nil { + return o, err + } + } + } + + return o, nil +} + +// Count returns the count of all Trace records in the query. +func (q traceQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "orm: failed to count traces rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q traceQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "orm: failed to check if traces exists") + } + + return count > 0, nil +} + +// Traces retrieves all the records using an executor. +func Traces(mods ...qm.QueryMod) traceQuery { + mods = append(mods, qm.From("\"getstronger\".\"traces\"")) + q := NewQuery(mods...) + if len(queries.GetSelect(q)) == 0 { + queries.SetSelect(q, []string{"\"getstronger\".\"traces\".*"}) + } + + return traceQuery{q} +} + +// FindTrace retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindTrace(ctx context.Context, exec boil.ContextExecutor, iD string, selectCols ...string) (*Trace, error) { + traceObj := &Trace{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"getstronger\".\"traces\" where \"id\"=$1", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, traceObj) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "orm: unable to select from traces") + } + + if err = traceObj.doAfterSelectHooks(ctx, exec); err != nil { + return traceObj, err + } + + return traceObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *Trace) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("orm: no traces provided for insertion") + } + + var err error + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + } + + if err := o.doBeforeInsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(traceColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + traceInsertCacheMut.RLock() + cache, cached := traceInsertCache[key] + traceInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + traceAllColumns, + traceColumnsWithDefault, + traceColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(traceType, traceMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(traceType, traceMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"getstronger\".\"traces\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"getstronger\".\"traces\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + queryReturning = fmt.Sprintf(" RETURNING \"%s\"", strings.Join(returnColumns, "\",\"")) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + + if err != nil { + return errors.Wrap(err, "orm: unable to insert into traces") + } + + if !cached { + traceInsertCacheMut.Lock() + traceInsertCache[key] = cache + traceInsertCacheMut.Unlock() + } + + return o.doAfterInsertHooks(ctx, exec) +} + +// Update uses an executor to update the Trace. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *Trace) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + var err error + if err = o.doBeforeUpdateHooks(ctx, exec); err != nil { + return 0, err + } + key := makeCacheKey(columns, nil) + traceUpdateCacheMut.RLock() + cache, cached := traceUpdateCache[key] + traceUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + traceAllColumns, + tracePrimaryKeyColumns, + ) + + if !columns.IsWhitelist() { + wl = strmangle.SetComplement(wl, []string{"created_at"}) + } + if len(wl) == 0 { + return 0, errors.New("orm: unable to update traces, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"getstronger\".\"traces\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, wl), + strmangle.WhereClause("\"", "\"", len(wl)+1, tracePrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(traceType, traceMapping, append(wl, tracePrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, values) + } + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to update traces row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by update for traces") + } + + if !cached { + traceUpdateCacheMut.Lock() + traceUpdateCache[key] = cache + traceUpdateCacheMut.Unlock() + } + + return rowsAff, o.doAfterUpdateHooks(ctx, exec) +} + +// UpdateAll updates all rows with the specified column values. +func (q traceQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to update all for traces") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: unable to retrieve rows affected for traces") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o TraceSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("orm: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), tracePrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"getstronger\".\"traces\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), len(colNames)+1, tracePrimaryKeyColumns, len(o))) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to update all in trace slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: unable to retrieve rows affected all in update all trace") + } + return rowsAff, nil +} + +// Upsert attempts an insert using an executor, and does an update or ignore on conflict. +// See boil.Columns documentation for how to properly use updateColumns and insertColumns. +func (o *Trace) Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns, opts ...UpsertOptionFunc) error { + if o == nil { + return errors.New("orm: no traces provided for upsert") + } + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + } + + if err := o.doBeforeUpsertHooks(ctx, exec); err != nil { + return err + } + + nzDefaults := queries.NonZeroDefaultSet(traceColumnsWithDefault, o) + + // Build cache key in-line uglily - mysql vs psql problems + buf := strmangle.GetBuffer() + if updateOnConflict { + buf.WriteByte('t') + } else { + buf.WriteByte('f') + } + buf.WriteByte('.') + for _, c := range conflictColumns { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(updateColumns.Kind)) + for _, c := range updateColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(insertColumns.Kind)) + for _, c := range insertColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + for _, c := range nzDefaults { + buf.WriteString(c) + } + key := buf.String() + strmangle.PutBuffer(buf) + + traceUpsertCacheMut.RLock() + cache, cached := traceUpsertCache[key] + traceUpsertCacheMut.RUnlock() + + var err error + + if !cached { + insert, _ := insertColumns.InsertColumnSet( + traceAllColumns, + traceColumnsWithDefault, + traceColumnsWithoutDefault, + nzDefaults, + ) + + update := updateColumns.UpdateColumnSet( + traceAllColumns, + tracePrimaryKeyColumns, + ) + + if updateOnConflict && len(update) == 0 { + return errors.New("orm: unable to upsert traces, could not build update column list") + } + + ret := strmangle.SetComplement(traceAllColumns, strmangle.SetIntersect(insert, update)) + + conflict := conflictColumns + if len(conflict) == 0 && updateOnConflict && len(update) != 0 { + if len(tracePrimaryKeyColumns) == 0 { + return errors.New("orm: unable to upsert traces, could not build conflict column list") + } + + conflict = make([]string, len(tracePrimaryKeyColumns)) + copy(conflict, tracePrimaryKeyColumns) + } + cache.query = buildUpsertQueryPostgres(dialect, "\"getstronger\".\"traces\"", updateOnConflict, ret, update, conflict, insert, opts...) + + cache.valueMapping, err = queries.BindMapping(traceType, traceMapping, insert) + if err != nil { + return err + } + if len(ret) != 0 { + cache.retMapping, err = queries.BindMapping(traceType, traceMapping, ret) + if err != nil { + return err + } + } + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + var returns []interface{} + if len(cache.retMapping) != 0 { + returns = queries.PtrsFromMapping(value, cache.retMapping) + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(returns...) + if errors.Is(err, sql.ErrNoRows) { + err = nil // Postgres doesn't return anything when there's no update + } + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + if err != nil { + return errors.Wrap(err, "orm: unable to upsert traces") + } + + if !cached { + traceUpsertCacheMut.Lock() + traceUpsertCache[key] = cache + traceUpsertCacheMut.Unlock() + } + + return o.doAfterUpsertHooks(ctx, exec) +} + +// Delete deletes a single Trace record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *Trace) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("orm: no Trace provided for delete") + } + + if err := o.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), tracePrimaryKeyMapping) + sql := "DELETE FROM \"getstronger\".\"traces\" WHERE \"id\"=$1" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to delete from traces") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by delete for traces") + } + + if err := o.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q traceQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("orm: no traceQuery provided for delete all") + } + + queries.SetDelete(q.Query) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to delete all from traces") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by deleteall for traces") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o TraceSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + if len(traceBeforeDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doBeforeDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + var args []interface{} + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), tracePrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"getstronger\".\"traces\" WHERE " + + strmangle.WhereInClause(string(dialect.LQ), string(dialect.RQ), 1, tracePrimaryKeyColumns, len(o)) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "orm: unable to delete all from trace slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "orm: failed to get rows affected by deleteall for traces") + } + + if len(traceAfterDeleteHooks) != 0 { + for _, obj := range o { + if err := obj.doAfterDeleteHooks(ctx, exec); err != nil { + return 0, err + } + } + } + + return rowsAff, nil +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *Trace) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindTrace(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *TraceSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := TraceSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), tracePrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"getstronger\".\"traces\".* FROM \"getstronger\".\"traces\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, tracePrimaryKeyColumns, len(*o)) + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "orm: unable to reload all in TraceSlice") + } + + *o = slice + + return nil +} + +// TraceExists checks if the Trace row exists. +func TraceExists(ctx context.Context, exec boil.ContextExecutor, iD string) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"getstronger\".\"traces\" where \"id\"=$1 limit 1)" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, iD) + } + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "orm: unable to check if traces exists") + } + + return exists, nil +} + +// Exists checks if the Trace row exists. +func (o *Trace) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + return TraceExists(ctx, exec, o.ID) +} diff --git a/server/pkg/repo/repo.go b/server/pkg/repo/repo.go index 5a63c330..9304f92f 100644 --- a/server/pkg/repo/repo.go +++ b/server/pkg/repo/repo.go @@ -933,3 +933,22 @@ func (r *Repo) CountUsers(ctx context.Context, opts ...CountUsersOpt) (int64, er return count, nil } + +type StoreTraceParams struct { + Request string + StatusCode int + DurationMS int +} + +func (r *Repo) StoreTrace(ctx context.Context, p StoreTraceParams) error { + trace := &orm.Trace{ + Request: p.Request, + StatusCode: p.StatusCode, + DurationMS: p.DurationMS, + } + if err := trace.Insert(ctx, r.executor(), boil.Infer()); err != nil { + return fmt.Errorf("trace insert: %w", err) + } + + return nil +} diff --git a/server/pkg/trace/trace.go b/server/pkg/trace/trace.go new file mode 100644 index 00000000..491e1bb3 --- /dev/null +++ b/server/pkg/trace/trace.go @@ -0,0 +1,61 @@ +package trace + +import ( + "context" + "net/http" + "time" + + "go.uber.org/zap" + + "github.com/crlssn/getstronger/server/pkg/repo" +) + +type ResponseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *ResponseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +type Tracer struct { + log *zap.Logger + repo *repo.Repo +} + +func NewTracer(log *zap.Logger, repo *repo.Repo) *Tracer { + return &Tracer{log, repo} +} + +type Trace struct { + start time.Time + onEnd func(duration time.Duration, statusCode int) +} + +const timeout = 5 * time.Second + +func (m *Tracer) Trace(uri string) *Trace { + return &Trace{ + start: time.Now().UTC(), + onEnd: func(duration time.Duration, statusCode int) { + m.log.Info("trace", zap.String("uri", uri), zap.Duration("duration", duration), zap.Int("status_code", statusCode)) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := m.repo.StoreTrace(ctx, repo.StoreTraceParams{ + Request: uri, + DurationMS: int(duration.Milliseconds()), + StatusCode: statusCode, + }); err != nil { + m.log.Error("trace store failed", zap.Error(err)) + } + }() + }, + } +} + +func (t *Trace) End(rw *ResponseWriter) { + t.onEnd(time.Since(t.start), rw.statusCode) +} diff --git a/server/rpc/middlewares/middlewares.go b/server/rpc/middlewares/middlewares.go index 0a083ffc..692fb14a 100644 --- a/server/rpc/middlewares/middlewares.go +++ b/server/rpc/middlewares/middlewares.go @@ -7,19 +7,22 @@ import ( "github.com/rs/cors" "github.com/crlssn/getstronger/server/pkg/config" + "github.com/crlssn/getstronger/server/pkg/trace" "github.com/crlssn/getstronger/server/pkg/xcontext" ) type Middleware struct { config *config.Config + tracer *trace.Tracer } -func New(c *config.Config) *Middleware { - return &Middleware{c} +func New(c *config.Config, t *trace.Tracer) *Middleware { + return &Middleware{c, t} } func (m *Middleware) Register(h http.Handler) http.Handler { middlewares := []func(http.Handler) http.Handler{ + m.trace, m.coors, m.cookies, } @@ -57,7 +60,7 @@ func (m *Middleware) coors(h http.Handler) http.Handler { func (m *Middleware) cookies(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie("refreshToken") + cookie, err := r.Cookie("refreshToken") // TODO: Move cookie logic to own package. if err == nil { r = r.WithContext(xcontext.WithRefreshToken(r.Context(), cookie.Value)) } @@ -65,3 +68,14 @@ func (m *Middleware) cookies(h http.Handler) http.Handler { h.ServeHTTP(w, r) }) } + +func (m *Middleware) trace(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Use a custom response writer to capture the status code. + rw := &trace.ResponseWriter{ResponseWriter: w} + t := m.tracer.Trace(r.RequestURI) + defer t.End(rw) + + h.ServeHTTP(rw, r) + }) +} diff --git a/server/rpc/v1/auth.go b/server/rpc/v1/auth.go index 8229a368..91ba646b 100644 --- a/server/rpc/v1/auth.go +++ b/server/rpc/v1/auth.go @@ -128,6 +128,7 @@ func (h *auth) Login(ctx context.Context, req *connect.Request[v1.LoginRequest]) AccessToken: accessToken, }) + // TODO: Move cookie logic to own package. cookie := &http.Cookie{ Name: "refreshToken", Value: refreshToken, @@ -201,6 +202,7 @@ func (h *auth) Logout(ctx context.Context, _ *connect.Request[v1.LogoutRequest]) } res := connect.NewResponse(&v1.LogoutResponse{}) + // TODO: Move cookie logic to own package. cookie := &http.Cookie{ Name: "refreshToken", Value: "",