From f9310d626b2010607fd977b9152c04efbacc7623 Mon Sep 17 00:00:00 2001 From: Vera Reynolds Date: Mon, 11 Jul 2022 17:30:36 -0400 Subject: [PATCH 1/4] maint: move json example to its own dir --- examples/{ => json_reader}/example1.json | 0 examples/{ => json_reader}/example2.json | 0 examples/{ => json_reader}/read_json_log.go | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename examples/{ => json_reader}/example1.json (100%) rename examples/{ => json_reader}/example2.json (100%) rename examples/{ => json_reader}/read_json_log.go (100%) diff --git a/examples/example1.json b/examples/json_reader/example1.json similarity index 100% rename from examples/example1.json rename to examples/json_reader/example1.json diff --git a/examples/example2.json b/examples/json_reader/example2.json similarity index 100% rename from examples/example2.json rename to examples/json_reader/example2.json diff --git a/examples/read_json_log.go b/examples/json_reader/read_json_log.go similarity index 100% rename from examples/read_json_log.go rename to examples/json_reader/read_json_log.go From c354373b666f818f3eab9b7119993ec4dc27ef36 Mon Sep 17 00:00:00 2001 From: Vera Reynolds Date: Mon, 11 Jul 2022 18:14:39 -0400 Subject: [PATCH 2/4] Add webapp example - ported from https://github.com/honeycombio/examples/blob/525c2e80f92c89f40241bcac8e2ba2e7c2e43a77/golang-webapp/README.md --- examples/webapp/Dockerfile | 11 + examples/webapp/README.md | 62 +++++ examples/webapp/db.go | 68 ++++++ examples/webapp/docker-compose.yml | 32 +++ examples/webapp/go.mod | 25 ++ examples/webapp/go.sum | 54 +++++ examples/webapp/handlers.go | 327 ++++++++++++++++++++++++++ examples/webapp/honeycomb_helpers.go | 121 ++++++++++ examples/webapp/main.go | 25 ++ examples/webapp/templates/base.html | 8 + examples/webapp/templates/home.html | 36 +++ examples/webapp/templates/login.html | 15 ++ examples/webapp/templates/signup.html | 31 +++ examples/webapp/types.go | 28 +++ 14 files changed, 843 insertions(+) create mode 100644 examples/webapp/Dockerfile create mode 100644 examples/webapp/README.md create mode 100644 examples/webapp/db.go create mode 100644 examples/webapp/docker-compose.yml create mode 100644 examples/webapp/go.mod create mode 100644 examples/webapp/go.sum create mode 100644 examples/webapp/handlers.go create mode 100644 examples/webapp/honeycomb_helpers.go create mode 100644 examples/webapp/main.go create mode 100644 examples/webapp/templates/base.html create mode 100644 examples/webapp/templates/home.html create mode 100644 examples/webapp/templates/login.html create mode 100644 examples/webapp/templates/signup.html create mode 100644 examples/webapp/types.go diff --git a/examples/webapp/Dockerfile b/examples/webapp/Dockerfile new file mode 100644 index 0000000..18920ad --- /dev/null +++ b/examples/webapp/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:alpine + +WORKDIR /app + +COPY . ./ + +RUN go mod download +RUN go build -o ./shoutr + +RUN apk add --update --no-cache ca-certificates +ENTRYPOINT ["./shoutr"] diff --git a/examples/webapp/README.md b/examples/webapp/README.md new file mode 100644 index 0000000..fc34778 --- /dev/null +++ b/examples/webapp/README.md @@ -0,0 +1,62 @@ +## golang-webapp + +Shoutr is an example Golang web application. You can register for accounts, sign +in and shout your opinions on the Internet. It has two tiers: A Golang web app +and a MySQL database. + +## Install + +Clone the repository. + +Create database in MySQL. + +``` +$ mysql -uroot -e 'create database shoutr;' +``` + +## Run app + +``` +$ export HONEYCOMB_API_KEY= +$ go run . +``` + +## (Alternatively) Run in Docker + +The whole webapp can be run in Docker (Compose). + +Set your [Honeycomb API key](https://ui.honeycomb.io/account) to +`HONEYCOMB_API_KEY`, or edit the `docker-compose.yml`. The `shoutr` database in +MySQL will be created automatically. + +``` +$ export HONEYCOMB_API_KEY= +``` + +Then: + +``` +$ docker-compose build && docker-compose up +``` + +## Event Fields + +| **Name** | **Description** | **Example Value** | +| --- | --- | --- | +| `flash.value` | Contents of the rendered flash message | `Your shout is too long!` | +| `request.content_length`| Length of the content (in bytes) of the sent HTTP request | `952` | +| `request.host` | Host the request was sent to | `localhost` | +| `request.method` | HTTP method | `POST` | +| `request.path` | Path of the request | `/shout` | +| `request.proto` | HTTP protocol version | `HTTP/1.1` | +| `request.remote_addr` | The IP and port that answered the request | `172.18.0.1:40484` | +| `request.user_agent`| User agent | `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36` | +| `response.status_code` | Status code written back to user | `200` | +| `runtime.memory_inuse` | Amount of memory in use (in bytes) by the whole process | `4,971,776` | +| `runtime.num_goroutines` | Number of goroutines in the process | `7` | +| `shout.content` | Content of the user's comment | `Hello world!` | +| `shout.content_length` | Length (in characters) of the user's comment | `80` | +| `system.hostname` | System hostname | `1ba87a98788c` | +| `timers.total_time_ms` | The total amount of time the request took to serve | `180` | +| `timers.mysql_insert_user_ms` | The time the `INSERT INTO users` query took | `50` | +| `user.id`| User ID | `2` | diff --git a/examples/webapp/db.go b/examples/webapp/db.go new file mode 100644 index 0000000..6db22aa --- /dev/null +++ b/examples/webapp/db.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "log" + "os" + "time" + + "github.com/jmoiron/sqlx" +) + +var ( + maxConnectRetries = 10 + db *sqlx.DB +) + +func init() { + var err error + dbUser := "root" + dbPass := "" + dbName := "shoutr" + dbHost := os.Getenv("DB_HOST") + if dbHost == "" { + dbHost = "localhost" + } + + for i := 0; i < maxConnectRetries; i++ { + db, err = sqlx.Connect("mysql", fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", dbUser, dbPass, dbHost, dbName)) + if err != nil { + log.Print("Error connecting to database: ", err) + } else { + break + } + if i == maxConnectRetries-1 { + panic("Couldn't connect to DB") + } + time.Sleep(1 * time.Second) + } + + log.Print("Bootstrapping database...") + + _, err = db.Exec(` +CREATE TABLE IF NOT EXISTS users ( + id INT NOT NULL AUTO_INCREMENT, + first_name VARCHAR(64) NOT NULL, + last_name VARCHAR(64) NOT NULL, + username VARCHAR(64) NOT NULL, + email VARCHAR(64), + PRIMARY KEY (id), + UNIQUE KEY (username) +);`) + if err != nil { + panic(err) + } + + _, err = db.Exec(` +CREATE TABLE IF NOT EXISTS shouts ( + id INT NOT NULL AUTO_INCREMENT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + user_id INT, + content VARCHAR(140) NOT NULL, + PRIMARY KEY (id) +); +`) + if err != nil { + panic(err) + } +} diff --git a/examples/webapp/docker-compose.yml b/examples/webapp/docker-compose.yml new file mode 100644 index 0000000..4efb7e4 --- /dev/null +++ b/examples/webapp/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3' + +services: + app: + build: . + ports: + - "8888:8888" + networks: + - main + depends_on: + - db + environment: + DB_HOST: db + HONEYCOMB_API_KEY: + + db: + image: mysql + networks: + - main + volumes: + - example-golang-webapp:/var/lib/mysql + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" # value is arbitrary + MYSQL_DATABASE: "shoutr" # create DB shoutr automatically + ENV: "dev" + +volumes: + example-golang-webapp: + +networks: + main: + driver: bridge diff --git a/examples/webapp/go.mod b/examples/webapp/go.mod new file mode 100644 index 0000000..3c3f165 --- /dev/null +++ b/examples/webapp/go.mod @@ -0,0 +1,25 @@ +module github.com/honeycombio/libhoney-go/examples/webapp + +go 1.17 + +require ( + github.com/go-ozzo/ozzo-validation v3.6.0+incompatible + github.com/go-sql-driver/mysql v1.6.0 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/schema v1.2.0 + github.com/gorilla/sessions v1.2.1 + github.com/honeycombio/libhoney-go v1.15.8 + github.com/jmoiron/sqlx v1.3.5 +) + +require ( + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect + github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 // indirect + github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + gopkg.in/alexcesaro/statsd.v2 v2.0.0 // indirect +) diff --git a/examples/webapp/go.sum b/examples/webapp/go.sum new file mode 100644 index 0000000..ccd81d7 --- /dev/null +++ b/examples/webapp/go.sum @@ -0,0 +1,54 @@ +github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= +github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= +github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01/go.mod h1:ypD5nozFk9vcGw1ATYefw6jHe/jZP++Z15/+VTMcWhc= +github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= +github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52/go.mod h1:yIquW87NGRw1FU5p5lEkpnt/QxoH5uPAOUlOVkAUuMg= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/honeycombio/libhoney-go v1.15.8 h1:TECEltZ48K6J4NG1JVYqmi0vCJNnHYooFor83fgKesA= +github.com/honeycombio/libhoney-go v1.15.8/go.mod h1:+tnL2etFnJmVx30yqmoUkVyQjp7uRJw0a2QGu48lSyY= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= +gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/webapp/handlers.go b/examples/webapp/handlers.go new file mode 100644 index 0000000..c1c4cac --- /dev/null +++ b/examples/webapp/handlers.go @@ -0,0 +1,327 @@ +package main + +import ( + "html/template" + "log" + "net/http" + "path/filepath" + "reflect" + "time" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/go-ozzo/ozzo-validation/is" + "github.com/gorilla/schema" + "github.com/gorilla/sessions" + libhoney "github.com/honeycombio/libhoney-go" +) + +var ( + decoder = schema.NewDecoder() + sessionName = "default" + sessionStore = sessions.NewCookieStore([]byte("best-secret-in-the-world")) +) + +const ( + templatesDir = "templates" + // 140 is the proper amount of characters for a microblog. Any other + // value is heresy. + maxShoutLength = 140 +) + +func hnyEventFromRequest(r *http.Request) *libhoney.Event { + ev, ok := r.Context().Value(hnyContextKey).(*libhoney.Event) + if !ok { + // We control the way this is being put on context anyway. + panic("Couldn't get libhoney event from request context") + } + + // Every libhoney event gets annotated automatically with user ID if a + // user is logged in! + session, _ := sessionStore.Get(r, sessionName) + userID, ok := session.Values["user_id"] + if ok { + ev.AddField("user.id", userID) + } + + return ev +} + +func addFinalErr(err *error, ev *libhoney.Event) { + if *err != nil { + ev.AddField("error", (*err).Error()) + } +} + +func signupHandlerGet(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + + tmpl := template.Must(template. + ParseFiles( + filepath.Join(templatesDir, "base.html"), + filepath.Join(templatesDir, "signup.html"), + )) + tmplData := struct { + ErrorMessage string + }{} + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } +} + +func signupHandlerPost(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + + tmpl := template.Must(template. + ParseFiles( + filepath.Join(templatesDir, "base.html"), + filepath.Join(templatesDir, "signup.html"), + )) + tmplData := struct { + ErrorMessage string + }{} + if err = r.ParseForm(); err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + tmplData.ErrorMessage = "Couldn't parse form" + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } + + var user User + if err = decoder.Decode(&user, r.PostForm); err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + + tmplData.ErrorMessage = "An error occurred" + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } + + ev.AddField("user.email", user.Email) + + if err = validation.ValidateStruct(&user, + validation.Field(&user.FirstName, is.Alpha), + validation.Field(&user.LastName, is.Alpha), + validation.Field(&user.Username, is.Alphanumeric), + validation.Field(&user.Email, is.Email), + ); err != nil { + log.Print(err) + tmplData.ErrorMessage = "Validation failure" + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } + + queryStart := time.Now() + + res, err := db.Exec(`INSERT INTO users +(first_name, last_name, username, email) +VALUES +(?, ?, ?, ?) +`, user.FirstName, user.LastName, user.Username, user.Email) + if err != nil { + log.Print(err) + http.Error(w, "Something went wrong", http.StatusInternalServerError) + return + } + + ev.AddField("timers.db.users_insert_ms", time.Since(queryStart)/time.Millisecond) + + session, _ := sessionStore.Get(r, sessionName) + userID, err := res.LastInsertId() + if err != nil { + log.Print(err) + http.Error(w, "Something went wrong", http.StatusInternalServerError) + return + } + session.Values["user_id"] = int(userID) + + ev.AddField("user.id", int(userID)) + + session.Save(r, w) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func loginHandlerGet(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + + tmpl := template.Must(template. + ParseFiles( + filepath.Join(templatesDir, "base.html"), + filepath.Join(templatesDir, "login.html"), + )) + tmplData := struct { + ErrorMessage string + }{} + + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } +} + +func loginHandlerPost(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + + tmpl := template.Must(template. + ParseFiles( + filepath.Join(templatesDir, "base.html"), + filepath.Join(templatesDir, "login.html"), + )) + tmplData := struct { + ErrorMessage string + }{} + + if r.Method == "GET" { + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } + + user := User{} + + if err = r.ParseForm(); err != nil { + log.Print(err) + w.WriteHeader(http.StatusBadRequest) + tmplData.ErrorMessage = "Couldn't parse form properly" + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } + + username := r.FormValue("username") + + if err = db.Get(&user, `SELECT id FROM users WHERE username = ?`, username); err != nil { + w.WriteHeader(http.StatusBadRequest) + tmplData.ErrorMessage = "Couldn't log you in." + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } + + session, _ := sessionStore.Get(r, sessionName) + session.Values["user_id"] = user.ID + session.Save(r, w) + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func shoutHandler(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + + session, _ := sessionStore.Get(r, sessionName) + userID := session.Values["user_id"] + if err = r.ParseForm(); err != nil { + log.Print(err) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + content := r.FormValue("content") + ev.AddField("shout.content_length", len(content)) + + if len(content) > maxShoutLength { + session, _ := sessionStore.Get(r, sessionName) + session.AddFlash("Your shout is too long!") + session.Save(r, w) + ev.AddField("shout.content", content[:140]) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + ev.AddField("shout.content", content) + + if _, err = db.Exec(`INSERT INTO shouts (content, user_id) VALUES (?, ?)`, content, userID); err != nil { + log.Print(err) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func logoutHandler(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + session, _ := sessionStore.Get(r, sessionName) + delete(session.Values, "user_id") + session.Save(r, w) + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func mainHandler(w http.ResponseWriter, r *http.Request) { + var err error + ev := hnyEventFromRequest(r) + defer addFinalErr(&err, ev) + tmpl := template.Must(template.ParseFiles( + filepath.Join(templatesDir, "base.html"), + filepath.Join(templatesDir, "home.html"), + )) + session, _ := sessionStore.Get(r, sessionName) + tmplData := struct { + User User + Shouts []RenderedShout + ErrorMessage string + }{} + + flashes := session.Flashes() + if len(flashes) == 1 { + flash, ok := flashes[0].(string) + if !ok { + ev.AddField("flash.err", "Flash didn't assert to type string, got "+reflect.TypeOf(flash).String()) + } else { + tmplData.ErrorMessage = flash + ev.AddField("flash.value", flash) + } + session.Save(r, w) + } + + // Not logged in + if userID, ok := session.Values["user_id"]; !ok { + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } else { + if err = db.Get(&tmplData.User, `SELECT * FROM users WHERE id = ?`, userID); err != nil { + log.Print(err) + http.Error(w, "Something went wrong", http.StatusInternalServerError) + return + } + + if err = db.Select(&tmplData.Shouts, ` +SELECT users.first_name, users.last_name, users.username, shouts.content, shouts.created_at +FROM shouts +INNER JOIN users +ON shouts.user_id = users.id +ORDER BY created_at DESC +`); err != nil { + log.Print(err) + http.Error(w, "Something went wrong", http.StatusInternalServerError) + return + } + + if err = tmpl.Execute(w, tmplData); err != nil { + log.Print(err) + } + return + } +} diff --git a/examples/webapp/honeycomb_helpers.go b/examples/webapp/honeycomb_helpers.go new file mode 100644 index 0000000..684969b --- /dev/null +++ b/examples/webapp/honeycomb_helpers.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" + + libhoney "github.com/honeycombio/libhoney-go" +) + +var ( + hostname string + hnyDatasetName = "examples.golang-webapp" + hnyContextKey = "honeycombEvent" +) + +func init() { + hcConfig := libhoney.Config{ + APIKey: os.Getenv("HONEYCOMB_API_KEY"), + APIHost: "https://api-dogfood.honeycomb.io", + Dataset: hnyDatasetName, + } + + if err := libhoney.Init(hcConfig); err != nil { + log.Print(err) + os.Exit(1) + } + + if hnyTeam, err := libhoney.VerifyAPIKey(hcConfig); err != nil { + log.Print(err) + log.Print("Please make sure the HONEYCOMB_API_KEY environment variable is set.") + os.Exit(1) + } else { + log.Print(fmt.Sprintf("Sending Honeycomb events to the %q dataset on %q team", hnyDatasetName, hnyTeam)) + } + + // Initialize fields that every sent event will have. + + // Getting hostname on every event can be very useful if, e.g., only a + // particular host or set of hosts are the source of an issue. + if hostname, err := os.Hostname(); err == nil { + libhoney.AddField("system.hostname", hostname) + } + libhoney.AddDynamicField("runtime.num_goroutines", func() interface{} { + return runtime.NumGoroutine() + }) + libhoney.AddDynamicField("runtime.memory_inuse", func() interface{} { + // This will ensure that every event includes information about + // the memory usage of the process at the time the event was + // sent. + var mem runtime.MemStats + runtime.ReadMemStats(&mem) + return mem.Alloc + }) +} + +type HoneyResponseWriter struct { + *libhoney.Event + http.ResponseWriter + StatusCode int +} + +func (hrw *HoneyResponseWriter) WriteHeader(status int) { + // Mark this down for adding to the libhoney event later. + hrw.StatusCode = status + hrw.ResponseWriter.WriteHeader(status) +} + +func addRequestProps(req *http.Request, ev *libhoney.Event) { + // Add a variety of details about the HTTP request, such as user agent + // and method, to any created libhoney event. + ev.AddField("request.method", req.Method) + ev.AddField("request.path", req.URL.Path) + ev.AddField("request.host", req.URL.Host) + ev.AddField("request.proto", req.Proto) + ev.AddField("request.content_length", req.ContentLength) + ev.AddField("request.remote_addr", req.RemoteAddr) + ev.AddField("request.user_agent", req.UserAgent()) +} + +// HoneycombMiddleware will wrap our HTTP handle funcs to automatically +// generate an event-per-request and set properties on them. +func HoneycombMiddleware(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // We'll time each HTTP request and add that as a property to + // the sent Honeycomb event, so start the timer for that. + startHandler := time.Now() + ev := libhoney.NewEvent() + + defer func() { + if err := ev.Send(); err != nil { + log.Print("Error sending libhoney event: ", err) + } + }() + + addRequestProps(r, ev) + + // Create a context where we will store the libhoney event. We + // will add default values to this event for every HTTP + // request, and the user can access it to add their own + // (powerful, custom) fields. + ctx := context.WithValue(r.Context(), hnyContextKey, ev) + reqWithContext := r.WithContext(ctx) + + honeyResponseWriter := &HoneyResponseWriter{ + Event: ev, + ResponseWriter: w, + StatusCode: 200, + } + + fn(honeyResponseWriter, reqWithContext) + + ev.AddField("response.status_code", honeyResponseWriter.StatusCode) + handlerDuration := time.Since(startHandler) + ev.AddField("timers.total_time_ms", handlerDuration/time.Millisecond) + } +} diff --git a/examples/webapp/main.go b/examples/webapp/main.go new file mode 100644 index 0000000..6c3577e --- /dev/null +++ b/examples/webapp/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "log" + "net/http" + + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", HoneycombMiddleware(mainHandler)).Methods("GET") + + r.HandleFunc("/signup", HoneycombMiddleware(signupHandlerGet)).Methods("GET") + r.HandleFunc("/signup", HoneycombMiddleware(signupHandlerPost)).Methods("POST") + + r.HandleFunc("/login", HoneycombMiddleware(loginHandlerGet)).Methods("GET") + r.HandleFunc("/login", HoneycombMiddleware(loginHandlerPost)).Methods("POST") + + r.HandleFunc("/logout", HoneycombMiddleware(logoutHandler)).Methods("POST") + r.HandleFunc("/shout", HoneycombMiddleware(shoutHandler)).Methods("POST") + + log.Print("Serving app on localhost:8888 ....") + log.Fatal(http.ListenAndServe(":8888", r)) +} diff --git a/examples/webapp/templates/base.html b/examples/webapp/templates/base.html new file mode 100644 index 0000000..b2484b9 --- /dev/null +++ b/examples/webapp/templates/base.html @@ -0,0 +1,8 @@ + + + +Shoutr + + +{{template "body" .}} + diff --git a/examples/webapp/templates/home.html b/examples/webapp/templates/home.html new file mode 100644 index 0000000..d37f2de --- /dev/null +++ b/examples/webapp/templates/home.html @@ -0,0 +1,36 @@ +{{define "body"}} +{{if ne .User.ID 0}} +
+ +
+

