Restructure application

This commit is contained in:
Dan Sosedoff
2015-04-30 11:47:07 -05:00
parent 7a75447364
commit c513930e27
18 changed files with 686 additions and 710 deletions

362
pkg/api/api.go Normal file
View File

@@ -0,0 +1,362 @@
package api
import (
"errors"
"fmt"
"mime"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/sosedoff/pgweb/pkg/bookmarks"
"github.com/sosedoff/pgweb/pkg/client"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/data"
)
var extraMimeTypes = map[string]string{
".icon": "image-x-icon",
".ttf": "application/x-font-ttf",
".woff": "application/x-font-woff",
".eot": "application/vnd.ms-fontobject",
".svg": "image/svg+xml",
}
var DbClient *client.Client
type Error struct {
Message string `json:"error"`
}
func NewError(err error) Error {
return Error{err.Error()}
}
func assetContentType(name string) string {
ext := filepath.Ext(name)
result := mime.TypeByExtension(ext)
if result == "" {
result = extraMimeTypes[ext]
}
if result == "" {
result = "text/plain; charset=utf-8"
}
return result
}
func SetupRoutes(router *gin.Engine) {
router.GET("/", API_Home)
router.GET("/static/*path", API_ServeAsset)
api := router.Group("/api")
{
api.Use(ApiMiddleware())
api.POST("/connect", API_Connect)
api.GET("/databases", API_GetDatabases)
api.GET("/connection", API_ConnectionInfo)
api.GET("/activity", API_Activity)
api.GET("/schemas", API_GetSchemas)
api.GET("/tables", API_GetTables)
api.GET("/tables/:table", API_GetTable)
api.GET("/tables/:table/rows", API_GetTableRows)
api.GET("/tables/:table/info", API_GetTableInfo)
api.GET("/tables/:table/indexes", API_TableIndexes)
api.GET("/query", API_RunQuery)
api.POST("/query", API_RunQuery)
api.GET("/explain", API_ExplainQuery)
api.POST("/explain", API_ExplainQuery)
api.GET("/history", API_History)
api.GET("/bookmarks", API_Bookmarks)
}
}
// Middleware function to check database connection status before running queries
func ApiMiddleware() gin.HandlerFunc {
allowedPaths := []string{
"/api/connect",
"/api/bookmarks",
"/api/history",
}
return func(c *gin.Context) {
if DbClient != nil {
c.Next()
return
}
currentPath := c.Request.URL.Path
allowed := false
for _, path := range allowedPaths {
if path == currentPath {
allowed = true
break
}
}
if allowed {
c.Next()
} else {
c.JSON(400, Error{"Not connected"})
c.Abort()
}
return
}
}
func API_Home(c *gin.Context) {
data, err := data.Asset("static/index.html")
if err != nil {
c.String(400, err.Error())
return
}
c.Data(200, "text/html; charset=utf-8", data)
}
func API_Connect(c *gin.Context) {
url := c.Request.FormValue("url")
if url == "" {
c.JSON(400, Error{"Url parameter is required"})
return
}
opts := command.Options{Url: url}
url, err := connection.FormatUrl(opts)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
cl, err := client.NewFromUrl(url)
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
err = cl.Test()
if err != nil {
c.JSON(400, Error{err.Error()})
return
}
info, err := cl.Info()
if err == nil {
if DbClient != nil {
DbClient.Close()
}
DbClient = cl
}
c.JSON(200, info.Format()[0])
}
func API_GetDatabases(c *gin.Context) {
names, err := DbClient.Databases()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, names)
}
func API_RunQuery(c *gin.Context) {
query := strings.TrimSpace(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, errors.New("Query parameter is missing"))
return
}
API_HandleQuery(query, c)
}
func API_ExplainQuery(c *gin.Context) {
query := strings.TrimSpace(c.Request.FormValue("query"))
if query == "" {
c.JSON(400, errors.New("Query parameter is missing"))
return
}
API_HandleQuery(fmt.Sprintf("EXPLAIN ANALYZE %s", query), c)
}
func API_GetSchemas(c *gin.Context) {
names, err := DbClient.Schemas()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, names)
}
func API_GetTables(c *gin.Context) {
names, err := DbClient.Tables()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, names)
}
func API_GetTable(c *gin.Context) {
res, err := DbClient.Table(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_GetTableRows(c *gin.Context) {
limit := 1000 // Number of rows to fetch
limitVal := c.Request.FormValue("limit")
if limitVal != "" {
num, err := strconv.Atoi(limitVal)
if err != nil {
c.JSON(400, Error{"Invalid limit value"})
return
}
if num <= 0 {
c.JSON(400, Error{"Limit should be greater than 0"})
return
}
limit = num
}
opts := client.RowsOptions{
Limit: limit,
SortColumn: c.Request.FormValue("sort_column"),
SortOrder: c.Request.FormValue("sort_order"),
}
res, err := DbClient.TableRows(c.Params.ByName("table"), opts)
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_GetTableInfo(c *gin.Context) {
res, err := DbClient.TableInfo(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res.Format()[0])
}
func API_History(c *gin.Context) {
c.JSON(200, DbClient.History)
}
func API_ConnectionInfo(c *gin.Context) {
res, err := DbClient.Info()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res.Format()[0])
}
func API_Activity(c *gin.Context) {
res, err := DbClient.Activity()
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_TableIndexes(c *gin.Context) {
res, err := DbClient.TableIndexes(c.Params.ByName("table"))
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, res)
}
func API_HandleQuery(query string, c *gin.Context) {
result, err := DbClient.Query(query)
if err != nil {
c.JSON(400, NewError(err))
return
}
q := c.Request.URL.Query()
if len(q["format"]) > 0 && q["format"][0] == "csv" {
filename := fmt.Sprintf("pgweb-%v.csv", time.Now().Unix())
c.Writer.Header().Set("Content-disposition", "attachment;filename="+filename)
c.Data(200, "text/csv", result.CSV())
return
}
c.JSON(200, result)
}
func API_Bookmarks(c *gin.Context) {
bookmarks, err := bookmarks.ReadAll(bookmarks.Path())
if err != nil {
c.JSON(400, NewError(err))
return
}
c.JSON(200, bookmarks)
}
func API_ServeAsset(c *gin.Context) {
path := "static" + c.Params.ByName("path")
data, err := data.Asset(path)
if err != nil {
c.String(400, err.Error())
return
}
if len(data) == 0 {
c.String(404, "Asset is empty")
return
}
c.Data(200, assetContentType(path), data)
}

29
pkg/api/api_test.go Normal file
View File

@@ -0,0 +1,29 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_assetContentType(t *testing.T) {
samples := map[string]string{
"foo.html": "text/html; charset=utf-8",
"foo.css": "text/css; charset=utf-8",
"foo.js": "application/javascript",
"foo.icon": "image-x-icon",
"foo.png": "image/png",
"foo.jpg": "image/jpeg",
"foo.gif": "image/gif",
"foo.eot": "application/vnd.ms-fontobject",
"foo.svg": "image/svg+xml",
"foo.ttf": "application/x-font-ttf",
"foo.woff": "application/x-font-woff",
"foo.foo": "text/plain; charset=utf-8",
"foo": "text/plain; charset=utf-8",
}
for name, expected := range samples {
assert.Equal(t, expected, assetContentType(name))
}
}

View File

@@ -0,0 +1,71 @@
package bookmarks
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/mitchellh/go-homedir"
)
type Bookmark struct {
Url string `json:"url"` // Postgres connection URL
Host string `json:"host"` // Server hostname
Port string `json:"port"` // Server port
User string `json:"user"` // Database user
Password string `json:"password"` // User password
Database string `json:"database"` // Database name
Ssl string `json:"ssl"` // Connection SSL mode
}
func readServerConfig(path string) (Bookmark, error) {
bookmark := Bookmark{}
buff, err := ioutil.ReadFile(path)
if err != nil {
return bookmark, err
}
_, err = toml.Decode(string(buff), &bookmark)
return bookmark, err
}
func fileBasename(path string) string {
filename := filepath.Base(path)
return strings.Replace(filename, filepath.Ext(path), "", 1)
}
func Path() string {
path, _ := homedir.Dir()
return fmt.Sprintf("%s/.pgweb/bookmarks", path)
}
func ReadAll(path string) (map[string]Bookmark, error) {
results := map[string]Bookmark{}
files, err := ioutil.ReadDir(path)
if err != nil {
return results, err
}
for _, file := range files {
if filepath.Ext(file.Name()) != ".toml" {
continue
}
fullPath := path + "/" + file.Name()
key := fileBasename(file.Name())
config, err := readServerConfig(fullPath)
if err != nil {
fmt.Printf("%s parse error: %s\n", fullPath, err)
continue
}
results[key] = config
}
return results, nil
}

View File

@@ -0,0 +1,71 @@
package bookmarks
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Invalid_Bookmark_Files(t *testing.T) {
_, err := readServerConfig("foobar")
assert.Error(t, err)
_, err = readServerConfig("./data/invalid.toml")
assert.Error(t, err)
assert.Equal(t, "Near line 1, key 'invalid encoding': Near line 2: Expected key separator '=', but got '\\n' instead.", err.Error())
_, err = readServerConfig("./data/invalid_port.toml")
assert.Error(t, err)
assert.Equal(t, "Type mismatch for 'main.Bookmark.Port': Expected string but found 'int64'.", err.Error())
}
func Test_Bookmark(t *testing.T) {
bookmark, err := readServerConfig("./data/bookmark.toml")
assert.Equal(t, nil, err)
assert.Equal(t, "localhost", bookmark.Host)
assert.Equal(t, "5432", bookmark.Port)
assert.Equal(t, "postgres", bookmark.User)
assert.Equal(t, "mydatabase", bookmark.Database)
assert.Equal(t, "disable", bookmark.Ssl)
assert.Equal(t, "", bookmark.Password)
assert.Equal(t, "", bookmark.Url)
}
func Test_Bookmark_URL(t *testing.T) {
bookmark, err := readServerConfig("./data/bookmark_url.toml")
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://username:password@host:port/database?sslmode=disable", bookmark.Url)
assert.Equal(t, "", bookmark.Host)
assert.Equal(t, "", bookmark.Port)
assert.Equal(t, "", bookmark.User)
assert.Equal(t, "", bookmark.Database)
assert.Equal(t, "", bookmark.Ssl)
assert.Equal(t, "", bookmark.Password)
}
func Test_Bookmarks_Path(t *testing.T) {
assert.NotEqual(t, "/.pgweb/bookmarks", bookmarksPath())
}
func Test_Basename(t *testing.T) {
assert.Equal(t, "filename", fileBasename("filename.toml"))
assert.Equal(t, "filename", fileBasename("path/filename.toml"))
assert.Equal(t, "filename", fileBasename("~/long/path/filename.toml"))
assert.Equal(t, "filename", fileBasename("filename"))
}
func Test_ReadBookmarks_Invalid(t *testing.T) {
bookmarks, err := readAllBookmarks("foobar")
assert.Error(t, err)
assert.Equal(t, 0, len(bookmarks))
}
func Test_ReadBookmarks(t *testing.T) {
bookmarks, err := readAllBookmarks("./data")
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(bookmarks))
}

