Compare commits

..

25 Commits

Author SHA1 Message Date
William Perron 2b3ff1aa04
improve instrumentation of internal notifier
6 months ago
William Perron 18165a827c
add some emojis
7 months ago
William Perron 3c4de5a8c3
enable propagation of the weekly notification spans
9 months ago
William Perron 451a113987
add root span for weekly reminder
9 months ago
William Perron 66a0fd92b7
add fly detector to otel setup
9 months ago
William Perron 96df84736b
mark some responses as ephemeral and enable cpu profiling
9 months ago
William Perron 13a65780d8
remove unused import
9 months ago
William Perron 619fa082ac
fix failing test
9 months ago
William Perron d52efdce94
fix lints and update logger
9 months ago
William Perron 8c7099796c
update sqlitexporter dep
9 months ago
William Perron a46de17b7c
refactor discord command span in its own middleware
10 months ago
William Perron 6b99919dd7
fix context TODO usage in main.go
10 months ago
William Perron d96cd1a0ee
bump system deps
10 months ago
William Perron e95f741820
bump sqliteexporter dependency and improve error handling for init tracing
10 months ago
William Perron 6404b508e8
update deps and go mod tidy
10 months ago
William Perron 3e8d49c0c0
add service name attribute and fix context for flush
10 months ago
William Perron 6b280e7b33
finish instrumenting with opentelemetry
10 months ago
William Perron cc45c55922
instrumented absences with opentelemetry
10 months ago
William Perron 27567d16a2
convert to opentelemetry part 1
10 months ago
William Perron 1646762081
wip: refactor command logging and tracing
10 months ago
William Perron 4c09153756
wip: implement embedded distributed tracing
10 months ago
William Perron 407d63d4e6
remove unused test function
11 months ago
William Perron 1097ce5fce
use table instead of view for better perf
11 months ago
William Perron 31f2813192
fix scheduled message emoji error
11 months ago
William Perron 171ef4ee98
Add `claimables` view to database
11 months ago

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

@ -3,13 +3,13 @@ WORKDIR /app
COPY . . COPY . .
RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/... RUN mkdir ./bin; go build -buildvcs=false -o ./bin ./cmd/...
FROM ubuntu:22.04 as litestream FROM ubuntu:23.10 as litestream
WORKDIR /download WORKDIR /download
RUN apt update -y && apt install -y wget tar 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; \ 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; tar -zxf litestream-v0.3.9-linux-amd64.tar.gz;
FROM ubuntu:22.04 FROM ubuntu:23.10
WORKDIR /themis WORKDIR /themis
COPY --from=builder /app/bin/themis-server /usr/local/bin/themis-server COPY --from=builder /app/bin/themis-server /usr/local/bin/themis-server
COPY --from=litestream /download/litestream /usr/local/bin/litestream 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 ### Requirements
To develop: To develop:
- [Go](https://go.dev/) version 1.19 or higher installed locally - [Go](https://go.dev/) version 1.21 or higher installed locally
- `sqlite3` installed locally (already ships by default on most OSes) - `sqlite3` installed locally (already ships by default on most OSes)
To deploy: To deploy:

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

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

@ -3,11 +3,14 @@ package themis
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"time" "time"
"github.com/rs/zerolog/log" "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 type EventType int
@ -45,36 +48,50 @@ func EventTypeFromString(ev string) (EventType, error) {
case "ABSENT": case "ABSENT":
return EventAbsence, nil return EventAbsence, nil
default: default:
return EventType(9999), errors.New("no such event type") return EventType(9999), fmt.Errorf("no such event type: %s", ev)
} }
} }
type AuditableEvent struct { type AuditableEvent struct {
userId string userId string
eventType EventType eventType EventType
timestamp time.Time
err error err error
} }
// Audit writes to the audit table, returns nothing because it is meant to be // 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. // used in a defered statement on functions that write to the database.
func (s *Store) Audit(ctx context.Context, ev *AuditableEvent) { 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 { if ev.err == nil {
log.Debug().Ctx(ctx).Str("event_type", ev.eventType.String()).Str("userid", ev.userId).Msg("recording audit log") log.Debug().Ctx(ctx).Str("event_type", ev.eventType.String()).Str("userid", ev.userId).Msg("recording audit log")
ctx := context.Background() ctx := context.Background()
tx, err := s.db.Begin() tx, err := s.db.Begin()
if err != nil { 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") log.Error().Ctx(ctx).Err(err).Msg("failed to start transaction")
} }
defer tx.Commit() //nolint:errcheck defer tx.Commit() //nolint:errcheck
stmt, err := s.db.PrepareContext(ctx, "INSERT INTO audit_log (userid, event_type, ts) VALUES (?, ?, ?)") stmt, err := s.db.PrepareContext(ctx, "INSERT INTO audit_log (userid, event_type, ts) VALUES (?, ?, ?)")
if err != nil { 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") 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 { 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") log.Error().Ctx(ctx).Err(err).Msg("failed to insert audit log")
} }
} }
@ -88,10 +105,20 @@ type AuditEvent struct {
} }
func (s *Store) LastOf(ctx context.Context, t EventType) (AuditEvent, error) { 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") 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`) 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 { 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) return AuditEvent{}, fmt.Errorf("failed to get last event of type %s: %w", t.String(), err)
} }
@ -101,15 +128,20 @@ func (s *Store) LastOf(ctx context.Context, t EventType) (AuditEvent, error) {
var rawEventType string var rawEventType string
err = row.Scan(&ev.Id, &ev.UserId, &rawEventType, &ev.Timestamp) err = row.Scan(&ev.Id, &ev.UserId, &rawEventType, &ev.Timestamp)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
span.RecordError(ErrNever)
return AuditEvent{}, ErrNever return AuditEvent{}, ErrNever
} }
if err != nil { if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to scan row")
return AuditEvent{}, fmt.Errorf("failed to scan row: %w", err) return AuditEvent{}, fmt.Errorf("failed to scan row: %w", err)
} }
ev.EventType, err = EventTypeFromString(rawEventType) ev.EventType, err = EventTypeFromString(rawEventType)
if err != nil { if err != nil {
return AuditEvent{}, fmt.Errorf("failed to parse event type %s: %w", rawEventType, err) span.RecordError(err)
span.SetStatus(codes.Error, "failed to parse event type")
return AuditEvent{}, fmt.Errorf("failed to parse event type: %w", err)
} }
return ev, nil return ev, nil