+Welcome {{.User.FirstName}}. +

+

Get shoutin':

+{{if .ErrorMessage}} +

{{.ErrorMessage}}

+{{end}} +
+ + +
+{{if .Shouts}} +{{range $shout := .Shouts}} +
+
{{$shout.FirstName}} {{$shout.LastName}} @{{$shout.Username}} | {{$shout.CreatedAt.Time.Format "Jan 02, 2006 15:04:05"}}
+{{$shout.Content}} +
+{{end}} +{{else}} +Once you or others do some shouting, the shouts will appear here. +{{end}} +{{else}} +

Shoutr

+

Shoutr is a new kind of web 3.0 social media platform.

+

With Shoutr, you can shout your opinions on the Internet!

+

Sign up for an account today to access the content in our walled garden.

+Sign Up | +Login +{{end}} +{{end}} diff --git a/examples/webapp/templates/login.html b/examples/webapp/templates/login.html new file mode 100644 index 0000000..043ff63 --- /dev/null +++ b/examples/webapp/templates/login.html @@ -0,0 +1,15 @@ +{{define "body"}} +{{if .ErrorMessage}} +

{{.ErrorMessage}}

+{{end}} +
+
+ + +
+ +
+ +
+
+{{end}} diff --git a/examples/webapp/templates/signup.html b/examples/webapp/templates/signup.html new file mode 100644 index 0000000..467ed72 --- /dev/null +++ b/examples/webapp/templates/signup.html @@ -0,0 +1,31 @@ +{{define "body"}} +

