diff --git a/README.md b/README.md index a099d22..5f26c77 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ To save shared links to a local SQLite database, set the `--shared-links.sql.dri #### MySQL -To save shared links in a MySQL database, set the `--shared-links.sql.driver=mysql` and `--shared-links.sql.dsn=` flag (see https://github.com/go-sql-driver/mysql#dsn-data-source-name for MySQL DNS specifications). +To save shared links in a MySQL database, set the `--shared-links.sql.driver=mysql` and `--shared-links.sql.dsn=` flag (see https://github.com/go-sql-driver/mysql#dsn-data-source-name for MySQL DSN specifications). By default, PromLens will try to auto-create the necessary tables in your MySQL database. This requires the PromLens database user to have `CREATE` permissions. To turn off automatic table creation for MySQL, set the `--no-shared-links.sql.create-tables` flag. If you want to create tables manually, run the following against your PromLens MySQL database: @@ -130,6 +130,28 @@ CREATE TABLE IF NOT EXISTS view( ); ``` +#### Postgres + +To save shared links in a Postgres database, set the `--shared-links.sql.driver=postgres` and `--shared-links.sql.dsn=` flag (see https://pkg.go.dev/github.com/lib/pq#hdr-Connection_String_Parameters for Postgres DSN specifications). + +By default, PromLens will try to auto-create the necessary tables in your Postgres database. This requires the PromLens database user to have `CREATE` permissions. To turn off automatic table creation for Postgres, set the `--no-shared-links.sql.create-tables` flag. If you want to create tables manually, run the following against your PromLens Postgres database: + +```sql +CREATE TABLE IF NOT EXISTS link ( + id SERIAL PRIMARY KEY, + created_at timestamptz DEFAULT now(), + short_name VARCHAR(11) UNIQUE, + page_state TEXT +); + +CREATE TABLE IF NOT EXISTS view( + id SERIAL PRIMARY KEY, + link_id INTEGER, + viewed_at timestamptz DEFAULT now(), + FOREIGN KEY(link_id) REFERENCES link(id) +); +``` + ### Enabling Grafana datasource integration To enable selection of datasources from an existing Grafana installation, set the `--grafana.url` flag to the URL of your Grafana installation, as well as either the `--grafana.api-token` flag (providing an API token directly as a flag) or the `--grafana.api-token-file` flag (providing an API token from a file). diff --git a/cmd/promlens/main.go b/cmd/promlens/main.go index a43445f..0cb08ec 100644 --- a/cmd/promlens/main.go +++ b/cmd/promlens/main.go @@ -94,8 +94,8 @@ func getLinkSharer(logger log.Logger, gcsBucket string, sqlDriver string, sqlDSN } if sqlDSN != "" { - if sqlDriver != "mysql" && sqlDriver != "sqlite" { - return nil, errors.Errorf("unsupported SQL driver %q, supported values are 'mysql' and 'sqlite'", sqlDriver) + if sqlDriver != "mysql" && sqlDriver != "sqlite" && sqlDriver != "postgres" { + return nil, errors.Errorf("unsupported SQL driver %q, supported values are 'mysql', 'postgres' and 'sqlite'", sqlDriver) } s, err := sharer.NewSQLSharer(logger, sqlDriver, sqlDSN, createTables, sqlRetention) diff --git a/go.mod b/go.mod index 1628163..d63168b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-kit/log v0.2.1 github.com/go-sql-driver/mysql v1.6.0 github.com/grafana/regexp v0.0.0-20220304095617-2e8d9baf4ac2 + github.com/lib/pq v1.10.7 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.13.0 github.com/prometheus/common v0.37.0 @@ -64,7 +65,7 @@ require ( golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8 // indirect - golang.org/x/text v0.3.7 // indirect + golang.org/x/text v0.3.8 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.97.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index a1f3235..937e579 100644 --- a/go.sum +++ b/go.sum @@ -646,6 +646,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linode/linodego v1.9.1 h1:29UpEPpYcGFnbwiJW8mbk/bjBZpgd/pv68io2IKTo34= @@ -1257,8 +1259,9 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= diff --git a/pkg/sharer/sharer.go b/pkg/sharer/sharer.go index f9ef060..f63be08 100644 --- a/pkg/sharer/sharer.go +++ b/pkg/sharer/sharer.go @@ -32,6 +32,7 @@ import ( // Load SQL drivers. _ "github.com/glebarez/go-sqlite" _ "github.com/go-sql-driver/mysql" + _ "github.com/lib/pq" ) const maxPageStateSize = 512 * 1024 @@ -167,7 +168,34 @@ func NewSQLSharer(logger log.Logger, driver string, dsn string, createTables boo return nil, fmt.Errorf("Error creating view table: %v", err) } } + case "postgres": + db.SetConnMaxLifetime(0) + db.SetMaxIdleConns(3) + db.SetMaxOpenConns(3) + if createTables { + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS link ( + id SERIAL PRIMARY KEY, + created_at timestamptz DEFAULT now(), + short_name VARCHAR(11) UNIQUE, + page_state TEXT + )`) + if err != nil { + return nil, fmt.Errorf("error creating link table: %v", err) + } + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS view( + id SERIAL PRIMARY KEY, + link_id INT, + viewed_at timestamptz DEFAULT now(), + FOREIGN KEY(link_id) REFERENCES link(id) ON DELETE CASCADE + )`, + ) + if err != nil { + return nil, fmt.Errorf("Error creating view table: %v", err) + } + } case "sqlite": _, err := db.Exec("PRAGMA foreign_keys = ON") if err != nil { @@ -238,13 +266,16 @@ func NewSQLSharer(logger log.Logger, driver string, dsn string, createTables boo } func (s SQLSharer) cleanupOldLinks(retention time.Duration) (int64, error) { - timestampFn := "DATETIME" - if s.driver == "mysql" { - timestampFn = "TIMESTAMP" + var query string + switch s.driver { + case "postgres": + query = `DELETE FROM link WHERE created_at < $1::timestamptz` + case "mysql": + query = `DELETE FROM link WHERE created_at < TIMESTAMP(?)` + default: + query = `DELETE FROM link WHERE created_at < DATETIME(?)` } - - res, err := s.db.Exec(` - DELETE FROM link WHERE created_at < `+timestampFn+`(?)`, + res, err := s.db.Exec(query, time.Now().Add(-retention), ) if err != nil { @@ -266,7 +297,13 @@ func (s SQLSharer) CreateLink(name string, pageState string) error { return fmt.Errorf("error beginning transaction: %v", err) } id := 0 - err = tx.QueryRow("SELECT id FROM link WHERE short_name = ?", name).Scan(&id) + var query string + if s.driver == "postgres" { + query = "SELECT id FROM link WHERE short_name = $1" + } else { + query = "SELECT id FROM link WHERE short_name = ?" + } + err = tx.QueryRow(query, name).Scan(&id) if err == nil { // TODO: Check rollback errors. _ = tx.Rollback() @@ -280,8 +317,12 @@ func (s SQLSharer) CreateLink(name string, pageState string) error { _ = tx.Rollback() return fmt.Errorf("error checking for link existence: %v", err) } - - _, err = tx.Exec("INSERT INTO link(short_name, page_state) values(?, ?)", name, pageState) + if s.driver == "postgres" { + query = "INSERT INTO link(short_name, page_state) values($1, $2)" + } else { + query = "INSERT INTO link(short_name, page_state) values(?, ?)" + } + _, err = tx.Exec(query, name, pageState) if err != nil { // TODO: Check rollback errors. _ = tx.Rollback() @@ -299,8 +340,13 @@ func (s SQLSharer) GetLink(name string) (pageState string, err error) { linkLookupErrors.Inc() } }() - - stmt, err := s.db.Prepare("SELECT id, page_state FROM link WHERE short_name = ?") + var query string + if s.driver == "postgres" { + query = "SELECT id, page_state FROM link WHERE short_name = $1" + } else { + query = "SELECT id, page_state FROM link WHERE short_name = ?" + } + stmt, err := s.db.Prepare(query) if err != nil { return "", fmt.Errorf("error preparing statement: %v", err) } @@ -313,7 +359,12 @@ func (s SQLSharer) GetLink(name string) (pageState string, err error) { return "", err } - _, err = s.db.Exec("INSERT INTO view(link_id) values(?)", id) + if s.driver == "postgres" { + query = "INSERT INTO view(link_id) values($1)" + } else { + query = "INSERT INTO view(link_id) values(?)" + } + _, err = s.db.Exec(query, id) if err != nil { return "", fmt.Errorf("error inserting view: %v", err) }