@ -34,12 +34,6 @@ const (
CLAIM_TYPE_TRADE = "trade" 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 { type Claim struct {
ID int ID int
Player string Player string

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

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

@ -1,16 +0,0 @@
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)
}
}

@ -1,47 +0,0 @@
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())
}

@ -1,18 +0,0 @@
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())
}

@ -1,19 +0,0 @@
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
}

@ -1,18 +0,0 @@
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
}

@ -0,0 +1,44 @@
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,6 +7,7 @@ processes = []
[env] [env]
TZ = "America/New_York" TZ = "America/New_York"
ENV = "production"
DISCORD_APP_ID = "1014881815921705030" DISCORD_APP_ID = "1014881815921705030"
DISCORD_GUILD_ID = "375417755777892353" DISCORD_GUILD_ID = "375417755777892353"
DISCORD_BOT_CHANNEL_ID = "1018997240968265768" DISCORD_BOT_CHANNEL_ID = "1018997240968265768"

@ -7,13 +7,23 @@ import (
"strings" "strings"
"github.com/rs/zerolog/log" "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) { 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{} sb := strings.Builder{}
cols, err := rows.Columns() cols, err := rows.Columns()
if err != nil { 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) return "", fmt.Errorf("failed to get rows columns: %w", err)
} }
@ -37,6 +47,8 @@ func FormatRows(ctx context.Context, rows *sql.Rows) (string, error) {
row[i] = new(sql.NullString) row[i] = new(sql.NullString)
} }
if err := rows.Scan(row...); err != nil { 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) return "", fmt.Errorf("failed to scan next row: %w", err)
} }

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

@ -1,13 +1,54 @@
module go.wperron.io/themis module go.wperron.io/themis
go 1.19 go 1.21.1
require ( require (
github.com/bwmarrin/discordgo v0.26.1 github.com/bwmarrin/discordgo v0.26.1
github.com/golang-migrate/migrate/v4 v4.16.2 github.com/golang-migrate/migrate/v4 v4.17.0
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.19
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/stretchr/testify v1.8.1 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
) )
require ( require (
@ -18,8 +59,10 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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 go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.7.0 // indirect golang.org/x/crypto v0.18.0 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

258
go.sum

@ -1,12 +1,70 @@
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 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= 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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 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/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= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -14,18 +72,58 @@ 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/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 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 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-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.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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
@ -35,24 +133,162 @@ 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.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.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.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.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 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 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.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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-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.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.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.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/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.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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 != ''), trades as (select distinct trade_node from provinces where trade_node != ''),
areas as (select distinct area from provinces where area != ''), areas as (select distinct area from provinces where area != ''),
regions as (select distinct region from provinces where region != '') regions as (select distinct region from provinces where region != '')
select 'trade_node' as typ, provinces.trade_node as name, name as province, id 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 from provinces inner join trades on trades.trade_node = provinces.trade_node
union union
select 'area' as typ, provinces.area as name, name as province, id select 'area' as typ, provinces.area as name, name as province, id

@ -0,0 +1,15 @@
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
;

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

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

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

Loading…
Cancel
Save