Backport event logging from v4

This commit is contained in:
Neale Pickett 2020-11-11 13:13:09 -07:00
parent cf241b7a0e
commit a1bba870d1
6 changed files with 150 additions and 22 deletions

View File

@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- Backport event logging from v4
## [v3.5.1] - 2020-03-16 ## [v3.5.1] - 2020-03-16
### Fixed ### Fixed

81
docs/logs.md Normal file
View File

@ -0,0 +1,81 @@
Moth Logs
=======
Moth has multiple log channels:
`points.log`
: the points log, used by server and scoreboard
`events.log`
: significant events, used to do manual analysis after an event
`stdout`
: HTTP server access
`stderr`
: warnings and errors
`points.log` format
----------------------
The points log is a space-separated file.
Each line has four fields:
| `timestamp` | `teamID` | `category` | `points` |
| --- | --- | --- | --- |
| int | string | string | int |
| Unix epoch | Team's unique ID | Name of category | Points awarded |
### Example
```
1602702696 2255 nocode 1
1602702705 2255 sequence 1
1602702787 2255 nocode 2
1602702831 2255 sequence 2
1602702839 9458 nocode 3
1602702896 2255 sequence 8
1602702900 9458 nocode 4
1602702913 2255 sequence 16
```
`events.log` format
----------------------
The events log is a space-separated file.
Each line has six fields minimum:
| `timestamp` | `event` | `participantID` | `teamID` | `category` | `points` | `extra`... |
| --- | --- | --- | --- | --- | --- | --- |
| int | string | string | string | string | int | string... |
| Unix epoch | Event type | Team's unique ID| Participant's (hopefully) unique ID | Name of category, if any | Points awarded, if any | Additional fields, if any |
Fields after `points` contain extra fields associated with the event.
### Event types
These may change in the future.
* init: startup of server
* disabled: points accumulation disabled for this maintenance run
* wrong: wrong answer submitted
* correct: correct answer submitted
### Example
```
1602716345 init - - - - 0
1602716349 load 2255 player5 sequence 1
1602716450 load 4824 player3 sequence 1
1602716359 correct 2255 player5 sequence 1
1602716423 wrong 4824 player3 sequence 1
1602716428 correct 4824 player3 sequence 1
1602716530 correct 4824 player3 sequence 1
1602716546 abduction 4824 player3 - 0 alien FM1490
```
The final entry is a made-up "alien abduction" entry,
since at the time of writing,
we didn't have any actual events that wrote extra fields.

View File