Sign Up

+{{if .ErrorMessage}} +

{{.ErrorMessage}}

+{{end}} +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+{{end}} diff --git a/examples/webapp/types.go b/examples/webapp/types.go new file mode 100644 index 0000000..7f4dabc --- /dev/null +++ b/examples/webapp/types.go @@ -0,0 +1,28 @@ +package main + +import "github.com/go-sql-driver/mysql" + +type User struct { + ID int `db:"id"` + FirstName string `db:"first_name" schema:"first_name"` + LastName string `db:"last_name" schema:"last_name"` + Username string `db:"username" schema:"username"` + Email string `db:"email" schema:"email"` +} + +type Shout struct { + ID int `db:"int"` + UserID int `db:"user_id"` + Content string `db:"content"` + CreatedAt mysql.NullTime `db:"created_at"` +} + +// Used to read the data from a MySQL JOIN query and render it on the +// front-end. +type RenderedShout struct { + FirstName string `db:"first_name"` + LastName string `db:"last_name" schema:"last_name"` + Username string `db:"username" schema:"username"` + Content string `db:"content"` + CreatedAt mysql.NullTime `db:"created_at"` +} From b5687d37d5f775fa28913c4658f3b17fdaee5545 Mon Sep 17 00:00:00 2001 From: Vera Reynolds Date: Mon, 11 Jul 2022 18:22:00 -0400 Subject: [PATCH 3/4] fix ci building json example --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bad6261..6b31b4c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -69,7 +69,7 @@ jobs: field_value: << parameters.goversion >> - run: name: Build example - command: go build examples/read_json_log.go + command: go build examples/json_reader/read_json_log.go publish_github: executor: github From 0b3d9f9f3ceb1e6f67b2d179f5ea466d23f5e67d Mon Sep 17 00:00:00 2001 From: Vera Reynolds Date: Tue, 12 Jul 2022 15:34:39 -0400 Subject: [PATCH 4/4] remove dogfood api --- examples/webapp/honeycomb_helpers.go | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/webapp/honeycomb_helpers.go b/examples/webapp/honeycomb_helpers.go index 684969b..dc08239 100644 --- a/examples/webapp/honeycomb_helpers.go +++ b/examples/webapp/honeycomb_helpers.go @@ -21,7 +21,6 @@ var ( func init() { hcConfig := libhoney.Config{ APIKey: os.Getenv("HONEYCOMB_API_KEY"), - APIHost: "https://api-dogfood.honeycomb.io", Dataset: hnyDatasetName, }