Compare commits

..

2 Commits

Author SHA1 Message Date
William Perron 122bf0e4cf
tmp
11 months ago
William Perron b75539416a
tmp
11 months ago

@ -6,7 +6,7 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.21
go-version: 1.19
- uses: actions/checkout@v3
- name: "fmt"
run: test -z $(go fmt ./...)
@ -19,4 +19,4 @@ jobs:
skip-build-cache: true
args: --timeout 2m
- name: test
run: go test ./...
run: go test ./...

@ -3,13 +3,13 @@ WORKDIR /app
COPY . .
RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/...
FROM ubuntu:23.10 as litestream
FROM ubuntu:22.04 as litestream
WORKDIR /download
RUN apt update -y && apt install -y wget tar
RUN wget https://github.com/benbjohnson/litestream/releases/download/v0.3.9/litestream-v0.3.9-linux-amd64.tar.gz; \
tar -zxf litestream-v0.3.9-linux-amd64.tar.gz;
FROM ubuntu:23.10
FROM ubuntu:22.04
WORKDIR /themis
COPY --from=builder /app/bin/themis-server /usr/local/bin/themis-server
COPY --from=litestream /download/litestream /usr/local/bin/litestream

@ -7,7 +7,7 @@ Discord App to allow EU4 players to take claims on regions and provinces.
### Requirements
To develop:
- [Go](https://go.dev/) version 1.21 or higher installed locally
- [Go](https://go.dev/) version 1.19 or higher installed locally
- `sqlite3` installed locally (already ships by default on most OSes)
To deploy:

@ -6,25 +6,11 @@ import (
"time"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string) error {
ctx, span := tracer.Start(ctx, "add_absence", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("absences"),
semconv.DBOperation("insert"),
attribute.String("user_id", userId),
attribute.String("session_date", session.Format(time.DateOnly)),
))
defer span.End()
if session.Weekday() != time.Monday {
log.Debug().Ctx(ctx).Msg(fmt.Sprintf("%s is not a monday", session))
span.RecordError(fmt.Errorf("%s is not a monday", session))
return fmt.Errorf("not a monday")
}
@ -35,23 +21,17 @@ func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string
tx, err := s.db.Begin()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO absences (session_date, userid) VALUES (?, ?)")
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare absence query")
return fmt.Errorf("failed to prepare absence query: %w", err)
}
_, err = stmt.ExecContext(ctx, session.Format(time.DateOnly), userId)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to insert absence")
return fmt.Errorf("failed to insert absence: %w", err)
}
@ -59,34 +39,20 @@ func (s *Store) AddAbsence(ctx context.Context, session time.Time, userId string
}
func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string, error) {
ctx, span := tracer.Start(ctx, "get_absentees", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("absences"),
semconv.DBOperation("select"),
attribute.String("session_date", session.Format(time.DateOnly)),
))
defer span.End()
log.Debug().Ctx(ctx).Time("session", session).Msg("getting list of absentees")
tx, err := s.db.Begin()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, `SELECT userid FROM absences WHERE session_date = ?`)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare query: %w", err)
}
rows, err := stmt.QueryContext(ctx, session.Format(time.DateOnly))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err)
}
@ -108,38 +74,23 @@ func (s *Store) GetAbsentees(ctx context.Context, session time.Time) ([]string,
type Schedule map[string][]string
func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule, error) {
ctx, span := tracer.Start(ctx, "get_schedule", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("absences"),
semconv.DBOperation("select"),
attribute.String("from", from.Format(time.DateOnly)),
attribute.String("to", to.Format(time.DateOnly)),
))
defer span.End()
log.Debug().Ctx(ctx).Time("from", from).Time("to", to).Msg("getting next sessions schedule")
schedule := make(Schedule)
initSchedule(schedule, from, to)
tx, err := s.db.Begin()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return nil, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, `SELECT session_date, userid FROM absences WHERE session_date BETWEEN ? AND ? ORDER BY session_date ASC`)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare query: %w", err)
}
rows, err := stmt.QueryContext(ctx, from.Format(time.DateOnly), to.Format(time.DateOnly))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err)
}
@ -148,12 +99,14 @@ func (s *Store) GetSchedule(ctx context.Context, from, to time.Time) (Schedule,
var user string
err = rows.Scan(&date, &user)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan row: %w", err)
}
schedule[date] = append(schedule[date], user)
if _, ok := schedule[date]; ok {
schedule[date] = append(schedule[date], user)
} else {
schedule[date] = []string{user}
}
}
return schedule, nil

@ -2,20 +2,16 @@ package themis
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAddAbsence(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAddAbsence"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAddAbsence"))
require.NoError(t, err)
now := NextMonday()
@ -35,9 +31,7 @@ func TestAddAbsence(t *testing.T) {
}
func TestGetSchedule(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestGetSchedule"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestGetSchedule"))
require.NoError(t, err)
now := NextMonday()

@ -3,14 +3,11 @@ package themis
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
type EventType int
@ -48,50 +45,36 @@ func EventTypeFromString(ev string) (EventType, error) {
case "ABSENT":
return EventAbsence, nil
default:
return EventType(9999), fmt.Errorf("no such event type: %s", ev)
return EventType(9999), errors.New("no such event type")
}
}
type AuditableEvent struct {
userId string
eventType EventType
timestamp time.Time
err error
}
// Audit writes to the audit table, returns nothing because it is meant to be
// used in a defered statement on functions that write to the database.
func (s *Store) Audit(ctx context.Context, ev *AuditableEvent) {
ctx, span := tracer.Start(ctx, "audit", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("audit_log"),
semconv.DBOperation("insert"),
attribute.String("user_id", ev.userId),
attribute.Stringer("event_type", ev.eventType),
))
defer span.End()
if ev.err == nil {
log.Debug().Ctx(ctx).Str("event_type", ev.eventType.String()).Str("userid", ev.userId).Msg("recording audit log")
ctx := context.Background()
tx, err := s.db.Begin()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to start transaction")
log.Error().Ctx(ctx).Err(err).Msg("failed to start transaction")
}
defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO audit_log (userid, event_type, ts) VALUES (?, ?, ?)")
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare audit log insert")
log.Error().Ctx(ctx).Err(err).Msg("failed to prepare audit log insert")
}
if _, err := stmt.ExecContext(ctx, ev.userId, ev.eventType.String(), time.Now()); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to insert audit log")
log.Error().Ctx(ctx).Err(err).Msg("failed to insert audit log")
}
}
@ -105,20 +88,10 @@ type AuditEvent struct {
}
func (s *Store) LastOf(ctx context.Context, t EventType) (AuditEvent, error) {
ctx, span := tracer.Start(ctx, "find_last_audit_log", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("audit_log"),
semconv.DBOperation("select"),
attribute.Stringer("event_type", t),
))
defer span.End()
log.Debug().Ctx(ctx).Str("event_type", t.String()).Msg("finding last audit log")
stmt, err := s.db.PrepareContext(ctx, `SELECT id, userid, event_type, ts FROM audit_log WHERE event_type = ? ORDER BY ts DESC LIMIT 1`)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get last event")
return AuditEvent{}, fmt.Errorf("failed to get last event of type %s: %w", t.String(), err)
}
@ -128,20 +101,15 @@ func (s *Store) LastOf(ctx context.Context, t EventType) (AuditEvent, error) {
var rawEventType string
err = row.Scan(&ev.Id, &ev.UserId, &rawEventType, &ev.Timestamp)
if err == sql.ErrNoRows {
span.RecordError(ErrNever)
return AuditEvent{}, ErrNever
}
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return AuditEvent{}, fmt.Errorf("failed to scan row: %w", err)
}
ev.EventType, err = EventTypeFromString(rawEventType)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse event type")
return AuditEvent{}, fmt.Errorf("failed to parse event type: %w", err)
return AuditEvent{}, fmt.Errorf("failed to parse event type %s: %w", rawEventType, err)
}
return ev, nil