259
pkg/client/client.go Normal file
View File

@@ -0,0 +1,259 @@
package client
import (
"bytes"
"encoding/csv"
"fmt"
"reflect"
"github.com/jmoiron/sqlx"
"github.com/sosedoff/pgweb/pkg/command"
"github.com/sosedoff/pgweb/pkg/connection"
"github.com/sosedoff/pgweb/pkg/history"
"github.com/sosedoff/pgweb/pkg/statements"
)
type Client struct {
db *sqlx.DB
History []history.Record
ConnectionString string
}
type Row []interface{}
type Result struct {
Columns []string `json:"columns"`
Rows []Row `json:"rows"`
}
// Struct to hold table rows browsing options
type RowsOptions struct {
Limit int // Number of rows to fetch
SortColumn string // Column to sort by
SortOrder string // Sort direction (ASC, DESC)
}
func New() (*Client, error) {
str, err := connection.BuildString(command.Opts)
if command.Opts.Debug && str != "" {
fmt.Println("Creating a new client for:", str)
}
if err != nil {
return nil, err
}
db, err := sqlx.Open("postgres", str)
if err != nil {
return nil, err
}
client := Client{
db: db,
ConnectionString: str,
History: history.New(),
}
return &client, nil
}
func NewFromUrl(url string) (*Client, error) {
if command.Opts.Debug {
fmt.Println("Creating a new client for:", url)
}
db, err := sqlx.Open("postgres", url)
if err != nil {
return nil, err
}
client := Client{
db: db,
ConnectionString: url,
History: history.New(),
}
return &client, nil
}
func (client *Client) Test() error {
return client.db.Ping()
}
func (client *Client) Info() (*Result, error) {
return client.query(statements.PG_INFO)
}
func (client *Client) Databases() ([]string, error) {
return client.fetchRows(statements.PG_DATABASES)
}
func (client *Client) Schemas() ([]string, error) {
return client.fetchRows(statements.PG_SCHEMAS)
}
func (client *Client) Tables() ([]string, error) {
return client.fetchRows(statements.PG_TABLES)
}
func (client *Client) Table(table string) (*Result, error) {
return client.query(statements.PG_TABLE_SCHEMA, table)
}
func (client *Client) TableRows(table string, opts RowsOptions) (*Result, error) {
sql := fmt.Sprintf(`SELECT * FROM "%s"`, table)
if opts.SortColumn != "" {
if opts.SortOrder == "" {
opts.SortOrder = "ASC"
}
sql += fmt.Sprintf(" ORDER BY %s %s", opts.SortColumn, opts.SortOrder)
}
if opts.Limit > 0 {
sql += fmt.Sprintf(" LIMIT %d", opts.Limit)
}
return client.query(sql)
}
func (client *Client) TableInfo(table string) (*Result, error) {
return client.query(statements.PG_TABLE_INFO, table)
}
func (client *Client) TableIndexes(table string) (*Result, error) {
res, err := client.query(statements.PG_TABLE_INDEXES, table)
if err != nil {
return nil, err
}
return res, err
}
// Returns all active queriers on the server
func (client *Client) Activity() (*Result, error) {
return client.query(statements.PG_ACTIVITY)
}
func (client *Client) Query(query string) (*Result, error) {
res, err := client.query(query)
// Save history records only if query did not fail
if err == nil {
client.History = append(client.History, history.NewRecord(query))
}
return res, err
}
func (client *Client) query(query string, args ...interface{}) (*Result, error) {
rows, err := client.db.Queryx(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
cols, err := rows.Columns()
if err != nil {
return nil, err
}
result := Result{Columns: cols}
for rows.Next() {
obj, err := rows.SliceScan()
for i, item := range obj {
if item == nil {
obj[i] = nil
} else {
t := reflect.TypeOf(item).Kind().String()
if t == "slice" {
obj[i] = string(item.([]byte))
}
}
}
if err == nil {
result.Rows = append(result.Rows, obj)
}
}
return &result, nil
}
func (res *Result) Format() []map[string]interface{} {
var items []map[string]interface{}
for _, row := range res.Rows {
item := make(map[string]interface{})
for i, c := range res.Columns {
item[c] = row[i]
}
items = append(items, item)
}
return items
}
func (res *Result) CSV() []byte {
buff := &bytes.Buffer{}
writer := csv.NewWriter(buff)
writer.Write(res.Columns)
for _, row := range res.Rows {
record := make([]string, len(res.Columns))
for i, item := range row {
if item != nil {
record[i] = fmt.Sprintf("%v", item)
} else {
record[i] = ""
}
}
err := writer.Write(record)
if err != nil {
fmt.Println(err)
break
}
}
writer.Flush()
return buff.Bytes()
}
// Close database connection
func (client *Client) Close() error {
return client.db.Close()
}
// Fetch all rows as strings for a single column
func (client *Client) fetchRows(q string) ([]string, error) {
res, err := client.query(q)
if err != nil {
return nil, err
}
// Init empty slice so json.Marshal will encode it to "[]" instead of "null"
results := make([]string, 0)
for _, row := range res.Rows {
results = append(results, row[0].(string))
}
return results, nil
}

255
pkg/client/client_test.go Normal file
View File

@@ -0,0 +1,255 @@
package client
import (
"fmt"
"os"
"os/exec"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
var testClient *Client
var testCommands map[string]string
func setupCommands() {
testCommands = map[string]string{
"createdb": "createdb",
"psql": "psql",
"dropdb": "dropdb",
}
if onWindows() {
for k, v := range testCommands {
testCommands[k] = v + ".exe"
}
}
}
func onWindows() bool {
return runtime.GOOS == "windows"
}
func setup() {
out, err := exec.Command(testCommands["createdb"], "-U", "postgres", "-h", "localhost", "booktown").CombinedOutput()
if err != nil {
fmt.Println("Database creation failed:", string(out))
fmt.Println("Error:", err)
os.Exit(1)
}
out, err = exec.Command(testCommands["psql"], "-U", "postgres", "-h", "localhost", "-f", "./data/booktown.sql", "booktown").CombinedOutput()
if err != nil {
fmt.Println("Database import failed:", string(out))
fmt.Println("Error:", err)
os.Exit(1)
}
}
func setupClient() {
testClient, _ = NewClientFromUrl("postgres://postgres@localhost/booktown?sslmode=disable")
}
func teardownClient() {
if testClient != nil {
testClient.db.Close()
}
}
func teardown() {
_, err := exec.Command(testCommands["dropdb"], "-U", "postgres", "-h", "localhost", "booktown").CombinedOutput()
if err != nil {
fmt.Println("Teardown error:", err)
}
}
func test_NewClientFromUrl(t *testing.T) {
url := "postgres://postgres@localhost/booktown?sslmode=disable"
client, err := NewClientFromUrl(url)
if err != nil {
defer client.db.Close()
}
assert.Equal(t, nil, err)
assert.Equal(t, url, client.connectionString)
}
func test_Test(t *testing.T) {
assert.Equal(t, nil, testClient.Test())
}
func test_Info(t *testing.T) {
res, err := testClient.Info()
assert.Equal(t, nil, err)
assert.NotEqual(t, nil, res)
}
func test_Databases(t *testing.T) {
res, err := testClient.Databases()
assert.Equal(t, nil, err)
assert.Contains(t, res, "booktown")
assert.Contains(t, res, "postgres")
}
func test_Tables(t *testing.T) {
res, err := testClient.Tables()
expected := []string{
"alternate_stock",
"authors",
"book_backup",
"book_queue",
"books",
"customers",
"daily_inventory",
"distinguished_authors",
"editions",
"employees",
"favorite_authors",
"favorite_books",
"money_example",
"my_list",
"numeric_values",
"publishers",
"recent_shipments",
"schedules",
"shipments",
"states",
"stock",
"stock_backup",
"stock_view",
"subjects",
"text_sorting",
}
assert.Equal(t, nil, err)
assert.Equal(t, expected, res)
}
func test_Table(t *testing.T) {
res, err := testClient.Table("books")
columns := []string{
"column_name",
"data_type",
"is_nullable",
"character_maximum_length",
"character_set_catalog",
"column_default",
}
assert.Equal(t, nil, err)
assert.Equal(t, columns, res.Columns)
assert.Equal(t, 4, len(res.Rows))
}
func test_TableRows(t *testing.T) {
res, err := testClient.TableRows("books", RowsOptions{})
assert.Equal(t, nil, err)
assert.Equal(t, 4, len(res.Columns))
assert.Equal(t, 15, len(res.Rows))
}
func test_TableInfo(t *testing.T) {
res, err := testClient.TableInfo("books")
assert.Equal(t, nil, err)
assert.Equal(t, 4, len(res.Columns))
assert.Equal(t, 1, len(res.Rows))
}
func test_TableIndexes(t *testing.T) {
res, err := testClient.TableIndexes("books")
assert.Equal(t, nil, err)
assert.Equal(t, 2, len(res.Columns))
assert.Equal(t, 2, len(res.Rows))
}
func test_Query(t *testing.T) {
res, err := testClient.Query("SELECT * FROM books")
assert.Equal(t, nil, err)
assert.Equal(t, 4, len(res.Columns))
assert.Equal(t, 15, len(res.Rows))
}
func test_QueryError(t *testing.T) {
res, err := testClient.Query("SELCT * FROM books")
assert.NotEqual(t, nil, err)
assert.Equal(t, "pq: syntax error at or near \"SELCT\"", err.Error())
assert.Equal(t, true, res == nil)
}
func test_QueryInvalidTable(t *testing.T) {
res, err := testClient.Query("SELECT * FROM books2")
assert.NotEqual(t, nil, err)
assert.Equal(t, "pq: relation \"books2\" does not exist", err.Error())
assert.Equal(t, true, res == nil)
}
func test_ResultCsv(t *testing.T) {
res, _ := testClient.Query("SELECT * FROM books ORDER BY id ASC LIMIT 1")
csv := res.CSV()
expected := "id,title,author_id,subject_id\n156,The Tell-Tale Heart,115,9\n"
assert.Equal(t, expected, string(csv))
}
func test_History(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books")
query := testClient.history[len(testClient.history)-1].Query
assert.Equal(t, nil, err)
assert.Equal(t, "SELECT * FROM books", query)
}
func test_HistoryError(t *testing.T) {
_, err := testClient.Query("SELECT * FROM books123")
query := testClient.history[len(testClient.history)-1].Query
assert.NotEqual(t, nil, err)
assert.NotEqual(t, "SELECT * FROM books123", query)
}
func TestAll(t *testing.T) {
if onWindows() {
// Dont have access to windows machines at the moment...
return
}
setupCommands()
teardown()
setup()
setupClient()
test_NewClientFromUrl(t)
test_Test(t)
test_Info(t)
test_Databases(t)
test_Tables(t)
test_Table(t)
test_TableRows(t)
test_TableInfo(t)
test_TableIndexes(t)
test_Query(t)
test_QueryError(t)
test_QueryInvalidTable(t)
test_ResultCsv(t)
test_History(t)
test_HistoryError(t)
teardownClient()
teardown()
}

39
pkg/command/options.go Normal file
View File

@@ -0,0 +1,39 @@
package command
import (
"os"
"github.com/jessevdk/go-flags"
)
type Options struct {
Version bool `short:"v" long:"version" description:"Print version"`
Debug bool `short:"d" long:"debug" description:"Enable debugging mode" default:"false"`
Url string `long:"url" description:"Database connection string"`
Host string `long:"host" description:"Server hostname or IP"`
Port int `long:"port" description:"Server port" default:"5432"`
User string `long:"user" description:"Database user"`
Pass string `long:"pass" description:"Password for user"`
DbName string `long:"db" description:"Database name"`
Ssl string `long:"ssl" description:"SSL option"`
HttpHost string `long:"bind" description:"HTTP server host" default:"localhost"`
HttpPort uint `long:"listen" description:"HTTP server listen port" default:"8081"`
AuthUser string `long:"auth-user" description:"HTTP basic auth user"`
AuthPass string `long:"auth-pass" description:"HTTP basic auth password"`
SkipOpen bool `short:"s" long:"skip-open" description:"Skip browser open on start"`
}
var Opts Options
func ParseOptions() error {
_, err := flags.ParseArgs(&Opts, os.Args)
if err != nil {
return err
}
if Opts.Url == "" {
Opts.Url = os.Getenv("DATABASE_URL")
}
return nil
}

View File

@@ -0,0 +1,100 @@
package connection
import (
"errors"
"fmt"
"os"
"os/user"
"strings"
"github.com/sosedoff/pgweb/pkg/command"
)
func currentUser() (string, error) {
u, err := user.Current()
if err == nil {
return u.Username, nil
}
name := os.Getenv("USER")
if name != "" {
return name, nil
}
return "", errors.New("Unable to detect OS user")
}
func FormatUrl(opts command.Options) (string, error) {
url := opts.Url
// Make sure to only accept urls in a standard format
if !strings.Contains(url, "postgres://") {
return "", errors.New("Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode")
}
// Special handling for local connections
if strings.Contains(url, "localhost") || strings.Contains(url, "127.0.0.1") {
if !strings.Contains(url, "?sslmode") {
if opts.Ssl == "" {
url += fmt.Sprintf("?sslmode=%s", "disable")
} else {
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
}
}
}
// Append sslmode parameter only if its defined as a flag and not present
// in the connection string.
if !strings.Contains(url, "?sslmode") && opts.Ssl != "" {
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
}
return url, nil
}
func IsBlank(opts command.Options) bool {
return opts.Host == "" && opts.User == "" && opts.DbName == "" && opts.Url == ""
}
func BuildString(opts command.Options) (string, error) {
if opts.Url != "" {
return FormatUrl(opts)
}
// Try to detect user from current OS user
if opts.User == "" {
u, err := currentUser()
if err == nil {
opts.User = u
}
}
// Disable ssl for localhost connections, most users have it disabled
if opts.Host == "localhost" || opts.Host == "127.0.0.1" {
if opts.Ssl == "" {
opts.Ssl = "disable"
}
}
url := "postgres://"
if opts.User != "" {
url += opts.User
}
if opts.Pass != "" {
url += fmt.Sprintf(":%s", opts.Pass)
}
url += fmt.Sprintf("@%s:%d", opts.Host, opts.Port)
if opts.DbName != "" {
url += fmt.Sprintf("/%s", opts.DbName)
}
if opts.Ssl != "" {
url += fmt.Sprintf("?sslmode=%s", opts.Ssl)
}
return url, nil
}

View File

@@ -0,0 +1,165 @@
package connection
import (
"fmt"
"os/user"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Invalid_Url(t *testing.T) {
opts := Options{}
examples := []string{
"postgresql://foobar",
"foobar",
}
for _, val := range examples {
opts.Url = val
str, err := buildConnectionString(opts)
assert.Equal(t, "", str)
assert.Error(t, err)
assert.Equal(t, "Invalid URL. Valid format: postgres://user:password@host:port/db?sslmode=mode", err.Error())
}
}
func Test_Valid_Url(t *testing.T) {
url := "postgres://myhost/database"
str, err := buildConnectionString(Options{Url: url})
assert.Equal(t, nil, err)
assert.Equal(t, url, str)
}
func Test_Url_And_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{
Url: "postgres://myhost/database",
Ssl: "disable",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://myhost/database?sslmode=disable", str)
}
func Test_Localhost_Url_And_No_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{
Url: "postgres://localhost/database",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=disable", str)
str, err = buildConnectionString(Options{
Url: "postgres://127.0.0.1/database",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=disable", str)
}
func Test_Localhost_Url_And_Ssl_Flag(t *testing.T) {
str, err := buildConnectionString(Options{
Url: "postgres://localhost/database",
Ssl: "require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
str, err = buildConnectionString(Options{
Url: "postgres://127.0.0.1/database",
Ssl: "require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
}
func Test_Localhost_Url_And_Ssl_Arg(t *testing.T) {
str, err := buildConnectionString(Options{
Url: "postgres://localhost/database?sslmode=require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://localhost/database?sslmode=require", str)
str, err = buildConnectionString(Options{
Url: "postgres://127.0.0.1/database?sslmode=require",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://127.0.0.1/database?sslmode=require", str)
}
func Test_Flag_Args(t *testing.T) {
str, err := buildConnectionString(Options{
Host: "host",
Port: 5432,
User: "user",
Pass: "password",
DbName: "db",
})
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@host:5432/db", str)
}
func Test_Localhost(t *testing.T) {
opts := Options{
Host: "localhost",
Port: 5432,
User: "user",
Pass: "password",
DbName: "db",
}
str, err := buildConnectionString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=disable", str)
opts.Host = "127.0.0.1"
str, err = buildConnectionString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@127.0.0.1:5432/db?sslmode=disable", str)
}
func Test_Localhost_And_Ssl(t *testing.T) {
opts := Options{
Host: "localhost",
Port: 5432,
User: "user",
Pass: "password",
DbName: "db",
Ssl: "require",
}
str, err := buildConnectionString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user:password@localhost:5432/db?sslmode=require", str)
}
func Test_No_User(t *testing.T) {
opts := Options{Host: "host", Port: 5432, DbName: "db"}
u, _ := user.Current()
str, err := buildConnectionString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, fmt.Sprintf("postgres://%s@host:5432/db", u.Username), str)
}
func Test_Port(t *testing.T) {
opts := Options{Host: "host", User: "user", Port: 5000, DbName: "db"}
str, err := buildConnectionString(opts)
assert.Equal(t, nil, err)
assert.Equal(t, "postgres://user@host:5000/db", str)
}
func Test_Blank(t *testing.T) {
assert.Equal(t, true, connectionSettingsBlank(Options{}))
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user"}))
assert.Equal(t, false, connectionSettingsBlank(Options{Host: "host", User: "user", DbName: "db"}))
assert.Equal(t, false, connectionSettingsBlank(Options{Url: "url"}))
}

492
pkg/data/bindata.go Normal file
View File

@@ -0,0 +1,492 @@
package data
import (
"fmt"
"io/ioutil"
"strings"
"os"
"path"
"path/filepath"
)
// bindata_read reads the given file from disk. It returns an error on failure.
func bindata_read(path, name string) ([]byte, error) {
buf, err := ioutil.ReadFile(path)
if err != nil {
err = fmt.Errorf("Error reading asset %s at %s: %v", name, path, err)
}
return buf, err
}
type asset struct {
bytes []byte
info os.FileInfo
}
// static_css_app_css reads file data from disk. It returns an error on failure.
func static_css_app_css() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/css/app.css"
name := "static/css/app.css"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_css_bootstrap_css reads file data from disk. It returns an error on failure.
func static_css_bootstrap_css() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/css/bootstrap.css"
name := "static/css/bootstrap.css"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_css_font_awesome_css reads file data from disk. It returns an error on failure.
func static_css_font_awesome_css() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/css/font-awesome.css"
name := "static/css/font-awesome.css"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_fonts_fontawesome_otf reads file data from disk. It returns an error on failure.
func static_fonts_fontawesome_otf() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/FontAwesome.otf"
name := "static/fonts/FontAwesome.otf"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_fonts_fontawesome_webfont_eot reads file data from disk. It returns an error on failure.
func static_fonts_fontawesome_webfont_eot() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.eot"
name := "static/fonts/fontawesome-webfont.eot"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_fonts_fontawesome_webfont_svg reads file data from disk. It returns an error on failure.
func static_fonts_fontawesome_webfont_svg() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.svg"
name := "static/fonts/fontawesome-webfont.svg"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_fonts_fontawesome_webfont_ttf reads file data from disk. It returns an error on failure.
func static_fonts_fontawesome_webfont_ttf() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.ttf"
name := "static/fonts/fontawesome-webfont.ttf"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_fonts_fontawesome_webfont_woff reads file data from disk. It returns an error on failure.
func static_fonts_fontawesome_webfont_woff() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/fonts/fontawesome-webfont.woff"
name := "static/fonts/fontawesome-webfont.woff"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_img_icon_ico reads file data from disk. It returns an error on failure.
func static_img_icon_ico() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/img/icon.ico"
name := "static/img/icon.ico"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_img_icon_png reads file data from disk. It returns an error on failure.
func static_img_icon_png() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/img/icon.png"
name := "static/img/icon.png"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_index_html reads file data from disk. It returns an error on failure.
func static_index_html() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/index.html"
name := "static/index.html"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_js_ace_pgsql_js reads file data from disk. It returns an error on failure.
func static_js_ace_pgsql_js() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/ace-pgsql.js"
name := "static/js/ace-pgsql.js"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_js_ace_js reads file data from disk. It returns an error on failure.
func static_js_ace_js() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/ace.js"
name := "static/js/ace.js"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_js_app_js reads file data from disk. It returns an error on failure.
func static_js_app_js() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/app.js"
name := "static/js/app.js"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// static_js_jquery_js reads file data from disk. It returns an error on failure.
func static_js_jquery_js() (*asset, error) {
path := "/Users/sosedoff/go/src/github.com/sosedoff/pgweb/static/js/jquery.js"
name := "static/js/jquery.js"
bytes, err := bindata_read(path, name)
if err != nil {
return nil, err
}
fi, err := os.Stat(path)
if err != nil {
err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err)
}
a := &asset{bytes: bytes, info: fi}
return a, err
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if (err != nil) {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"static/css/app.css": static_css_app_css,
"static/css/bootstrap.css": static_css_bootstrap_css,
"static/css/font-awesome.css": static_css_font_awesome_css,
"static/fonts/FontAwesome.otf": static_fonts_fontawesome_otf,
"static/fonts/fontawesome-webfont.eot": static_fonts_fontawesome_webfont_eot,
"static/fonts/fontawesome-webfont.svg": static_fonts_fontawesome_webfont_svg,
"static/fonts/fontawesome-webfont.ttf": static_fonts_fontawesome_webfont_ttf,
"static/fonts/fontawesome-webfont.woff": static_fonts_fontawesome_webfont_woff,
"static/img/icon.ico": static_img_icon_ico,
"static/img/icon.png": static_img_icon_png,
"static/index.html": static_index_html,
"static/js/ace-pgsql.js": static_js_ace_pgsql_js,
"static/js/ace.js": static_js_ace_js,
"static/js/app.js": static_js_app_js,
"static/js/jquery.js": static_js_jquery_js,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for name := range node.Children {
rv = append(rv, name)
}
return rv, nil
}
type _bintree_t struct {
Func func() (*asset, error)
Children map[string]*_bintree_t
}
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"static": &_bintree_t{nil, map[string]*_bintree_t{
"css": &_bintree_t{nil, map[string]*_bintree_t{
"app.css": &_bintree_t{static_css_app_css, map[string]*_bintree_t{
}},
"bootstrap.css": &_bintree_t{static_css_bootstrap_css, map[string]*_bintree_t{
}},
"font-awesome.css": &_bintree_t{static_css_font_awesome_css, map[string]*_bintree_t{
}},
}},
"fonts": &_bintree_t{nil, map[string]*_bintree_t{
"FontAwesome.otf": &_bintree_t{static_fonts_fontawesome_otf, map[string]*_bintree_t{
}},
"fontawesome-webfont.eot": &_bintree_t{static_fonts_fontawesome_webfont_eot, map[string]*_bintree_t{
}},
"fontawesome-webfont.svg": &_bintree_t{static_fonts_fontawesome_webfont_svg, map[string]*_bintree_t{
}},
"fontawesome-webfont.ttf": &_bintree_t{static_fonts_fontawesome_webfont_ttf, map[string]*_bintree_t{
}},
"fontawesome-webfont.woff": &_bintree_t{static_fonts_fontawesome_webfont_woff, map[string]*_bintree_t{
}},
}},
"img": &_bintree_t{nil, map[string]*_bintree_t{
"icon.ico": &_bintree_t{static_img_icon_ico, map[string]*_bintree_t{
}},
"icon.png": &_bintree_t{static_img_icon_png, map[string]*_bintree_t{
}},
}},
"index.html": &_bintree_t{static_index_html, map[string]*_bintree_t{
}},
"js": &_bintree_t{nil, map[string]*_bintree_t{
"ace-pgsql.js": &_bintree_t{static_js_ace_pgsql_js, map[string]*_bintree_t{
}},
"ace.js": &_bintree_t{static_js_ace_js, map[string]*_bintree_t{
}},
"app.js": &_bintree_t{static_js_app_js, map[string]*_bintree_t{
}},
"jquery.js": &_bintree_t{static_js_jquery_js, map[string]*_bintree_t{
}},
}},
}},
}}
// Restore an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// Restore assets under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
if err != nil { // File
return RestoreAsset(dir, name)
} else { // Dir
for _, child := range children {
err = RestoreAssets(dir, path.Join(name, child))
if err != nil {
return err
}
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

21
pkg/history/history.go Normal file
View File

@@ -0,0 +1,21 @@
package history
import (
"time"
)
type Record struct {
Query string `json:"query"`
Timestamp string `json:"timestamp"`
}
func New() []Record {
return make([]Record, 0)
}
func NewRecord(query string) Record {
return Record{
Query: query,
Timestamp: time.Now().String(),
}
}

47
pkg/statements/sql.go Normal file
View File

@@ -0,0 +1,47 @@
package statements
const (
PG_DATABASES = `SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname ASC`
PG_SCHEMAS = `SELECT schema_name FROM information_schema.schemata ORDER BY schema_name ASC`
PG_INFO = `SELECT
session_user
, current_user
, current_database()
, current_schemas(false)
, inet_client_addr()
, inet_client_port()
, inet_server_addr()
, inet_server_port()
, version()`
PG_TABLE_INDEXES = `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = $1`
PG_TABLE_INFO = `SELECT
pg_size_pretty(pg_table_size($1)) AS data_size
, pg_size_pretty(pg_indexes_size($1)) AS index_size
, pg_size_pretty(pg_total_relation_size($1)) AS total_size
, (SELECT reltuples FROM pg_class WHERE oid = $1::regclass) AS rows_count`
PG_TABLE_SCHEMA = `SELECT
column_name, data_type, is_nullable, character_maximum_length, character_set_catalog, column_default
FROM information_schema.columns
WHERE table_name = $1`
PG_TABLES = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_schema,table_name`
PG_ACTIVITY = `SELECT
datname,
query,
state,
waiting,
query_start,
state_change,
pid,
datid,
application_name,
client_addr
FROM pg_stat_activity
WHERE state IS NOT NULL`
)

32
pkg/util/profiler.go Normal file
View File

@@ -0,0 +1,32 @@
package util
import (
"log"
"os"
"runtime"
"time"
)
const MEGABYTE = 1024 * 1024
func runProfiler() {
logger := log.New(os.Stdout, "", 0)
m := &runtime.MemStats{}
for {
runtime.ReadMemStats(m)
logger.Printf(
"[DEBUG] Goroutines: %v, Mem used: %v (%v mb), Mem acquired: %v (%v mb)\n",
runtime.NumGoroutine(),
m.Alloc, m.Alloc/MEGABYTE,
m.Sys, m.Sys/MEGABYTE,
)
time.Sleep(time.Second * 30)
}
}
func StartProfiler() {
go runProfiler()
}