3
\$\begingroup\$

I've been learning Go for a few months and this is one of my first projects with it. It's an analytics beacon for my low-volume blog; it emits a tracking pixel and logs parameters from the browser to a database. I'm hoping to use it in addition to Google Analytics, so it isn't mission critical or anything.

I'd appreciate feedback on the general structure of the project and whether I'm using Go's concurrency effectively.

main.go

package main

import (
    "database/sql"
    "encoding/base64"
    "github.com/caarlos0/env"
    _ "github.com/mattn/go-sqlite3"
    "log"
    "net/http"
    "time"
)

const (
    pixelRaw   = "R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
    pixelRoute = "/a.gif"
)

type Config struct {
    Port                 string `env:"PORT"             envDefault:"8080"`
    DBFile               string `env:"DB_FILE"          envDefault:"events.db"`
    MaxConnections       int    `env:"MAX_CONNECTIONS"  envDefault:"500000"`
    WriteQueueSize       int    `env:"WRITE_QUEUE_SIZE" envDefault:"100000"`
    WriteFrequencyMillis int    `env:"WRITE_FREQUENCY"  envDefault:"15"`
}

func main() {
    var config Config
    if err := env.Parse(&config); err != nil {
        log.Fatal(err)
    }

    db := initDB(&config)
    defer db.Close()

    eventQueue := make(chan *PageEvent, config.MaxConnections)

    go writeEvents(&config, db, eventQueue)
    http.HandleFunc(pixelRoute, makeHandlePixel(eventQueue))

    log.Fatal(http.ListenAndServe(":"+config.Port, nil))
}

func makeHandlePixel(eventQueue chan *PageEvent) http.HandlerFunc {
    pixel, err := base64.StdEncoding.DecodeString(pixelRaw)
    if err != nil {
        log.Fatalf("failed to encode pixel: %v", err)
    }

    return func(out http.ResponseWriter, req *http.Request) {
        eventQueue <- PageEventFromRequest(req)
        out.Header().Set("Content-Type", "image/gif")
        out.WriteHeader(http.StatusOK)
        out.Write(pixel)
    }
}

func writeEvents(config *Config, db *sql.DB, eventQueue chan *PageEvent) {
    writeFrequency := time.Duration(time.Duration(config.WriteFrequencyMillis) * time.Millisecond)
    writeTimer := time.NewTimer(writeFrequency)
    writeQueue := make([]*PageEvent, 0, config.WriteQueueSize)

    for {
        select {
        case ev := <-eventQueue:
            writeQueue = append(writeQueue, ev)

        case <-writeTimer.C:
            if len(writeQueue) > 0 {
                writeTimer.Stop()

                db.Exec("BEGIN TRANSACTION;")
                for _, ev := range writeQueue {
                    ev.InsertIntoDB(db)
                }
                db.Exec("END TRANSACTION;")

                log.Println("wrote", len(writeQueue), "records.")
                writeQueue = writeQueue[:0]
            }
            writeTimer.Reset(writeFrequency)
        }
    }
}

func initDB(config *Config) *sql.DB {
    db, err := sql.Open("sqlite3", config.DBFile)

    if err != nil {
        log.Fatal(err)
    }
    if db == nil {
        log.Fatal("database is null")
    }

    _, err = db.Exec(SQLPageEventCreateTable)
    if err != nil {
        log.Fatal(err)
    }

    return db
}

events.go

package main

import (
    "database/sql"
    "log"
    "net/http"
    "time"
)

const (
    PageEventSchemaVersion  = 1
    SQLPageEventCreateTable = `
        CREATE TABLE IF NOT EXISTS events (
            schema_version   integer,
            script_version   integer,
            datetime         datetime NOT NULL,
            server           text,
            remote_addr      text,
            user_agent       text,
            request_referrer text,
            title            text,
            referrer         text,
            url              text,
            event_type       text,
            session_token    text,
            user_token       text
        );
        PRAGMA journal_mode = MEMORY;
    `
    SQLPageEventInsert = `
        INSERT INTO events(
            schema_version,
            script_version,
            datetime,
            server,
            remote_addr,
            user_agent,
            request_referrer,
            title,
            referrer,
            url,
            event_type,
            session_token,
            user_token
        )
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
    `
)

type PageEvent struct {
    ScriptVersion   string    `json:"script_version"`
    Time            time.Time `json:"datetime"`
    Host            string    `json:"server"`
    RemoteAddr      string    `json:"remote_addr"`
    UserAgent       string    `json:"user_agent"`
    RequestReferrer string    `json:"request_referrer"`
    Title           string    `json:"title"`
    PageReferrer    string    `json:"referrer"`
    URL             string    `json:"url"`
    EventType       string    `json:"event_type"`
    SessionToken    string    `json:"session_token"`
    UserToken       string    `json:"user_token"`
}

func PageEventFromRequest(req *http.Request) *PageEvent {
    params := req.URL.Query()
    return &PageEvent{
        Host:            req.Host,
        RemoteAddr:      req.RemoteAddr,
        UserAgent:       req.UserAgent(),
        RequestReferrer: req.Referer(),
        Time:            time.Now(),
        Title:           params.Get("title"),
        PageReferrer:    params.Get("referrer"),
        URL:             params.Get("url"),
        ScriptVersion:   params.Get("version"),
        EventType:       params.Get("event_type"),
        SessionToken:    params.Get("session_token"),
        UserToken:       params.Get("user_token"),
    }
}

func (ev PageEvent) InsertIntoDB(db *sql.DB) {
    stmt, err := db.Prepare(SQLPageEventInsert)
    if err != nil {
        log.Fatal(err)
    }
    defer stmt.Close()

    _, err = stmt.Exec(
        PageEventSchemaVersion,
        ev.ScriptVersion,
        ev.Time,
        ev.Host,
        ev.RemoteAddr,
        ev.UserAgent,
        ev.RequestReferrer,
        ev.Title,
        ev.PageReferrer,
        ev.URL,
        ev.EventType,
        ev.SessionToken,
        ev.UserToken,
    )

    if err != nil {
        log.Fatal(err)
    }
}

Thanks in advance.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.