@ -34,6 +34,12 @@ const (
CLAIM_TYPE_TRADE = "trade"
)
var claimTypeToColumn = map[ClaimType]string{
CLAIM_TYPE_AREA: "area",
CLAIM_TYPE_REGION: "region",
CLAIM_TYPE_TRADE: "trade_node",
}
type Claim struct {
ID int
Player string

@ -10,7 +10,6 @@ import (
"net/url"
"os"
"os/signal"
"runtime/pprof"
"sort"
"strconv"
"strings"
@ -21,19 +20,10 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"go.wperron.io/flydetector"
"go.wperron.io/sqliteexporter"
"go.wperron.io/themis"
"go.wperron.io/themis/correlation"
zerologcompat "go.wperron.io/themis/correlation/compat/zerolog"
)
const (
@ -41,16 +31,15 @@ const (
)
var (
dbFile = flag.String("db", "", "SQlite database file path.")
debug = flag.Bool("debug", false, "Set log level to DEBUG.")
cpuProfile = flag.String("cpuprofile", "", "Output file for pprof profiling.")
dbFile = flag.String("db", "", "SQlite database file path.")
debug = flag.Bool("debug", false, "Set log level to DEBUG.")
store *themis.Store
tracer trace.Tracer
propagator = propagation.TraceContext{}
store *themis.Store
seq = &correlation.CryptoRandSequencer{}
gen = correlation.NewGenerator(seq)
)
type Handler func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error
type Handler func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate)
func main() {
log.Info().Msg("startup.")
@ -66,20 +55,9 @@ func main() {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
log.Logger = log.Logger.Hook(correlation.TraceContextHook{})
log.Logger = log.Logger.Hook(zerologcompat.CorrelationHook{})
zerolog.DurationFieldUnit = time.Millisecond
if *cpuProfile != "" && os.Getenv("ENV") != "production" {
log.Info().Str("file", *cpuProfile).Msg("starting profiler")
f, err := os.Create(*cpuProfile)
if err != nil {
log.Fatal().Err(err).Msg("failed to create cpu profile output file")
}
_ = pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
go func() {
if err := serve(":8080"); err != nil {
log.Error().Err(err).Msg("failed to serve requests")
@ -94,23 +72,13 @@ func main() {
connString := fmt.Sprintf(CONN_STRING_PATTERN, *dbFile)
log.Debug().Str("connection_string", connString).Msg("opening sqlite3 database")
db, err := sql.Open("sqlite3", connString)
if err != nil {
log.Fatal().Err(err).Msg("failed to open database")
}
store, err = themis.NewStore(db, log.Logger)
store, err = themis.NewStore(connString)
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize database")
}
defer store.Close()
if err := initTracing(ctx, db); err != nil {
log.Fatal().Err(err).Msg("failed to initialize tracing")
}
notifChan := make(chan context.Context)
notifChan := make(chan struct{})
notifier := themis.NewNotifier(notifChan)
go notifier.Start(ctx)
@ -247,7 +215,7 @@ func main() {
},
}
handlers := map[string]Handler{
"info": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"info": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
uptime, err := themis.Uptime(ctx)
if err != nil {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -257,9 +225,10 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to get server uptime: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to get server uptime")
return
}
claimCount, uniquePlayers, err := store.CountClaims(ctx)
@ -271,9 +240,10 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to count claims: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to count claims")
return
}
ev, err := store.LastOf(ctx, themis.EventFlush)
@ -287,9 +257,10 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed get last flush event: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed get last flush event")
return
}
lastFlush = "never"
} else {
@ -303,11 +274,10 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
"list-claims": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"list-claims": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
claims, err := store.ListClaims(ctx)
if err != nil {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -317,9 +287,10 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to list claims: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to list claims")
return
}
sb := strings.Builder{}
@ -335,16 +306,14 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
"claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type == discordgo.InteractionApplicationCommandAutocomplete {
log.Debug().Ctx(ctx).Msg("command type interaction autocomplete")
// TODO(wperron) fix this
handleClaimAutocomplete(ctx, store, s, i)
return nil
return
}
opts := i.ApplicationCommandData().Options
@ -352,14 +321,13 @@ func main() {
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "`claim-type` and `name` are mandatory parameters",
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
return
}
claimType, err := themis.ClaimTypeFromString(opts[0].StringValue())
@ -367,14 +335,14 @@ func main() {
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "You can only take claims of types `area`, `region` or `trade`",
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to parse claim: %w", err)
log.Error().Ctx(ctx).Err(err).Str("claim_type", opts[0].StringValue()).Msg("failed to parse claim")
return
}
name := opts[1].StringValue()
@ -387,7 +355,6 @@ func main() {
_, err = store.Claim(ctx, userId, player, name, claimType)
if err != nil {
// TODO(wperron) fix this error cast
conflict, ok := err.(themis.ErrConflict)
if ok {
sb := strings.Builder{}
@ -400,27 +367,26 @@ func main() {
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: sb.String(),
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction")
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
return
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "failed to acquire claim :(",
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to acquire claim: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to acquire claim")
return
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -430,11 +396,10 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
"describe-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"describe-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
id := i.ApplicationCommandData().Options[0]
detail, err := store.DescribeClaim(ctx, int(id.IntValue()))
if err != nil {
@ -445,15 +410,16 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to describe claim: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to describe claim")
return
}
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf("#%d %s %s (%s)\n", detail.ID, detail.Name, detail.Type, detail.Player))
for _, p := range detail.Provinces {
sb.WriteString(fmt.Sprintf("- %s\n", p))
sb.WriteString(fmt.Sprintf(" - %s\n", p))
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -463,16 +429,15 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
"delete-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"delete-claim": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
id := i.ApplicationCommandData().Options[0]
userId := i.Member.User.ID
err := store.DeleteClaim(ctx, int(id.IntValue()), userId)
if err != nil {
msg := "Oops, something went wrong :( blame <@wperron>"
msg := "Oops, something went wrong :( blame @wperron"
if errors.Is(err, themis.ErrNoSuchClaim) {
msg = fmt.Sprintf("Claim #%d not found for %s", id.IntValue(), i.Member.Nick)
}
@ -483,33 +448,34 @@ func main() {
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to delete claim: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to delete claim")
return
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "Got it chief.",
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
"flush": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"flush": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
cid := correlation.FromContext(ctx)
baggage := make(url.Values)
propagator.Inject(ctx, correlation.UrlValuesCarrier(baggage))
baggage.Set("correlation_id", cid.String())
state := baggage.Encode()
sb := strings.Builder{}
sb.WriteString("modal_flush")
if len(baggage) != 0 {
if state != "" {
sb.WriteRune(':')
sb.WriteString(baggage.Encode())
sb.WriteString(state)
}
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
@ -540,14 +506,14 @@ func main() {
},
},
}); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
"query": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"query": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
roDB, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=private&mode=ro", *dbFile))
if err != nil {
return fmt.Errorf("failed to open read-only copy of database: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to open read-only copy of database")
return
}
q := i.ApplicationCommandData().Options[0].StringValue()
@ -555,12 +521,14 @@ func main() {
defer cancelDeadline()
rows, err := roDB.QueryContext(deadlined, q)
if err != nil {
return fmt.Errorf("faied to exec user-provided query: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to exec user-provided query")
return
}
fmtd, err := themis.FormatRows(ctx, rows)
if err != nil {
return fmt.Errorf("failed to format rows: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to format rows")
return
}
// 2000 is a magic number here, it's the character limit for a discord
@ -575,11 +543,9 @@ func main() {
},
}); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
return fmt.Errorf("failed to respond to interaction: %w", err)
}
return nil
},
"schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
// get schedule from now to 4 mondays into the future
sched, err := store.GetSchedule(ctx, themis.NextMonday(), themis.NextMonday().Add(4*7*24*time.Hour))
if err != nil {
@ -589,9 +555,10 @@ func main() {
Content: "failed to get schedule, check logs for more info.",
},
}); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to get schedule: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to get schedule")
return
}
sb := strings.Builder{}
@ -621,26 +588,21 @@ func main() {
},
}); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
return fmt.Errorf("failed to respond to interaction: %w", err)
}
return nil
},
"send-schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
notifier.Send(ctx)
"send-schedule": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
notifier.Send()
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "Done.",
},
}); err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
return fmt.Errorf("failed to respond to interaction: %w", err)
}
return nil
},
"absent": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
"absent": func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
var rawDate string
if len(i.ApplicationCommandData().Options) == 0 {
rawDate = themis.NextMonday().Format(time.DateOnly)
@ -653,40 +615,37 @@ func main() {
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "failed to parse provided date, make sure to use the YYYY-MM-DD format.",
},
}); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
return
}
if date.Before(time.Now()) {
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "The date must be some time in the future.",
},
}); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
return
}
if date.Weekday() != time.Monday {
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "The date you provided is not a Monday.",
},
}); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
// TODO(wperron) suggest Mondays before and after?
return nil
return
}
userId := i.Member.User.ID
@ -697,22 +656,20 @@ func main() {
Content: "something went wrong recording your absence, check logs for more info.",
},
}); err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return fmt.Errorf("failed to record absence: %w", err)
return
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Flags: discordgo.MessageFlagsEphemeral,
Content: "Okey dokey.",
},
})
if err != nil {
return fmt.Errorf("failed to respond to interaction: %w", err)
log.Error().Ctx(ctx).Err(err).Msg("failed to respond to interaction")
}
return nil
},
}
@ -731,13 +688,7 @@ func main() {
}
log.Info().Int("count", len(created)).Dur("startup_latency_ms", time.Since(start)).Msg("registered commands, ready to operate")
go notifier.NotifyFunc(ctx, func(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
ctx, span := tracer.Start(ctx, "weekly_notification")
defer span.End()
go notifier.NotifyFunc(ctx, func() {
log.Info().Msg("sending weekly reminder")
absentees, err := store.GetAbsentees(ctx, themis.NextMonday())
if err != nil {
@ -753,7 +704,7 @@ func main() {
var msg string
var components []discordgo.MessageComponent
if len(absentees) == 0 {
msg = "Everybody can make it next Monday, see you then! 🎉"
msg = "Everybody can make it next Monday, see you then!"
components = []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
@ -761,16 +712,12 @@ func main() {
CustomID: "schedule-response",
Label: "I Can't Make It",
Style: discordgo.DangerButton,
Disabled: false,
Emoji: discordgo.ComponentEmoji{
Name: "🙁",
},
},
},
},
}
} else {
msg = fmt.Sprintf("%s can't make it next Monday. 🙁", themis.FormatStringSlice(absentees))
msg = fmt.Sprintf("%s can't make it next Monday. :sad:", themis.FormatStringSlice(absentees))
}
_, err = discord.ChannelMessageSendComplex(channelId, &discordgo.MessageSend{
@ -784,7 +731,7 @@ func main() {
<-ctx.Done()
log.Info().Msg("context cancelled, exiting")
store.Close()
os.Exit(0)
}
func touchDbFile(path string) error {
@ -815,9 +762,10 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
case discordgo.InteractionApplicationCommand:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx = context.WithValue(ctx, "correlation_id", gen.Next())
if h, ok := handlers[i.ApplicationCommandData().Name]; ok {
_ = inSpan(i.ApplicationCommandData().Name, withLogging(i.ApplicationCommandData().Name, h))(ctx, s, i)
withLogging(i.ApplicationCommandData().Name, h)(ctx, s, i)
}
case discordgo.InteractionModalSubmit:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@ -825,11 +773,16 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
state, err := parseCustomIDState(i.ModalSubmitData().CustomID)
if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("unexpected error occurred while parsing state from custom id, returning early.")
log.Error().Ctx(ctx).Err(err).Msg("unexpected error occured while parsing state from custom id, returning early.")
return
}
ctx = propagator.Extract(ctx, correlation.UrlValuesCarrier(state))
cid := state.Get("correlation_id")
if cid != "" {
ctx = context.WithValue(ctx, "correlation_id", cid)
} else {
ctx = context.WithValue(ctx, "correlation_id", gen.Next())
}
if strings.HasPrefix(i.ModalSubmitData().CustomID, "modal_flush") {
sub := i.ModalSubmitData().Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
@ -837,7 +790,7 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
log.Debug().Ctx(ctx).Str("value", sub).Msg("flush modal submitted")
if sub == "y" || sub == "ye" || sub == "yes" {
err := store.Flush(ctx, i.Member.User.ID)
err := store.Flush(context.Background(), i.Member.User.ID)
msg := "Flushed all claims!"
if err != nil {
log.Error().Ctx(ctx).Err(err).Msg("failed to flush claims")
@ -877,14 +830,19 @@ func registerHandlers(sess *discordgo.Session, handlers map[string]Handler) {
return
}
ctx = propagator.Extract(ctx, correlation.UrlValuesCarrier(state))
cid := state.Get("correlation_id")
if cid != "" {
ctx = context.WithValue(ctx, "correlation_id", cid)
} else {
ctx = context.WithValue(ctx, "correlation_id", gen.Next())
}
switch i.MessageComponentData().CustomID {
case "schedule-response":
userId := i.Member.User.ID
log.Info().Ctx(ctx).Str("message_component", "schedule-response").Str("userid", userId).Msg("handling message component interaction")
if err := store.AddAbsence(ctx, themis.NextMonday(), userId); err != nil {
if err := store.AddAbsence(context.TODO(), themis.NextMonday(), userId); err != nil {
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
@ -984,57 +942,55 @@ func serve(address string) error {
return http.ListenAndServe(address, nil)
}
func inSpan(name string, h Handler) Handler {
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
ctx, span := tracer.Start(ctx, fmt.Sprintf("discord_command %s", name))
defer span.End()
return h(ctx, s, i)
}
}
func withLogging(name string, h Handler) Handler {
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) error {
return func(ctx context.Context, s *discordgo.Session, i *discordgo.InteractionCreate) {
start := time.Now()
logCommandInvocation(ctx, name, s, i)
err := h(ctx, s, i)
debugCommandCompletion(ctx, name, time.Since(start), err, s, i)
return nil
h(ctx, s, i)
debugCommandCompletion(ctx, name, time.Since(start), s, i)
}
}
func logCommandInvocation(ctx context.Context, name string, s *discordgo.Session, i *discordgo.InteractionCreate) {
span := trace.SpanFromContext(ctx)
span.SetAttributes(
attribute.String("user_id", i.Member.User.ID),
attribute.String("username", i.Member.User.Username),
attribute.String("command_name", name),
)
for _, o := range i.ApplicationCommandData().Options {
switch o.Type {
case discordgo.ApplicationCommandOptionSubCommand, discordgo.ApplicationCommandOptionSubCommandGroup:
panic("unreachable")
case discordgo.ApplicationCommandOptionString:
span.SetAttributes(attribute.String(fmt.Sprintf("command_option.%s", o.Name), o.StringValue()))
case discordgo.ApplicationCommandOptionInteger:
span.SetAttributes(attribute.Int64(fmt.Sprintf("command_option.%s", o.Name), o.IntValue()))
case discordgo.ApplicationCommandOptionBoolean:
span.SetAttributes(attribute.Bool(fmt.Sprintf("command_option.%s", o.Name), o.BoolValue()))
default:
span.SetAttributes(attribute.String(fmt.Sprintf("command_option.%s", o.Name), "unsupported_type"))
}
}
log.Info().Ctx(ctx).Msg("command invoked")
log.Info().
Ctx(ctx).
Str("userid", i.Member.User.ID).
Str("username", i.Member.User.Username).
Str("command", name).
Str("params", func() string {
p := make([]string, 0, len(i.ApplicationCommandData().Options))
for _, o := range i.ApplicationCommandData().Options {
sb := strings.Builder{}
sb.WriteString(o.Name)
sb.WriteRune('=')
switch o.Type {
case discordgo.ApplicationCommandOptionSubCommand, discordgo.ApplicationCommandOptionSubCommandGroup:
panic("unreachable")
case discordgo.ApplicationCommandOptionString:
sb.WriteString(o.StringValue())
case discordgo.ApplicationCommandOptionInteger:
sb.WriteString(fmt.Sprint(o.IntValue()))
case discordgo.ApplicationCommandOptionBoolean:
sb.WriteString(fmt.Sprint(o.BoolValue()))
default:
sb.WriteString("[unsupported type]")
}
p = append(p, sb.String())
}
return strings.Join(p, ", ")
}()).
Msg("command invoked")
}
func debugCommandCompletion(ctx context.Context, name string, dur time.Duration, err error, s *discordgo.Session, i *discordgo.InteractionCreate) {
span := trace.SpanFromContext(ctx)
if err != nil {
span.SetStatus(codes.Error, err.Error())
}
log.Info().Ctx(ctx).Dur("latency_ms", dur).Msg("command completed")
func debugCommandCompletion(ctx context.Context, name string, dur time.Duration, s *discordgo.Session, i *discordgo.InteractionCreate) {
log.Info().
Ctx(ctx).
Str("userid", i.Member.User.ID).
Str("username", i.Member.User.Username).
Str("command", name).
Dur("latency_ms", dur).
Msg("command completed")
}
func min(a, b int) int {
@ -1056,36 +1012,3 @@ func parseCustomIDState(qs string) (url.Values, error) {
}
return v, nil
}
func initTracing(ctx context.Context, db *sql.DB) error {
fd, _ := flydetector.NewDetector().Detect(ctx) // this can't error
rd, err := resource.New(ctx,
resource.WithHost(),
resource.WithOS(),
resource.WithProcess(),
resource.WithTelemetrySDK(),
resource.WithAttributes(semconv.ServiceName("themis")),
)
if err != nil {
return fmt.Errorf("failed to create resource: %w", err)
}
res, err := resource.Merge(fd, rd)
if err != nil {
return fmt.Errorf("failed to merge resources: %w", err)
}
ex, err := sqliteexporter.NewSqliteSDKTraceExporterWithDB(db)
if err != nil {
return fmt.Errorf("failed to create span exporter: %w", err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(ex, sdktrace.WithExportTimeout(time.Second)),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
tracer = tp.Tracer("themis")
return nil
}

@ -2,13 +2,10 @@ package themis
import (
"context"
"database/sql"
"fmt"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
type Conflict struct {
@ -23,10 +20,31 @@ func (c Conflict) String() string {
return fmt.Sprintf("%s owned by #%d %s %s (%s)", c.Province, c.ClaimID, c.ClaimType, c.Claim, c.Player)
}
const conflictQuery string = `WITH claiming AS (
// const conflictQuery string = `SELECT name, player, claim_type, val, id FROM (
// SELECT provinces.name, claims.player, claims.claim_type, claims.val, claims.id
// FROM claims
// LEFT JOIN provinces ON claims.val = provinces.trade_node
// WHERE claims.claim_type = 'trade' AND claims.userid IS NOT ?
// AND provinces.%[1]s = ?
// UNION
// SELECT provinces.name, claims.player, claims.claim_type, claims.val, claims.id
// FROM claims
// LEFT JOIN provinces ON claims.val = provinces.region
// WHERE claims.claim_type = 'region' AND claims.userid IS NOT ?
// AND provinces.%[1]s = ?
// UNION
// SELECT provinces.name, claims.player, claims.claim_type, claims.val, claims.id
// FROM claims
// LEFT JOIN provinces ON claims.val = provinces.area
// WHERE claims.claim_type = 'area' AND claims.userid IS NOT ?
// AND provinces.%[1]s = ?
// );`
const conflictQuery string = `
WITH claiming AS (
SELECT province FROM claimables
WHERE claimables.typ = ?
AND claimables.name = ?
WHERE typ = ?
AND name = ?
)
SELECT claimables.province, claims.player, claims.claim_type, claims.val, claims.id
FROM claims
@ -35,35 +53,41 @@ INNER JOIN claimables
AND claims.val = claimables.name
INNER JOIN claiming
ON claiming.province = claimables.province
WHERE claims.userid IS NOT ?;`
WHERE claims.userid IS NOT ?`
func (s *Store) FindConflicts(ctx context.Context, userId, name string, claimType ClaimType) ([]Conflict, error) {
ctx, span := tracer.Start(ctx, "find_conflicts", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
attribute.String("user_id", userId),
attribute.String("claim_name", name),
attribute.Stringer("claim_type", claimType),
))
defer span.End()
log.Debug().Ctx(ctx).Stringer("claim_type", claimType).Str("userid", userId).Msg("searching for potential conflicts")
stmt, err := s.db.PrepareContext(ctx, conflictQuery)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare conflicts query: %w", err)
}
rows, err := stmt.QueryContext(ctx, claimType, name, userId)
// =========================================================================
rows, err := s.db.QueryContext(ctx, `select claims.val, claims.claim_type, claims.player, claimables.province
from claims
inner join claimables on claims.val = claimables.name and claims.claim_type = claimables.typ;
`)
if err != nil {
panic(err)
}
res2 := make([]string, 0)
for rows.Next() {
var name, typ, play string
var pro sql.NullString
if err := rows.Scan(&name, &typ, &play, &pro); err != nil {
panic(err)
}
res2 = append(res2, fmt.Sprintf("%s, %s, %s, %s", name, typ, play, pro.String))
}
log.Debug().Strs("list", res2).Msg("existing claims")
// =========================================================================
rows, err = stmt.QueryContext(ctx, claimTypeToColumn[claimType], name, userId)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to get conflicting provinces: %w", err)
}
defer stmt.Close()
conflicts := make([]Conflict, 0)
for rows.Next() {
@ -76,8 +100,6 @@ func (s *Store) FindConflicts(ctx context.Context, userId, name string, claimTyp
)
err = rows.Scan(&province, &player, &sClaimType, &claimName, &claimId)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan row: %w", err)
}

@ -2,24 +2,19 @@ package themis
import (
"context"
"database/sql"
"fmt"
"reflect"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStore_FindConflicts(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_FindConflicts"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_FindConflicts"))
assert.NoError(t, err)
id, err := store.Claim(context.Background(), "000000000000000001", "foo", "Bordeaux", CLAIM_TYPE_TRADE)
require.NoError(t, err)
id, err := store.Claim(context.TODO(), "000000000000000001", "foo", "Bordeaux", CLAIM_TYPE_TRADE)
assert.NoError(t, err)
type args struct {
ctx context.Context
@ -33,21 +28,21 @@ func TestStore_FindConflicts(t *testing.T) {
want []Conflict
wantErr bool
}{
{
name: "same-player",
args: args{
context.TODO(),
"000000000000000001",
"France",
CLAIM_TYPE_REGION,
},
want: []Conflict{},
wantErr: false,
},
// {
// name: "same-player",
// args: args{
// context.TODO(),
// "000000000000000001",
// "France",
// CLAIM_TYPE_REGION,
// },
// want: []Conflict{},
// wantErr: false,
// },
{
name: "overlapping",
args: args{
context.Background(),
context.TODO(),
"000000000000000002",
"Iberia",
CLAIM_TYPE_REGION,
@ -59,17 +54,17 @@ func TestStore_FindConflicts(t *testing.T) {
},
wantErr: false,
},
{
name: "no-overlap",
args: args{
context.TODO(),
"000000000000000002",
"Scandinavia",
CLAIM_TYPE_REGION,
},
want: []Conflict{},
wantErr: false,
},
// {
// name: "no-overlap",
// args: args{
// context.TODO(),
// "000000000000000002",
// "Scandinavia",
// CLAIM_TYPE_REGION,
// },
// want: []Conflict{},
// wantErr: false,
// },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -0,0 +1,16 @@
package zerolog
import (
zl "github.com/rs/zerolog"
"go.wperron.io/themis/correlation"
)
type CorrelationHook struct{}
func (h CorrelationHook) Run(e *zl.Event, level zl.Level, msg string) {
ctx := e.GetCtx()
c := correlation.FromContext(ctx)
if c != nil {
e.Stringer("correlation_id", c)
}
}

@ -0,0 +1,47 @@
package correlation
import (
"context"
"encoding/hex"
)
const Key string = "correlation_id"
var Empty CorrelationID = CorrelationID{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
// Correlation ID is a byte array of length 16
type CorrelationID []byte
func (ci CorrelationID) String() string {
if ci == nil {
return hex.EncodeToString(Empty)
}
return hex.EncodeToString(ci)
}
func FromContext(ctx context.Context) CorrelationID {
if v := ctx.Value("correlation_id"); v != nil {
if c, ok := v.(CorrelationID); ok {
return c
}
}
return nil
}
type Sequencer interface {
Next() []byte
}
type Generator struct {
seq Sequencer
}
func NewGenerator(seq Sequencer) *Generator {
return &Generator{
seq: seq,
}
}
func (g *Generator) Next() CorrelationID {
return CorrelationID(g.seq.Next())
}

@ -0,0 +1,18 @@
package correlation
import (
"math/rand"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGeneratorNext(t *testing.T) {
rand.Seed(0)
seq := &MathRandSequencer{}
gen := NewGenerator(seq)
assert.Equal(t, "0194fdc2fa2ffcc041d3ff12045b73c8", gen.Next().String())
assert.Equal(t, "6e4ff95ff662a5eee82abdf44a2d0b75", gen.Next().String())
assert.Equal(t, "fb180daf48a79ee0b10d394651850fd4", gen.Next().String())
}

@ -0,0 +1,19 @@
package correlation
import "crypto/rand"
type CryptoRandSequencer struct{}
func (crs *CryptoRandSequencer) Next() []byte {
buf := make([]byte, 16)
read, err := rand.Read(buf)
if err != nil {
panic("not implemented")
}
if read != 16 {
panic("todo")
}
return buf
}

@ -0,0 +1,18 @@
package correlation
import "math/rand"
type MathRandSequencer struct{}
func (mrs *MathRandSequencer) Next() []byte {
buf := make([]byte, 16)
read, err := rand.Read(buf)
if err != nil {
panic("not implemented")
}
if read != 16 {
panic("todo")
}
return buf
}

@ -1,44 +0,0 @@
package correlation
import (
"net/url"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
type TraceContextHook struct{}
func (h TraceContextHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
ctx := e.GetCtx()
spanContext := trace.SpanContextFromContext(ctx)
trace_id := spanContext.TraceID()
if trace_id.IsValid() {
e.Stringer("trace_id", trace_id)
}
}
var _ propagation.TextMapCarrier = UrlValuesCarrier{}
type UrlValuesCarrier url.Values
// Get implements propagation.TextMapCarrier.
func (u UrlValuesCarrier) Get(key string) string {
return url.Values(u).Get(key)
}
// Keys implements propagation.TextMapCarrier.
func (u UrlValuesCarrier) Keys() []string {
raw := map[string][]string(u)
ks := make([]string, 0, len(raw))
for k := range raw {
ks = append(ks, k)
}
return ks
}
// Set implements propagation.TextMapCarrier.
func (u UrlValuesCarrier) Set(key string, value string) {
url.Values(u).Set(key, value)
}

@ -7,7 +7,6 @@ processes = []
[env]
TZ = "America/New_York"
ENV = "production"
DISCORD_APP_ID = "1014881815921705030"
DISCORD_GUILD_ID = "375417755777892353"
DISCORD_BOT_CHANNEL_ID = "1018997240968265768"

@ -7,23 +7,13 @@ import (
"strings"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
)
func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) {
ctx, span := tracer.Start(ctx, "format_rows", trace.WithAttributes(
semconv.DBSystemSqlite,
))
defer span.End()
sb := strings.Builder{}
cols, err := rows.Columns()
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get rows columns")
return "", fmt.Errorf("failed to get rows columns: %w", err)
}
@ -47,8 +37,6 @@ func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) {
row[i] = new(sql.NullString)
}
if err := rows.Scan(row...); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return "", fmt.Errorf("failed to scan next row: %w", err)
}

@ -2,19 +2,14 @@ package themis
import (
"context"
"database/sql"
"fmt"
"testing"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFormatRows(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
assert.NoError(t, err)
rows, err := store.db.Query("SELECT provinces.name, provinces.region, provinces.area, provinces.trade_node FROM provinces WHERE area = 'Gascony'")
@ -32,9 +27,7 @@ func TestFormatRows(t *testing.T) {
}
func TestFormatRowsAggregated(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
assert.NoError(t, err)
rows, err := store.db.Query("SELECT count(1) as total, trade_node from provinces where region = 'France' group by trade_node")
@ -53,9 +46,7 @@ func TestFormatRowsAggregated(t *testing.T) {
}
func TestFormatRowsInvalidQuery(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "format-rows"))
assert.NoError(t, err)
_, err = store.db.Query("SELECT count(name), distinct(trade_node) from provinces where region = 'France'")

@ -1,54 +1,13 @@
module go.wperron.io/themis
go 1.21.1
go 1.19
require (
github.com/bwmarrin/discordgo v0.26.1
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/mattn/go-sqlite3 v1.14.19
github.com/golang-migrate/migrate/v4 v4.16.2
github.com/mattn/go-sqlite3 v1.14.16
github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.4
go.opentelemetry.io/otel/trace v1.24.0
go.wperron.io/sqliteexporter v0.1.0
)
require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
github.com/knadh/koanf/v2 v2.0.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/collector v0.92.0 // indirect
go.opentelemetry.io/collector/component v0.94.1 // indirect
go.opentelemetry.io/collector/config/configretry v0.92.0 // indirect
go.opentelemetry.io/collector/config/configtelemetry v0.94.1 // indirect
go.opentelemetry.io/collector/confmap v0.94.1 // indirect
go.opentelemetry.io/collector/consumer v0.92.0 // indirect
go.opentelemetry.io/collector/exporter v0.92.0 // indirect
go.opentelemetry.io/collector/extension v0.92.0 // indirect
go.opentelemetry.io/collector/featuregate v1.0.1 // indirect
go.opentelemetry.io/collector/pdata v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
go.wperron.io/flydetector v0.0.0-20240302202855-7f606a93cdb6 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/grpc v1.61.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
github.com/stretchr/testify v1.8.1
)
require (
@ -59,10 +18,8 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel v1.24.0
go.opentelemetry.io/otel/sdk v1.24.0
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sys v0.12.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

258
go.sum

@ -1,70 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg=
contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
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/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU=
github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -72,58 +14,18 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
github.com/knadh/koanf/v2 v2.0.2 h1:sEZzPW2rVWSahcYILNq/syJdEyRafZIG0l9aWwL86HA=
github.com/knadh/koanf/v2 v2.0.2/go.mod h1:HN9uZ+qFAejH1e4G41gnoffIanINWQuONLXiV7kir6k=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0=
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@ -133,162 +35,24 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/collector v0.92.0 h1:XiC0ptaT1EmOkK2RI0gt3n2tkzLAkNQGf0E7hrGdyeA=
go.opentelemetry.io/collector v0.92.0/go.mod h1:wbksjM63DTKA1BbdUVS7gAFzAngCZTWb46RBpKdtsPw=
go.opentelemetry.io/collector/component v0.94.1 h1:j4peKsWb+QVBKPs2RJeIj5EoQW7yp2ZVGrd8Bu9HU9M=
go.opentelemetry.io/collector/component v0.94.1/go.mod h1:vg+kAH81C3YS0SPzUXkSFWLPC1WH7zx70dAtUWWIHcE=
go.opentelemetry.io/collector/config/configretry v0.92.0 h1:3WUabmCRIBHSkOLGCHGieUGchlHkBw3Fa4Cj9Do5Xdw=
go.opentelemetry.io/collector/config/configretry v0.92.0/go.mod h1:gt1HRYyMxcMca9lbDLPbivQzsUCjVjkPAn/3S6fiD14=
go.opentelemetry.io/collector/config/configtelemetry v0.94.1 h1:ztYpBEBlvhcoxMiDKNmQ2SS+A41JZ4M19GfcxjCo8Zs=
go.opentelemetry.io/collector/config/configtelemetry v0.94.1/go.mod h1:2XLhyR/GVpWeZ2K044vCmrvH/d4Ewt0aD/y46avZyMU=
go.opentelemetry.io/collector/confmap v0.94.1 h1:O69bkeyR1YPAFz+jMd45aDZc1DtYnwb3Skgr2yALPqQ=
go.opentelemetry.io/collector/confmap v0.94.1/go.mod h1:pCT5UtcHaHVJ5BIILv1Z2VQyjZzmT9uTdBmC9+Z0AgA=
go.opentelemetry.io/collector/consumer v0.92.0 h1:twa8T0iR9KVglvRbwZ5OPKLXPCC2DO6gVhrgDZ47MPE=
go.opentelemetry.io/collector/consumer v0.92.0/go.mod h1:fBZqP7bou3I7pDhWjleBuzdaLfQgJBc92wPJVOcKaGU=
go.opentelemetry.io/collector/exporter v0.92.0 h1:z6u+/hswJUuZbuPYIF2gXMZsqjIDd/tJO60XjLM850U=
go.opentelemetry.io/collector/exporter v0.92.0/go.mod h1:54ODYn1weY/Wr0bdxESj4P1fgyX+zaUsnJJnafORqIY=
go.opentelemetry.io/collector/extension v0.92.0 h1:zaehgW+LXCMNEb1d6Af/VHWphh5ZwX9aZl+NuQLGhpQ=
go.opentelemetry.io/collector/extension v0.92.0/go.mod h1:5EYwiaGU6deSY8YWqT5gvlnD850yJXP3NqFRKVVbYLs=
go.opentelemetry.io/collector/featuregate v1.0.1 h1:ok//hLSXttBbyu4sSV1pTx1nKdr5udSmrWy5sFMIIbM=
go.opentelemetry.io/collector/featuregate v1.0.1/go.mod h1:QQXjP4etmJQhkQ20j4P/rapWuItYxoFozg/iIwuKnYg=
go.opentelemetry.io/collector/pdata v1.1.0 h1:cE6Al1rQieUjMHro6p6cKwcu3sjHXGG59BZ3kRVUvsM=
go.opentelemetry.io/collector/pdata v1.1.0/go.mod h1:IDkDj+B4Fp4wWOclBELN97zcb98HugJ8Q2gA4ZFsN8Q=
go.opentelemetry.io/collector/receiver v0.92.0 h1:TRz4ufr5bFEszpAWgYVEx/b7VPZzEcECsyMzztf5PsQ=
go.opentelemetry.io/collector/receiver v0.92.0/go.mod h1:bYAAYbMuUVj3wx7ave2iyyJ+aGUpACliYOQ5xI92I7k=
go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E=
go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0=
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/prometheus v0.45.1 h1:R/bW3afad6q6VGU+MFYpnEdo0stEARMCdhWu6+JI6aI=
go.opentelemetry.io/otel/exporters/prometheus v0.45.1/go.mod h1:wnHAfKRav5Dfp4iZhyWZ7SzQfT+rDZpEpYG7To+qJ1k=
go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo=
go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo=
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.23.0 h1:0KM9Zl2esnl+WSukEmlaAEjVY5HDZANOHferLq36BPc=
go.opentelemetry.io/otel/sdk v1.23.0/go.mod h1:wUscup7byToqyKJSilEtMf34FgdCAsFpFOjXnAwFfO0=
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/sdk/metric v1.23.0 h1:u81lMvmK6GMgN4Fty7K7S6cSKOZhMKJMK2TB+KaTs0I=
go.opentelemetry.io/otel/sdk/metric v1.23.0/go.mod h1:2LUOToN/FdX6wtfpHybOnCZjoZ6ViYajJYMiJ1LKDtQ=
go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI=
go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
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=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go.wperron.io/flydetector v0.0.0-20240302202855-7f606a93cdb6 h1:SaV3yjkbcoC6sFgQbQhmVIYN9VSEMmvuUadAzBFe/YA=
go.wperron.io/flydetector v0.0.0-20240302202855-7f606a93cdb6/go.mod h1:XUxNxHOGyI6655abXX5+04qdore1sDRErBVmWFrmZV4=
go.wperron.io/sqliteexporter v0.1.0-rc5.0.20240209234500-ad89647cf9b6 h1:GbIjafkOpD/bWS2qR8PAOQAZPTQTQLk5XC4+d7DI7Fw=
go.wperron.io/sqliteexporter v0.1.0-rc5.0.20240209234500-ad89647cf9b6/go.mod h1:iQD28FG3zrdOEpKTGxvWCcLdir5eavk5bKjjN3RQ6Xc=
go.wperron.io/sqliteexporter v0.1.0 h1:0oK7DxZOMAwaGqRo5FVr9Nba3/IB5uO8Arfvi9rQADo=
go.wperron.io/sqliteexporter v0.1.0/go.mod h1:iQD28FG3zrdOEpKTGxvWCcLdir5eavk5bKjjN3RQ6Xc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -3,7 +3,7 @@ create view if not exists claimables as
trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '')
select 'trade' as typ, provinces.trade_node as name, name as province, id
select 'trade_node' as typ, provinces.trade_node as name, name as province, id
from provinces inner join trades on trades.trade_node = provinces.trade_node
union
select 'area' as typ, provinces.area as name, name as province, id

@ -1,15 +0,0 @@
drop table claimables;
create view if not exists claimables as
with
trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '')
select 'trade' as typ, provinces.trade_node as name, name as province, id
from provinces inner join trades on trades.trade_node = provinces.trade_node
union
select 'area' as typ, provinces.area as name, name as province, id
from provinces inner join areas on areas.area = provinces.area
union
select 'region' as typ, provinces.region as name, name as province, id
from provinces inner join regions on regions.region = provinces.region
;

@ -1,15 +0,0 @@
drop view claimables;
create table if not exists claimables as
with
trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '')
select 'trade' as typ, provinces.trade_node as name, name as province, id
from provinces inner join trades on trades.trade_node = provinces.trade_node
union
select 'area' as typ, provinces.area as name, name as province, id
from provinces inner join areas on areas.area = provinces.area
union
select 'region' as typ, provinces.region as name, name as province, id
from provinces inner join regions on regions.region = provinces.region
;

@ -6,7 +6,6 @@ import (
"time"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/trace"
)
var loc *time.Location
@ -16,10 +15,10 @@ func init() {
}
type Notifier struct {
c chan context.Context
c chan struct{}
}
func NewNotifier(c chan context.Context) *Notifier {
func NewNotifier(c chan struct{}) *Notifier {
return &Notifier{
c: c,
}
@ -46,11 +45,8 @@ func (n *Notifier) Start(ctx context.Context) {
log.Debug().Msg("context deadline exceeded, exiting notifier")
return
default:
log.Debug().Str("parent", "ticker").Msg("notifier tick")
ctx, span := tracer.Start(ctx, "notifier_tick", trace.WithNewRoot())
n.c <- ctx
span.End()
log.Debug().Msg("notifier tick")
n.c <- struct{}{}
}
ticker := time.NewTicker(time.Hour * 24 * 7)
@ -60,34 +56,27 @@ func (n *Notifier) Start(ctx context.Context) {
log.Debug().Msg("context deadline exceeded, exiting notifier")
return
case <-ticker.C:
log.Debug().Str("parent", "ticker").Msg("notifier tick")
ctx, span := tracer.Start(ctx, "notifier_tick", trace.WithNewRoot())
n.c <- ctx
span.End()
log.Debug().Msg("notifier tick")
n.c <- struct{}{}
}
time.Sleep(time.Second)
}
}
// Trigger the notifier manually. Should be used for testing purposes only.
func (n *Notifier) Send(ctx context.Context) {
log.Debug().Str("parent", "trigger").Ctx(ctx).Msg("notifier tick")
n.c <- ctx
func (n *Notifier) Send() {
n.c <- struct{}{}
}
func (n *Notifier) NotifyFunc(ctx context.Context, f func(context.Context)) {
func (n *Notifier) NotifyFunc(ctx context.Context, f func()) {
for {
select {
case <-ctx.Done():
log.Debug().Msg("context deadline exceeded, exiting notify func")
return
case innerCtx := <-n.c:
innerCtx, span := tracer.Start(innerCtx, "notify_func")
log.Debug().Ctx(innerCtx).Msg("tick received, notifying function")
f(innerCtx)
span.End()
case <-n.c:
log.Debug().Msg("tick received, notifying function")
f()
}
time.Sleep(time.Second)
}

@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"embed"
_ "embed"
"errors"
"fmt"
"strings"
@ -12,30 +13,23 @@ import (
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
"go.opentelemetry.io/otel/trace"
"github.com/rs/zerolog/log"
)
//go:embed migrations/*.sql
var migrations embed.FS
var tracer trace.Tracer
func init() {
tp := otel.GetTracerProvider()
tracer = tp.Tracer("themis")
}
type Store struct {
db *sql.DB
logger zerolog.Logger
db *sql.DB
}
func NewStore(db *sql.DB, logger zerolog.Logger) (*Store, error) {
func NewStore(conn string) (*Store, error) {
log.Debug().Str("connection_string", conn).Msg("opening sqlite3 database")
db, err := sql.Open("sqlite3", conn)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
d, err := iofs.New(migrations, "migrations")
if err != nil {
return nil, fmt.Errorf("failed to open iofs migration source: %w", err)
@ -61,35 +55,24 @@ func NewStore(db *sql.DB, logger zerolog.Logger) (*Store, error) {
return nil, fmt.Errorf("failed to get database migration version: %w", err)
}
logger.Debug().Uint("current_version", ver).Bool("dirty", dirty).Msg("running database migrations")
log.Debug().Uint("current_version", ver).Bool("dirty", dirty).Msg("running database migrations")
return &Store{
logger: logger,
db: db,
db: db,
}, nil
}
func (s *Store) Close() error {
s.logger.Debug().Msg("closing database")
log.Debug().Msg("closing database")
return s.db.Close()
}
func (s *Store) Claim(ctx context.Context, userId, player, province string, claimType ClaimType) (int, error) {
ctx, span := tracer.Start(ctx, "claim", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("insert"),
attribute.String("user_id", userId),
attribute.String("claim_name", province),
attribute.Stringer("claim_type", claimType),
))
defer span.End()
s.logger.Debug().
log.Debug().
Ctx(ctx).
Str("userid", userId).
Str("player", player).
Str("province", province).
Str("provice", province).
Stringer("claim_type", claimType).
Msg("inserting claim")
audit := &AuditableEvent{
@ -101,8 +84,6 @@ func (s *Store) Claim(ctx context.Context, userId, player, province string, clai
tx, err := s.db.Begin()
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to begin transaction")
return 0, fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Commit() //nolint:errcheck
@ -110,66 +91,50 @@ func (s *Store) Claim(ctx context.Context, userId, player, province string, clai
conflicts, err := s.FindConflicts(ctx, userId, province, claimType)
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "conflict check failed")
return 0, fmt.Errorf("failed to run conflicts check: %w", err)
}
if len(conflicts) > 0 {
s.logger.Debug().Ctx(ctx).Int("len", len(conflicts)).Msg("found conflicts")
audit.err = errors.New("found conflicts")
log.Debug().Ctx(ctx).Int("len", len(conflicts)).Msg("found conflicts")
audit.err = err
return 0, ErrConflict{Conflicts: conflicts}
}
// check that provided name matches the claim type
stmt, err := s.db.PrepareContext(ctx, `SELECT COUNT(1) FROM claimables WHERE lower(name) = ? and typ = ?`)
stmt, err := s.db.PrepareContext(ctx, fmt.Sprintf(`SELECT COUNT(1) FROM provinces WHERE LOWER(provinces.%s) = ?`, claimTypeToColumn[claimType]))
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return 0, fmt.Errorf("failed to prepare count query: %w", err)
}
defer stmt.Close()
row := stmt.QueryRowContext(ctx, strings.ToLower(province), claimType)
row := stmt.QueryRowContext(ctx, strings.ToLower(province))
var count int
err = row.Scan(&count)
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return 0, fmt.Errorf("failed to scan: %w", err)
}
if count == 0 {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "no matching provinces found")
return 0, fmt.Errorf("found no provinces for %s named %s", claimType, province)
}
stmt, err = s.db.PrepareContext(ctx, "INSERT INTO claims (player, claim_type, val, userid) VALUES (?, ?, ?, ?)")
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return 0, fmt.Errorf("failed to prepare claim query: %w", err)
}
defer stmt.Close()
res, err := stmt.ExecContext(ctx, player, claimType, province, userId)
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return 0, fmt.Errorf("failed to insert claim: %w", err)
}
id, err := res.LastInsertId()
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to last insert id")
return 0, fmt.Errorf("failed to get last ID: %w", err)
}
@ -177,42 +142,25 @@ func (s *Store) Claim(ctx context.Context, userId, player, province string, clai
}
func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, search ...string) ([]string, error) {
ctx, span := tracer.Start(ctx, "list_availability", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claimables"),
semconv.DBOperation("select"),
attribute.StringSlice("search_terms", search),
attribute.Stringer("claim_type", claimType),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Stringer("claim_type", claimType).Strs("search_terms", search).Msg("listing available entries")
log.Debug().Ctx(ctx).Stringer("claim_type", claimType).Strs("search_terms", search).Msg("listing available entries")
queryParams := []any{string(claimType)}
queryPattern := `SELECT distinct name
FROM claimables
LEFT JOIN claims ON claimables.name = claims.val AND claimables.typ = claims.claim_type
queryPattern := `SELECT DISTINCT(provinces.%[1]s)
FROM provinces LEFT JOIN claims ON provinces.%[1]s = claims.val AND claims.claim_type = ?
WHERE claims.val IS NULL
AND claimables.typ = ?`
AND provinces.typ = 'Land'`
if len(search) > 0 && search[0] != "" {
// only take one search param, ignore the rest
queryPattern += `AND claimables.name LIKE ?`
queryPattern += `AND provinces.%[1]s LIKE ?`
queryParams = append(queryParams, fmt.Sprintf("%%%s%%", search[0]))
}
stmt, err := s.db.PrepareContext(ctx, queryPattern)
stmt, err := s.db.PrepareContext(ctx, fmt.Sprintf(queryPattern, claimTypeToColumn[claimType]))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare query: %w", err)
}
defer stmt.Close()
rows, err := stmt.QueryContext(ctx, queryParams...)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err)
}
@ -220,8 +168,6 @@ func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, searc
for rows.Next() {
var s string
if err := rows.Scan(&s); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan rows: %w", err)
}
avail = append(avail, s)
@ -231,26 +177,14 @@ func (s *Store) ListAvailability(ctx context.Context, claimType ClaimType, searc
}
func (s *Store) ListClaims(ctx context.Context) ([]Claim, error) {
ctx, span := tracer.Start(ctx, "list_claims", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Msg("listing all claims currently in database")
log.Debug().Ctx(ctx).Msg("listing all claims currently in database")
stmt, err := s.db.PrepareContext(ctx, `SELECT id, player, claim_type, val FROM claims`)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return nil, fmt.Errorf("failed to prepare query: %w", err)
}
defer stmt.Close()
rows, err := stmt.QueryContext(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return nil, fmt.Errorf("failed to execute query: %w", err)
}
@ -260,14 +194,10 @@ func (s *Store) ListClaims(ctx context.Context) ([]Claim, error) {
var rawType string
err = rows.Scan(&c.ID, &c.Player, &rawType, &c.Name)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return nil, fmt.Errorf("failed to scan row: %w", err)
}
cl, err := ClaimTypeFromString(rawType)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse claim type")
return nil, fmt.Errorf("unexpected error converting raw claim type: %w", err)
}
c.Type = cl
@ -287,28 +217,17 @@ func (cd ClaimDetail) String() string {
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf("%s\n", cd.Claim))
for _, p := range cd.Provinces {
sb.WriteString(fmt.Sprintf("- %s\n", p))
sb.WriteString(fmt.Sprintf(" - %s\n", p))
}
return sb.String()
}
func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error) {
ctx, span := tracer.Start(ctx, "describe_claim", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
attribute.Int("claim_id", ID),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Int("id", ID).Msg("describing claim")
log.Debug().Ctx(ctx).Int("id", ID).Msg("describing claim")
stmt, err := s.db.PrepareContext(ctx, `SELECT id, player, claim_type, val FROM claims WHERE id = ?`)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return ClaimDetail{}, fmt.Errorf("failed to prepare select claim query: %w", err)
return ClaimDetail{}, fmt.Errorf("failed to get claim: %w", err)
}
defer stmt.Close()
row := stmt.QueryRowContext(ctx, ID)
@ -316,34 +235,24 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
var rawType string
err = row.Scan(&c.ID, &c.Player, &rawType, &c.Name)
if err == sql.ErrNoRows {
span.RecordError(ErrNoSuchClaim)
return ClaimDetail{}, ErrNoSuchClaim
}
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return ClaimDetail{}, fmt.Errorf("failed to scan row: %w", err)
}
cl, err := ClaimTypeFromString(rawType)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse claim type")
return ClaimDetail{}, fmt.Errorf("unexpected error converting raw claim type: %w", err)
}
c.Type = cl
stmt, err = s.db.PrepareContext(ctx, `SELECT province FROM claimables WHERE name = ? AND typ = ?`)
stmt, err = s.db.PrepareContext(ctx, fmt.Sprintf(`SELECT name FROM provinces where provinces.%s = ?`, claimTypeToColumn[cl]))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return ClaimDetail{}, fmt.Errorf("failed to prepare query: %w", err)
}
defer stmt.Close()
rows, err := stmt.QueryContext(ctx, c.Name, cl)
rows, err := stmt.QueryContext(ctx, c.Name)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return ClaimDetail{}, fmt.Errorf("failed to execute query: %w", err)
}
@ -352,8 +261,6 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
var p string
err = rows.Scan(&p)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return ClaimDetail{}, fmt.Errorf("failed to scan result set: %w", err)
}
provinces = append(provinces, p)
@ -366,16 +273,7 @@ func (s *Store) DescribeClaim(ctx context.Context, ID int) (ClaimDetail, error)
}
func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error {
ctx, span := tracer.Start(ctx, "delete_claim", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("delete"),
attribute.Int("claim_id", ID),
attribute.String("user_id", userId),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Str("userid", userId).Int("id", ID).Msg("deleting claim")
log.Debug().Ctx(ctx).Str("userid", userId).Int("id", ID).Msg("deleting claim")
audit := &AuditableEvent{
userId: userId,
eventType: EventUnclaim,
@ -385,57 +283,37 @@ func (s *Store) DeleteClaim(ctx context.Context, ID int, userId string) error {
stmt, err := s.db.PrepareContext(ctx, "DELETE FROM claims WHERE id = ? AND userid = ?")
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return fmt.Errorf("failed to prepare query: %w", err)
}
defer stmt.Close()
res, err := stmt.ExecContext(ctx, ID, userId)
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return fmt.Errorf("failed to delete claim ID %d: %w", ID, err)
}
rows, err := res.RowsAffected()
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get number of affected rows")
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rows == 0 {
audit.err = ErrNoSuchClaim
span.RecordError(ErrNoSuchClaim)
return ErrNoSuchClaim
}
return nil
}
func (s *Store) CountClaims(ctx context.Context) (total, uniquePlayers int, err error) {
ctx, span := tracer.Start(ctx, "count_claims", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("select"),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Msg("counting all claims and unique users")
log.Debug().Ctx(ctx).Msg("counting all claims and unique users")
stmt, err := s.db.PrepareContext(ctx, "SELECT COUNT(1), COUNT(DISTINCT(userid)) FROM claims")
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to prepare query")
return 0, 0, fmt.Errorf("failed to prepare query: %w", err)
}
defer stmt.Close()
res := stmt.QueryRowContext(ctx)
if err := res.Scan(&total, &uniquePlayers); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return 0, 0, fmt.Errorf("failed to scan result: %w", err)
}
@ -443,14 +321,7 @@ func (s *Store) CountClaims(ctx context.Context) (total, uniquePlayers int, err
}
func (s *Store) Flush(ctx context.Context, userId string) error {
ctx, span := tracer.Start(ctx, "flush", trace.WithAttributes(
semconv.DBSystemSqlite,
semconv.DBSQLTable("claims"),
semconv.DBOperation("delete"),
))
defer span.End()
s.logger.Debug().Ctx(ctx).Str("initiated_by", userId).Msg("flushing all currently help claims")
log.Debug().Ctx(ctx).Str("initiated_by", userId).Msg("flushing all currently help claims")
audit := &AuditableEvent{
userId: userId,
eventType: EventFlush,
@ -460,8 +331,6 @@ func (s *Store) Flush(ctx context.Context, userId string) error {
_, err := s.db.ExecContext(ctx, "DELETE FROM claims;")
if err != nil {
audit.err = err
span.RecordError(err)
span.SetStatus(codes.Error, "failed to execute query")
return fmt.Errorf("failed to execute delete query: %w", err)
}
return nil

@ -3,13 +3,11 @@ package themis
import (
"context"
"database/sql"
_ "embed"
"fmt"
"testing"
_ "github.com/mattn/go-sqlite3"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -17,9 +15,7 @@ import (
const TEST_CONN_STRING_PATTERN = "file:%s?mode=memory&cache=shared"
func TestStore_Claim(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Claim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Claim"))
require.NoError(t, err)
type args struct {
@ -101,20 +97,16 @@ func TestStore_Claim(t *testing.T) {
t.Errorf("Store.Claim() error = %v, wantErr %v", err, tt.wantErr)
}
if !tt.wantErr {
ae, err := store.LastOf(context.TODO(), EventClaim)
require.NoError(t, err)
assert.Greater(t, ae.Id, lastAudit)
lastAudit = ae.Id
}
ae, err := store.LastOf(context.TODO(), EventClaim)
require.NoError(t, err)
assert.Greater(t, ae.Id, lastAudit)
lastAudit = ae.Id
})
}
}
func TestAvailability(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Availability"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestAvailability"))
assert.NoError(t, err)
store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -130,22 +122,22 @@ func TestAvailability(t *testing.T) {
store.Claim(context.TODO(), "000000000000000001", "foo", "France", CLAIM_TYPE_REGION)
store.Claim(context.TODO(), "000000000000000001", "foo", "Italy", CLAIM_TYPE_REGION)
// There's a total of 92 distinct regions, there should be 90 available
// There's a total of 73 distinct regions, there should be 71 available
// after the two claims above
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_REGION)
assert.NoError(t, err)
assert.Equal(t, 90, len(availability))
assert.Equal(t, 71, len(availability))
store.Claim(context.TODO(), "000000000000000001", "foo", "Normandy", CLAIM_TYPE_AREA)
store.Claim(context.TODO(), "000000000000000001", "foo", "Champagne", CLAIM_TYPE_AREA)
store.Claim(context.TODO(), "000000000000000001", "foo", "Lorraine", CLAIM_TYPE_AREA)
store.Claim(context.TODO(), "000000000000000001", "foo", "Provence", CLAIM_TYPE_AREA)
// There's a total of 882 distinct regions, there should be 878 available
// There's a total of 823 distinct regions, there should be 819 available
// after the four claims above
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA)
assert.NoError(t, err)
assert.Equal(t, 878, len(availability))
assert.Equal(t, 819, len(availability))
// There is both a Trade Node and an Area called 'Valencia', while the trade
// node is claimed, the area should show up in the availability list (even
@ -153,17 +145,15 @@ func TestAvailability(t *testing.T) {
store.Claim(context.TODO(), "000000000000000001", "foo", "Valencia", CLAIM_TYPE_TRADE)
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA)
assert.NoError(t, err)
assert.Equal(t, 878, len(availability)) // availability for areas should be the same as before
assert.Equal(t, 819, len(availability)) // availability for areas should be the same as before
availability, err = store.ListAvailability(context.TODO(), CLAIM_TYPE_AREA, "bay")
assert.NoError(t, err)
assert.Equal(t, 6, len(availability)) // availability for areas should be the same as before
assert.Equal(t, 3, len(availability)) // availability for areas should be the same as before
}
func TestDeleteClaim(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_DeleteClaim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestDeleteClaim"))
assert.NoError(t, err)
// make sure all claims are gone, this is due to how the in-memory database
// with a shared cache interacts with other tests running in parallel
@ -195,9 +185,7 @@ func TestDeleteClaim(t *testing.T) {
}
func TestDescribeClaim(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_DescribeClaim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestDescribeClaim"))
assert.NoError(t, err)
id, err := store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -213,9 +201,7 @@ func TestDescribeClaim(t *testing.T) {
}
func TestCountClaims(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_CountClaim"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestCountClaims"))
assert.NoError(t, err)
store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)
@ -231,9 +217,7 @@ func TestCountClaims(t *testing.T) {
}
func TestFlush(t *testing.T) {
db, err := sql.Open("sqlite3", fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestStore_Flush"))
require.NoError(t, err)
store, err := NewStore(db, zerolog.Nop())
store, err := NewStore(fmt.Sprintf(TEST_CONN_STRING_PATTERN, "TestFlush"))
assert.NoError(t, err)
store.Claim(context.TODO(), "000000000000000001", "foo", "Genoa", CLAIM_TYPE_TRADE)

Loading…
Cancel
Save