@ -98,6 +98,7 @@ func (ctx *Instance) registerHandler(w http.ResponseWriter, req *http.Request) {
"Team registered", "Team registered",
"Your team has been named and you may begin using your team ID!", "Your team has been named and you may begin using your team ID!",
) )
ctx.LogEvent("register", req.FormValue("pid"), teamId, "", 0)
} }
func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) { func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
@ -152,6 +153,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
"Wrong answer", "Wrong answer",
"That is not the correct answer for %s %d.", category, points, "That is not the correct answer for %s %d.", category, points,
) )
ctx.LogEvent("wrong", req.FormValue("pid"), teamId, category, points)
return return
} }
@ -168,6 +170,7 @@ func (ctx *Instance) answerHandler(w http.ResponseWriter, req *http.Request) {
"Points awarded", "Points awarded",
fmt.Sprintf("%d points for %s!", points, teamId), fmt.Sprintf("%d points for %s!", points, teamId),
) )
ctx.LogEvent("correct", req.FormValue("pid"), teamId, category, points)
} }
func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) { func (ctx *Instance) puzzlesHandler(w http.ResponseWriter, req *http.Request) {
@ -342,7 +345,7 @@ func (ctx *Instance) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
clientIP := r.RemoteAddr clientIP := r.RemoteAddr
if (ctx.UseXForwarded) { if ctx.UseXForwarded {
forwardedIP := r.Header.Get("X-Forwarded-For") forwardedIP := r.Header.Get("X-Forwarded-For")
forwardedIP = strings.Split(forwardedIP, ", ")[0] forwardedIP = strings.Split(forwardedIP, ", ")[0]

View File

@ -34,6 +34,8 @@ type Instance struct {
update chan bool update chan bool
jPuzzleList []byte jPuzzleList []byte
jPointsLog []byte jPointsLog []byte
eventStream chan string
eventLogWriter io.WriteCloser
nextAttempt map[string]time.Time nextAttempt map[string]time.Time
nextAttemptMutex *sync.RWMutex nextAttemptMutex *sync.RWMutex
mux *http.ServeMux mux *http.ServeMux
@ -47,10 +49,17 @@ func (ctx *Instance) Initialize() error {
if _, err := os.Stat(ctx.StateDir); err != nil { if _, err := os.Stat(ctx.StateDir); err != nil {
return err return err
} }
if f, err := os.OpenFile(ctx.StatePath("events.log"), os.O_RDWR|os.O_CREATE, 0644); err != nil {
return err
} else {
// This stays open for the life of the process
ctx.eventLogWriter = f
}
ctx.Base = strings.TrimRight(ctx.Base, "/") ctx.Base = strings.TrimRight(ctx.Base, "/")
ctx.categories = map[string]*Mothball{} ctx.categories = map[string]*Mothball{}
ctx.update = make(chan bool, 10) ctx.update = make(chan bool, 10)
ctx.eventStream = make(chan string, 80)
ctx.nextAttempt = map[string]time.Time{} ctx.nextAttempt = map[string]time.Time{}
ctx.nextAttemptMutex = new(sync.RWMutex) ctx.nextAttemptMutex = new(sync.RWMutex)
ctx.mux = http.NewServeMux() ctx.mux = http.NewServeMux()
@ -84,6 +93,7 @@ func (ctx *Instance) MaybeInitialize() {
os.Remove(ctx.StatePath("until")) os.Remove(ctx.StatePath("until"))
os.Remove(ctx.StatePath("disabled")) os.Remove(ctx.StatePath("disabled"))
os.Remove(ctx.StatePath("points.log")) os.Remove(ctx.StatePath("points.log"))
os.Remove(ctx.StatePath("events.log"))
os.RemoveAll(ctx.StatePath("points.tmp")) os.RemoveAll(ctx.StatePath("points.tmp"))
os.RemoveAll(ctx.StatePath("points.new")) os.RemoveAll(ctx.StatePath("points.new"))
@ -102,6 +112,9 @@ func (ctx *Instance) MaybeInitialize() {
} }
} }
// Record that we did all this
ctx.LogEvent("init", "", "", "", 0)
// Create initialized file that signals whether we're set up // Create initialized file that signals whether we're set up
f, err := os.OpenFile(ctx.StatePath("initialized"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) f, err := os.OpenFile(ctx.StatePath("initialized"), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil { if err != nil {
@ -111,6 +124,30 @@ func (ctx *Instance) MaybeInitialize() {
fmt.Fprintln(f, "Remove this file to reinitialize the contest") fmt.Fprintln(f, "Remove this file to reinitialize the contest")
} }
func logstr(s string) string {
if s == "" {
return "-"
}
return s
}
// LogEvent writes to the event log
func (ctx *Instance) LogEvent(event, participantID, teamID, cat string, points int, extra ...string) {
event = strings.ReplaceAll(event, " ", "-")
msg := fmt.Sprintf(
"%s %s %s %s %d",
logstr(event),
logstr(participantID),
logstr(teamID),
logstr(cat),
points,
)
for _, x := range extra {
msg = msg + " " + strings.ReplaceAll(x, " ", "-")
}
ctx.eventStream <- msg
}
func pathCleanse(parts []string) string { func pathCleanse(parts []string) string {
clean := make([]string, len(parts)) clean := make([]string, len(parts))
for i := range parts { for i := range parts {

View File

@ -322,10 +322,14 @@ func (ctx *Instance) Maintenance(maintenanceInterval time.Duration) {
ctx.collectPoints() ctx.collectPoints()
ctx.generatePuzzleList() ctx.generatePuzzleList()
ctx.generatePointsLog("") ctx.generatePointsLog("")
} else {
ctx.LogEvent("disabled", "", "", "", 0)
} }
select { select {
case <-ctx.update: case <-ctx.update:
// log.Print("Forced update") // log.Print("Forced update")
case msg := <-ctx.eventStream:
fmt.Fprintln(ctx.eventLogWriter, msg)
case <-time.After(maintenanceInterval): case <-time.After(maintenanceInterval):
// log.Print("Housekeeping...") // log.Print("Housekeeping...")
} }

View File

@ -73,6 +73,7 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer ctx.eventLogWriter.Close()
// Add some MIME extensions // Add some MIME extensions
// Doing this avoids decompressing a mothball entry twice per request // Doing this avoids decompressing a mothball entry twice per request