Merge pull request #2 from Kugelschieber/tracking

Server Side Tracking
This commit is contained in:
Marvin Blum
2020-06-24 18:01:33 +02:00
committed by GitHub
20 changed files with 562 additions and 95 deletions

View File

@@ -19,8 +19,9 @@ WORKDIR /app
# default config
ENV MB_LOGLEVEL=info
ENV MB_ALLOWED_ORIGINS=*
ENV MB_HOST=0.0.0.0:80
ENV MB_HOST=0.0.0.0:8888
ENV MB_DB_PORT=5432
ENV MB_DB_SSLMODE=disable
EXPOSE 80
EXPOSE 443
EXPOSE 8888
CMD ["/app/main"]

View File

@@ -2,6 +2,7 @@ package blog
import (
"fmt"
"github.com/Kugelschieber/marvinblum.de/tpl"
emvi "github.com/emvi/api-go"
"github.com/emvi/logbuch"
"io/ioutil"
@@ -9,6 +10,7 @@ import (
"os"
"path/filepath"
"regexp"
"sync"
"time"
)
@@ -29,11 +31,13 @@ type Blog struct {
articles map[string]emvi.Article // id -> article
articlesYear map[int][]emvi.Article // year -> articles
nextUpdate time.Time
cache *tpl.Cache
m sync.Mutex
}
func NewBlog() *Blog {
func NewBlog(cache *tpl.Cache) *Blog {
logbuch.Info("Initializing blog")
b := new(Blog)
b := &Blog{cache: cache}
b.client = emvi.NewClient(os.Getenv("MB_EMVI_CLIENT_ID"),
os.Getenv("MB_EMVI_CLIENT_SECRET"),
os.Getenv("MB_EMVI_ORGA"),
@@ -76,6 +80,8 @@ func (blog *Blog) GetLatestArticles() []emvi.Article {
}
func (blog *Blog) loadArticles() {
blog.m.Lock()
defer blog.m.Unlock()
logbuch.Info("Refreshing blog articles...")
articles, offset, count := make(map[string]emvi.Article), 0, 1
var err error
@@ -188,6 +194,7 @@ func (blog *Blog) setArticles(articles map[string]emvi.Article) {
func (blog *Blog) refreshIfRequired() {
if blog.nextUpdate.Before(time.Now()) {
blog.cache.Clear()
blog.loadArticles()
}
}

9
dev.sh
View File

@@ -10,5 +10,14 @@ export MB_HOT_RELOAD=true
export MB_EMVI_CLIENT_ID=3fBBn144yvSF9R3dPC8l
export MB_EMVI_CLIENT_SECRET=dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
export MB_EMVI_ORGA=marvin
export MB_DB_HOST=localhost
export MB_DB_PORT=5432
export MB_DB_USER=postgres
export MB_DB_PASSWORD=postgres
export MB_DB_SCHEMA=marvinblum
export MB_DB_SSLMODE=disable
export MB_DB_SSLCERT=
export MB_DB_SSLKEY=
export MB_DB_SSLROOTCERT=
go run main.go

View File

@@ -5,11 +5,14 @@ services:
image: "traefik:v2.2"
container_name: traefik
restart: always
networks:
- traefik-internal
command:
# - "--log.level=DEBUG"
# - "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=marvinblum_traefik-internal"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.tls-resolver.acme.httpchallenge=true"
@@ -24,18 +27,31 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /root/marvinblum/letsencrypt:/letsencrypt
# labels:
# - "traefik.enable=true"
# - "traefik.port=8080"
# - "traefik.http.routers.traefik.entrypoints=web"
# - "traefik.http.routers.traefik.service=api@internal"
marvinblum:
image: kugel/marvinblum
container_name: marvinblum
restart: always
depends_on:
- traefik
networks:
- postgres_db-internal
- traefik-internal
environment:
- MB_EMVI_CLIENT_ID="3fBBn144yvSF9R3dPC8l"
- MB_EMVI_CLIENT_SECRET="dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg"
- MB_EMVI_ORGA=marvin
MB_EMVI_CLIENT_ID: 3fBBn144yvSF9R3dPC8l
MB_EMVI_CLIENT_SECRET: dw3FeshelTgdf1Gj13J7uF5FfdPDi40sQvvwqeFVKTTyIDuCdlAHhRY72csFL6yg
MB_EMVI_ORGA: marvin
MB_DB_HOST: postgres
MB_DB_USER:
MB_DB_PASSWORD:
MB_DB_SCHEMA:
labels:
- "traefik.enable=true"
- "traefik.port=8888"
- "traefik.http.routers.marvinblum.entrypoints=web"
- "traefik.http.routers.marvinblum.rule=Host(`marvinblum.de`)"
- "traefik.http.routers.marvinblum.middlewares=http-redirect"
@@ -44,3 +60,9 @@ services:
- "traefik.http.routers.marvinblum-secure.tls.certresolver=tls-resolver"
- "traefik.http.middlewares.http-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.http-redirect.redirectscheme.permanent=true"
networks:
traefik-internal:
driver: bridge
postgres_db-internal:
external: true

15
go.mod
View File

@@ -4,10 +4,21 @@ go 1.14
require (
github.com/NYTimes/gziphandler v1.1.1
github.com/caddyserver/certmagic v0.10.13
github.com/caddyserver/certmagic v0.11.2
github.com/cenkalti/backoff/v4 v4.0.2 // indirect
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934
github.com/emvi/logbuch v1.1.1
github.com/emvi/pirsch v0.0.0-20200624123353-86381b017755
github.com/gorilla/mux v1.7.4
github.com/gosimple/slug v1.9.0
github.com/jmoiron/sqlx v1.2.0
github.com/klauspost/cpuid v1.3.0 // indirect
github.com/lib/pq v1.7.0
github.com/miekg/dns v1.1.29 // indirect
github.com/rs/cors v1.7.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect
golang.org/x/text v0.3.3 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
)

43
go.sum
View File

@@ -51,10 +51,12 @@ github.com/aws/aws-sdk-go v1.30.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZve
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/caddyserver/certmagic v0.10.13 h1:wfyYpXVXSSYMS1ZFpSr7HptwsC+j7elda5PUERrHtRc=
github.com/caddyserver/certmagic v0.10.13/go.mod h1:Yz6cSRUdddGy6Ut5JfrvcqBwrm1BqXxJRqJq2TwjPnA=
github.com/caddyserver/certmagic v0.11.2 h1:nPBqyuFNHJEf2FwC1ixJjArtTKWyPqpaH6k4jl7gxYI=
github.com/caddyserver/certmagic v0.11.2/go.mod h1:fqY1IZk5iqhsj5FU3Vw20Sjq66tEKaanTFYNZ74soMY=
github.com/cenkalti/backoff/v4 v4.0.0 h1:6VeaLF9aI+MAUQ95106HwWzYZgJJpZ4stumjj6RFYAU=
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/cenkalti/backoff/v4 v4.0.2 h1:JIufpQLbh4DkbQoii76ItQIUFzevQSqOLZca4eamEDs=
github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@@ -75,8 +77,18 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8 h1:DZJfRdvwlybLvHkyeN8HRU4QnBFqVZg21ki4to1AVPo=
github.com/emvi/api-go v0.0.0-20191210194347-0a945446f6a8/go.mod h1:g9RdDC3s5ebCknAHQQ5PjoM2vRFSyyGoOUX3QkDKU+o=
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934 h1:+G10WRp72llJuaW89QezNK8QU9SsOmd5Ja7ocrrUaI0=
github.com/emvi/logbuch v0.0.0-20200214115750-61de9b6d5934/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
github.com/emvi/logbuch v1.1.1 h1:poBGNbHy/nB95oNoqLKAaJoBrcKxTO0W9DhMijKEkkU=
github.com/emvi/logbuch v1.1.1/go.mod h1:J2Wgbr3BuSc1JO+D2MBVh6q3WPVSK5GzktwWz8pvkKw=
github.com/emvi/pirsch v0.0.0-20200623183712-76dda933f960 h1:7q8uRCOILenzPP7au4BsylEvkJeIkqJcKN5z/49Wpm8=
github.com/emvi/pirsch v0.0.0-20200623183712-76dda933f960/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
github.com/emvi/pirsch v0.0.0-20200623190422-b0f4c9980175 h1:rj+o6eyEkYy9bYd/pupAiUnLZxo9U1qRfdHJ9nzQSLY=
github.com/emvi/pirsch v0.0.0-20200623190422-b0f4c9980175/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
github.com/emvi/pirsch v0.0.0-20200623192632-5c5fcd1d78cc h1:G05tiq3JKZwjnPSObRBwypzWplKTUWG2XFWhgFnqD/g=
github.com/emvi/pirsch v0.0.0-20200623192632-5c5fcd1d78cc/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
github.com/emvi/pirsch v0.0.0-20200623193552-b3a3d4a6434d h1:+HRIYgA9AaXlGFQfhXpUShXxJ1knG2stwNIai1M6mzs=
github.com/emvi/pirsch v0.0.0-20200623193552-b3a3d4a6434d/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
github.com/emvi/pirsch v0.0.0-20200624123353-86381b017755 h1:TdiDC7+IfV6giMtFNvbYKFlSPJJZewr9jfh3KGcr8QQ=
github.com/emvi/pirsch v0.0.0-20200624123353-86381b017755/go.mod h1:+YmBbltJ3feZz9L/QQyqwywltYvQKBfzrGD51TPKl5g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/exoscale/egoscale v0.18.1/go.mod h1:Z7OOdzzTOz1Q1PjQXumlz9Wn/CddH0zSYdCF3rnBKXE=
@@ -93,6 +105,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -150,6 +163,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -160,6 +175,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.0 h1:2JqaNE1hGdABW2YbA3TenkO7RiPFRvSWnEnGqWh9sHE=
github.com/klauspost/cpuid v1.3.0/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/kolo/xmlrpc v0.0.0-20190717152603-07c4ee3fd181/go.mod h1:o03bZfuBwAXHetKXuInt4S7omeXUu62/A845kiycsSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -169,15 +186,22 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labbsr0x/bindman-dns-webhook v1.0.2/go.mod h1:p6b+VCXIR8NYKpDr8/dg1HKfQoRHCdcsROXKvmoehKA=
github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c027w=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/linode/linodego v0.10.0/go.mod h1:cziNP7pbvE3mXIPneHj0oRY8L1WtGEIKlZ8LANE4eXA=
github.com/liquidweb/liquidweb-go v1.6.0/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-vnc v0.0.0-20150629162542-723ed9867aed/go.mod h1:3rdaFaCv4AyBgu5ALFM0+tSuHrBh6v692nyQe3ikrq0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@@ -268,6 +292,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -323,6 +349,8 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -364,11 +392,16 @@ golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -465,6 +498,8 @@ gopkg.in/resty.v1 v1.9.1/go.mod h1:vo52Hzryw9PnPHcJfPsBiFW62XhNx5OczbV9y+IMpgc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

111
main.go
View File

@@ -1,17 +1,22 @@
package main
import (
"context"
"github.com/Kugelschieber/marvinblum.de/blog"
"github.com/Kugelschieber/marvinblum.de/tpl"
"github.com/Kugelschieber/marvinblum.de/tracking"
"github.com/NYTimes/gziphandler"
"github.com/caddyserver/certmagic"
emvi "github.com/emvi/api-go"
"github.com/emvi/logbuch"
"github.com/emvi/pirsch"
"github.com/gorilla/mux"
_ "github.com/lib/pq"
"github.com/rs/cors"
"html/template"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"time"
)
@@ -21,9 +26,12 @@ const (
staticDirPrefix = "/static/"
logTimeFormat = "2006-01-02_15:04:05"
envPrefix = "MB_"
shutdownTimeout = time.Second * 30
)
var (
tracker *pirsch.Tracker
tplCache *tpl.Cache
blogInstance *blog.Blog
)
@@ -52,36 +60,36 @@ func logEnvConfig() {
func serveAbout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := struct {
tracker.Hit(r)
tplCache.Render(w, "about.html", struct {
Articles []emvi.Article
}{
blogInstance.GetLatestArticles(),
}
})
}
}
if err := tpl.Get().ExecuteTemplate(w, "about.html", data); err != nil {
logbuch.Error("Error executing blog template", logbuch.Fields{"err": err})
w.WriteHeader(http.StatusInternalServerError)
}
func serveLegal() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tracker.Hit(r)
tplCache.Render(w, "legal.html", nil)
}
}
func serveBlogPage() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data := struct {
tracker.Hit(r)
tplCache.Render(w, "blog.html", struct {
Articles map[int][]emvi.Article
}{
blogInstance.GetArticles(),
}
if err := tpl.Get().ExecuteTemplate(w, "blog.html", data); err != nil {
logbuch.Error("Error executing blog template", logbuch.Fields{"err": err})
w.WriteHeader(http.StatusInternalServerError)
}
})
}
}
func serveBlogArticle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tracker.Hit(r)
vars := mux.Vars(r)
slug := strings.Split(vars["slug"], "-")
@@ -97,7 +105,7 @@ func serveBlogArticle() http.HandlerFunc {
return
}
data := struct {
tplCache.Render(w, "article.html", struct {
Title string
Content template.HTML
Published time.Time
@@ -105,12 +113,38 @@ func serveBlogArticle() http.HandlerFunc {
article.LatestArticleContent.Title,
template.HTML(article.LatestArticleContent.Content),
article.Published,
})
}
}
func serveTracking() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start, _ := strconv.Atoi(r.URL.Query().Get("start"))
if start > 365 {
start = 365
} else if start < 7 {
start = 7
}
if err := tpl.Get().ExecuteTemplate(w, "article.html", data); err != nil {
logbuch.Error("Error executing blog article template", logbuch.Fields{"err": err})
w.WriteHeader(http.StatusInternalServerError)
}
totalVisitorsLabels, totalVisitorsDps := tracking.GetTotalVisitors(start)
tplCache.RenderWithoutCache(w, "tracking.html", struct {
Start int
TotalVisitorsLabels template.JS
TotalVisitorsDps template.JS
PageVisits []tracking.PageVisits
}{
start,
totalVisitorsLabels,
totalVisitorsDps,
tracking.GetPageVisits(start),
})
}
}
func serveNotFound() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tplCache.Render(w, "notfound.html", nil)
}
}
@@ -119,9 +153,10 @@ func setupRouter() *mux.Router {
router.PathPrefix(staticDirPrefix).Handler(http.StripPrefix(staticDirPrefix, gziphandler.GzipHandler(http.FileServer(http.Dir(staticDir)))))
router.Handle("/blog/{slug}", serveBlogArticle())
router.Handle("/blog", serveBlogPage())
router.Handle("/legal", tpl.ServeTemplate("legal.html"))
router.Handle("/legal", serveLegal())
router.Handle("/tracking", serveTracking())
router.Handle("/", serveAbout())
router.NotFoundHandler = tpl.ServeTemplate("notfound.html")
router.NotFoundHandler = serveNotFound()
return router
}
@@ -141,28 +176,36 @@ func configureCors(router *mux.Router) http.Handler {
func start(handler http.Handler) {
logbuch.Info("Starting server...")
var server http.Server
server.Handler = handler
server.Addr = os.Getenv("MB_HOST")
if strings.ToLower(os.Getenv("MB_TLS")) == "true" {
logbuch.Info("TLS enabled")
certmagic.DefaultACME.Agreed = true
certmagic.DefaultACME.Email = os.Getenv("MB_TLS_EMAIL")
certmagic.DefaultACME.CA = certmagic.LetsEncryptProductionCA
go func() {
sigint := make(chan os.Signal)
signal.Notify(sigint, os.Interrupt)
<-sigint
logbuch.Info("Shutting down server...")
tracker.Stop()
ctx, _ := context.WithTimeout(context.Background(), shutdownTimeout)
if err := certmagic.HTTPS(strings.Split(os.Getenv("MB_DOMAIN"), ","), handler); err != nil {
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
}
} else {
if err := http.ListenAndServe(os.Getenv("MB_HOST"), handler); err != nil {
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
if err := server.Shutdown(ctx); err != nil {
logbuch.Fatal("Error shutting down server gracefully", logbuch.Fields{"err": err})
}
}()
if err := server.ListenAndServe(); err != http.ErrServerClosed {
logbuch.Fatal("Error starting server", logbuch.Fields{"err": err})
}
logbuch.Info("Server shut down")
}
func main() {
configureLog()
logEnvConfig()
tpl.LoadTemplate()
blogInstance = blog.NewBlog()
tracker = tracking.NewTracker()
tplCache = tpl.NewCache()
blogInstance = blog.NewBlog(tplCache)
router := setupRouter()
corsConfig := configureCors(router)
start(corsConfig)

4
postgres/clear_logs.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
echo "Clearing docker logs..."
echo "" > $(docker inspect --format='{{.LogPath}}' postgres)

View File

@@ -0,0 +1,22 @@
version: "3"
services:
postgres:
image: postgres:12-alpine
container_name: postgres
restart: always
command: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key
networks:
- db-internal
environment:
POSTGRES_PASSWORD:
ports:
- "5432:5432"
volumes:
- /root/postgres/data:/var/lib/postgresql/data
- /root/postgres/cert/server.crt:/var/lib/postgresql/server.crt
- /root/postgres/cert/server.key:/var/lib/postgresql/server.key
networks:
db-internal:
driver: bridge

10
postgres/gen_cert.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
mkdir cert
cd cert
openssl req -new -text -passout pass:$1 -subj /CN=localhost -out server.req -keyout privkey.pem
openssl rsa -in privkey.pem -passin pass:$1 -out server.key
openssl req -x509 -in server.req -text -key server.key -out server.crt
chown 0:70 server.key
chmod 640 server.key
echo "done"

7
static/js/Chart-v2.9.3.bundle.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -47,6 +47,11 @@ body {
margin-right: 10px;
}
.tracking {
width: 100%;
height: 300px;
}
section {
margin: 80px 0;
}

View File

@@ -11,14 +11,14 @@
{{template "menu.html"}}
<section>
<h2>who am I?</h2>
<h2>Who Am I?</h2>
<p>
I'm a full stack software engineer from Germany, open source and Linux enthusiast and co-founder of <a href="https://emvi.com/" target="_blank">Emvi</a>.
In love with Go, but fluent in a lot of programming languages.
</p>
</section>
<section>
<h2>latest blog posts</h2>
<h2>Latest Blog Posts</h2>
{{range $article := .Articles}}
<p>
<a href="/blog/{{slug $article.LatestArticleContent.Title}}-{{$article.Id}}">{{$article.LatestArticleContent.Title}}</a>
@@ -32,11 +32,14 @@
</p>
</section>
<section>
<h2>projects</h2>
<h2>Projects</h2>
<ul>
<li>
<a href="https://emvi.com/" target="_blank">Emvi</a> a note taking, collaboration and knowledge management tool for personal use and teams of any size
</li>
<li>
<a href="https://github.com/emvi/pirsch" target="_blank">pirsch</a> a server side, no-cookie and privacy focused tracking library for Golang
</li>
<li>
<a href="https://github.com/emvi/logbuch" target="_blank">logbuch</a> a simple Golang logging library
</li>
@@ -52,7 +55,7 @@
</ul>
</section>
<section>
<h2>skills</h2>
<h2>Skills</h2>
<ul>
<li>Go (Golang)</li>
<li>JavaScript (Vue, Node)</li>
@@ -66,7 +69,7 @@
</ul>
</section>
<section>
<h2>work</h2>
<h2>Work</h2>
<ul>
<li>
<a href="https://emvi.com/" target="_blank">Emvi</a> co-founder of a note taking, collaboration and knowledge management SaaS startup

View File

@@ -3,7 +3,9 @@
<section>
<h1>Blog</h1>
{{range $year, $articles := .Articles}}
</section>
{{range $year, $articles := .Articles}}
<section>
<h2>{{$year}}</h2>
{{range $article := $articles}}
@@ -12,9 +14,11 @@
<small>{{format $article.Published "2. January 2006"}}</small>
</p>
{{end}}
{{else}}
</section>
{{else}}
<section>
<p>There are no blog posts yet...</p>
{{end}}
</section>
</section>
{{end}}
{{template "end.html"}}

View File

@@ -1,13 +1,13 @@
<hr />
<section>
<p>
Like to see more? Read my blog articles on <a href="https://emvi.com/blog" target="_blank">Emvi</a>, my project page on <a href="https://github.com/Kugelschieber" target="_blank">GitHub</a> or send me a <a href="mailto:marvin@marvinblum.de">mail</a>.
Would you like to see more? Read my blog articles on <a href="https://emvi.com/blog" target="_blank">Emvi</a>, my project page on <a href="https://github.com/Kugelschieber" target="_blank">GitHub</a> or send me a <a href="mailto:marvin@marvinblum.de">mail</a>.
</p>
<p>
This page uses <a href="https://concrete.style/" target="_blank">concrete</a> for styling. Check it out!
</p>
<p>
<a href="/legal">Legal</a>
This page does not use cookies. <a href="/legal">Legal</a>
</p>
</section>
</body>

View File

@@ -1,6 +1,9 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h1>Legal</h1>
</section>
<section>
<h2>According to §5 TMG</h2>
<p>
@@ -10,5 +13,11 @@
<a href="mailto:marvin@marvinblum.de">marvin@marvinblum.de</a>
</p>
</section>
<section>
<h2>Cookie Policy</h2>
<p>
This page does not use cookies.
</p>
</section>
{{template "end.html"}}

74
template/tracking.html Normal file
View File

@@ -0,0 +1,74 @@
{{template "head.html"}}
{{template "menu.html"}}
<section>
<h1>Tracking</h1>
<p>
This page shows tracking statistics for my website using <a href="https://github.com/emvi/pirsch" target="_blank">Pirsch</a> and <a href="https://www.chartjs.org/" target="_blank">Chart.Js</a>. The data shows unique visitors.
</p>
<p>
<a href="/tracking?start=7" class="button {{if eq .Start 7}}filled{{end}}">Week</a>
<a href="/tracking?start=30" class="button {{if eq .Start 30}}filled{{end}}">Month</a>
<a href="/tracking?start=90" class="button {{if eq .Start 90}}filled{{end}}">Quarter</a>
<a href="/tracking?start=182" class="button {{if eq .Start 182}}filled{{end}}">Half Year</a>
<a href="/tracking?start=365" class="button {{if eq .Start 365}}filled{{end}}">Year</a>
</p>
</section>
<!-- TODO real time -->
<section>
<h2>Total Visitors</h2>
<canvas id="totalVisitors" class="tracking"></canvas>
</section>
<section>
<h2>Pages Visits</h2>
</section>
{{range $i, $data := .PageVisits}}
<section>
<h3>{{$data.Path}}</h3>
<canvas id="pageVisits{{$i}}" class="tracking"></canvas>
</section>
{{end}}
<!-- TODO -->
<!--<section>
<h2>Languages</h2>
</section>
<section>
<h2>Visitors Per Hour</h2>
</section>-->
<script type="text/javascript" src="/static/js/Chart-v2.9.3.bundle.min.js"></script>
<script type="text/javascript">
new Chart(document.getElementById('totalVisitors').getContext('2d'), {
type: "line",
data: {
labels: [{{.TotalVisitorsLabels}}],
datasets: [{
backgroundColor: "rgba(0, 0, 0, 0.05)",
borderColor: "#000",
label: "Total Visitors",
data: [{{.TotalVisitorsDps}}]
}]
}
});
{{range $i, $data := .PageVisits}}
new Chart(document.getElementById('pageVisits{{$i}}').getContext('2d'), {
type: "line",
data: {
labels: [{{$data.Labels}}],
datasets: [{
backgroundColor: "rgba(0, 0, 0, 0.05)",
borderColor: "#000",
label: "Page Visits",
data: [{{$data.Data}}]
}]
}
});
{{end}}
</script>
{{template "end.html"}}

View File

@@ -7,6 +7,7 @@ import (
"html/template"
"net/http"
"os"
"sync"
"time"
)
@@ -14,59 +15,87 @@ const (
templateDir = "template/*"
)
var (
type Cache struct {
tpl *template.Template
tplCache = make(map[string][]byte)
cache map[string][]byte
hotReload bool
)
var funcMap = template.FuncMap{
"slug": slug.Make,
"format": func(t time.Time, layout string) string {
return t.Format(layout)
},
m sync.RWMutex
}
func LoadTemplate() {
func NewCache() *Cache {
cache := &Cache{
cache: make(map[string][]byte),
hotReload: os.Getenv("MB_HOT_RELOAD") == "true",
}
logbuch.Debug("Template cache hot reload", logbuch.Fields{"hot_reload": cache.hotReload})
cache.load()
return cache
}
func (cache *Cache) load() {
logbuch.Debug("Loading templates")
funcMap := template.FuncMap{
"slug": slug.Make,
"format": func(t time.Time, layout string) string {
return t.Format(layout)
},
}
var err error
tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir)
cache.tpl, err = template.New("").Funcs(funcMap).ParseGlob(templateDir)
if err != nil {
logbuch.Fatal("Error loading template", logbuch.Fields{"err": err})
}
hotReload = os.Getenv("MB_HOT_RELOAD") == "true"
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": hotReload})
logbuch.Debug("Templates loaded", logbuch.Fields{"hot_reload": cache.hotReload})
}
func renderTemplate(name string) {
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
var buffer bytes.Buffer
func (cache *Cache) Render(w http.ResponseWriter, name string, data interface{}) {
cache.m.RLock()
if err := tpl.ExecuteTemplate(&buffer, name, nil); err != nil {
logbuch.Fatal("Error executing template", logbuch.Fields{"err": err, "name": name})
}
if cache.cache[name] == nil || cache.hotReload {
cache.m.RUnlock()
cache.m.Lock()
defer cache.m.Unlock()
logbuch.Debug("Rendering template", logbuch.Fields{"name": name})
tplCache[name] = buffer.Bytes()
}
func Get() *template.Template {
return tpl
}
func ServeTemplate(name string) http.HandlerFunc {
// render once so we have it in cache
renderTemplate(name)
return func(w http.ResponseWriter, r *http.Request) {
if hotReload {
LoadTemplate()
renderTemplate(name)
if cache.hotReload {
logbuch.Debug("Reloading templates")
cache.load()
}
if _, err := w.Write(tplCache[name]); err != nil {
logbuch.Error("Error returning page to client", logbuch.Fields{"err": err, "name": name})
var buffer bytes.Buffer
if err := cache.tpl.ExecuteTemplate(&buffer, name, data); err != nil {
logbuch.Error("Error executing template", logbuch.Fields{"err": err, "name": name})
w.WriteHeader(http.StatusInternalServerError)
}
cache.cache[name] = buffer.Bytes()
} else {
cache.m.RUnlock()
}
if _, err := w.Write(cache.cache[name]); err != nil {
logbuch.Error("Error sending response to client", logbuch.Fields{"err": err, "template": name})
w.WriteHeader(http.StatusInternalServerError)
}
}
func (cache *Cache) RenderWithoutCache(w http.ResponseWriter, name string, data interface{}) {
if cache.hotReload {
logbuch.Debug("Reloading templates")
cache.load()
}
if err := cache.tpl.ExecuteTemplate(w, name, data); err != nil {
logbuch.Error("Error executing template", logbuch.Fields{"err": err, "name": name})
w.WriteHeader(http.StatusInternalServerError)
}
}
func (cache *Cache) Clear() {
cache.m.Lock()
defer cache.m.Unlock()
cache.cache = make(map[string][]byte)
}

102
tracking/statistics.go Normal file
View File

@@ -0,0 +1,102 @@
package tracking
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/emvi/pirsch"
"html/template"
"strings"
"time"
)
const (
statisticsDateFormat = "2006-01-02"
)
type PageVisits struct {
Path string
Labels template.JS
Data template.JS
}
func GetTotalVisitors(start int) (template.JS, template.JS) {
query := `SELECT "date" "day",
CASE WHEN "visitors_per_day".visitors IS NULL THEN 0 ELSE "visitors_per_day".visitors END
FROM (SELECT * FROM generate_series(date($1), date(now()), interval '1 day') "date") AS date_series
LEFT JOIN "visitors_per_day" ON date("visitors_per_day"."day") = date("date")
ORDER BY "date" ASC`
startTime := today()
startTime = startTime.Add(-time.Hour * 24 * time.Duration(start-1))
logbuch.Debug("Reading total visitors since", logbuch.Fields{"since": startTime})
var visitors []pirsch.VisitorsPerDay
if err := db.Select(&visitors, query, startTime); err != nil {
logbuch.Error("Error reading total visitors", logbuch.Fields{"err": err, "since": startTime})
return "", ""
}
today := today()
visitorsToday, err := store.VisitorsPerDay(today)
if err != nil {
logbuch.Error("Error reading total visitors for today", logbuch.Fields{"err": err, "since": startTime})
return "", ""
}
visitors[len(visitors)-1].Visitors = visitorsToday
return getLabelsAndData(visitors)
}
func GetPageVisits(start int) []PageVisits {
pathsQuery := `SELECT * FROM (SELECT DISTINCT "path" FROM "visitors_per_page" WHERE day >= $1) AS paths ORDER BY length("path") ASC`
query := `SELECT "date" "day",
CASE WHEN "visitors_per_page".visitors IS NULL THEN 0 ELSE "visitors_per_page".visitors END
FROM (SELECT * FROM generate_series(date($1), date(now() - INTERVAL '1 day'), interval '1 day') "date") AS date_series
LEFT JOIN "visitors_per_page" ON date("visitors_per_page"."day") = date("date") AND "visitors_per_page"."path" = $2
ORDER BY "date" ASC, length("visitors_per_page"."path") ASC`
startTime := today()
startTime = startTime.Add(-time.Hour * 24 * time.Duration(start-1))
logbuch.Debug("Reading page visits", logbuch.Fields{"since": startTime})
var paths []string
if err := db.Select(&paths, pathsQuery, startTime); err != nil {
logbuch.Error("Error reading distinct paths", logbuch.Fields{"err": err})
return nil
}
// TODO add visitors for today
pageVisits := make([]PageVisits, len(paths))
for i, path := range paths {
var visitors []pirsch.VisitorsPerDay
if err := db.Select(&visitors, query, startTime, path); err != nil {
logbuch.Error("Error reading visitor for path", logbuch.Fields{"err": err, "since": startTime})
return nil
}
labels, data := getLabelsAndData(visitors)
pageVisits[i] = PageVisits{path, labels, data}
}
return pageVisits
}
func getLabelsAndData(visitors []pirsch.VisitorsPerDay) (template.JS, template.JS) {
var labels strings.Builder
var dp strings.Builder
for _, point := range visitors {
labels.WriteString(fmt.Sprintf("'%s',", point.Day.Format(statisticsDateFormat)))
dp.WriteString(fmt.Sprintf("%d,", point.Visitors))
}
labelsStr := labels.String()
dataStr := dp.String()
return template.JS(labelsStr[:len(labelsStr)-1]), template.JS(dataStr[:len(dataStr)-1])
}
func today() time.Time {
now := time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
}

70
tracking/tracking.go Normal file
View File

@@ -0,0 +1,70 @@
package tracking
import (
"database/sql"
"fmt"
"github.com/emvi/logbuch"
"github.com/emvi/pirsch"
"github.com/jmoiron/sqlx"
"os"
"strconv"
"time"
)
const (
connectionString = `host=%s port=%s user=%s password=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s connectTimeout=%s timezone=%s`
)
var db *sqlx.DB
var store pirsch.Store
func NewTracker() *pirsch.Tracker {
logbuch.Info("Connecting to database...")
host := os.Getenv("MB_DB_HOST")
port := os.Getenv("MB_DB_PORT")
user := os.Getenv("MB_DB_USER")
password := os.Getenv("MB_DB_PASSWORD")
schema := os.Getenv("MB_DB_SCHEMA")
sslMode := os.Getenv("MB_DB_SSLMODE")
sslCert := os.Getenv("MB_DB_SSLCERT")
sslKey := os.Getenv("MB_DB_SSLKEY")
sslRootCert := os.Getenv("MB_DB_SSLROOTCERT")
zone, offset := time.Now().Zone()
timezone := zone + strconv.Itoa(-offset/3600)
logbuch.Info("Setting time zone", logbuch.Fields{"timezone": timezone})
connectionStr := fmt.Sprintf(connectionString, host, port, user, password, schema, sslMode, sslCert, sslKey, sslRootCert, "30", timezone)
conn, err := sql.Open("postgres", connectionStr)
if err != nil {
logbuch.Fatal("Error connecting to database", logbuch.Fields{"err": err})
return nil
}
if err := conn.Ping(); err != nil {
logbuch.Fatal("Error pinging database", logbuch.Fields{"err": err})
return nil
}
db = sqlx.NewDb(conn, "postgres")
store = pirsch.NewPostgresStore(conn)
tracker := pirsch.NewTracker(store, nil)
processor := pirsch.NewProcessor(store)
processTrackingData(processor)
pirsch.RunAtMidnight(func() {
processTrackingData(processor)
})
return tracker
}
func processTrackingData(processor *pirsch.Processor) {
logbuch.Info("Processing tracking data...")
defer func() {
if err := recover(); err != nil {
logbuch.Error("Error processing tracking data", logbuch.Fields{"err": err})
}
}()
processor.Process()
logbuch.Info("Done processing tracking data")
}