Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
little-brother committed Apr 11, 2018
0 parents commit 15d7277
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
httpsql.exe
httpsql.zip
config.json
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2018 Little brother.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Httpsql is a http server to provides a simple way of monitoring SQL databases via a http request by predefined queries.

### How to build app
1. Download and install [Golang](https://golang.org/dl/)
2. Download dependencies
```bash
go get -v -d ./
```
3. Build application
```bash
go build -ldflags="-s -w"
```

Also you can download [binary](https://github.com/little-brother/httpsql/releases).

### How to use
Before continue you must create `config.json` in app folder. Below is an example:

<pre>
{
"port": "9000",
"databases": {

"demo": {
"driver": "mysql",
"dns": "myuser@tcp(192.168.0.101)/mydb",
"metrics": ["now", "count", "minmax", "getbyid"]
},

"demo2": {
"driver": "postgres",
"dns": "host=192.168.0.101 user=home password=password dbname=mydb2 sslmode=disable",
"metrics": ["count", "minmax"]
}
},

"metrics": {

"now": {
"query": "select now()",
"description": "Params: none. Returns current date and time."
},

"count": {
"query": "select count(1) as count from #table",
"description": "Params: table. Returns row count of table."
},

"minmax": {
"query": "select min(#column) min, max(#column) max from #table",
"description": "Params: table, column. Returns max and min value."
},

"getbyid": {
"query": "select * from #table where id = $id",
"description": "Params: table, id. Returns row with requested id."
}
}
}
</pre>

The following links will be available for this configuration:
* `/` returns all database aliases: `demo` and `demo2`
* `/demo` returns all available metrics for database `demo` and their description
* `/demo/now` returns `mydb` date and time
* `/demo/count?table=orders` returns row count for `mydb.orders`
* `/demo/minmax?table=orders&column=price` returns minimal and maximum `price` in `mydb.orders`
* `/demo/getbyid?table=orders&id=10` returns order detail with id = `10`
* `/demo2/count?table=customers` returns customer count in `mydb2.customers`
* `/demo2/minmax?table=...`

In query `#param` defines a url parameter `param` that value will be substituted directly into the query. To avoid sql injections, all characters except `a-Z0-9_.$` will be removed from value and length is limited to 64 characters. `$param` defines a placeholder parameter and can contains any symbols.
<br>

Request result is `json` or `text`(csv). By default data format defines by http `Accept` header. You can lock format by adding `json` or `text` to requested url e.g. `/demo2/count?table=customers&text`.
<br>

| One permanent connection is used for each database. If necessary, the connection will be restored. |
|---|

### Supported databases

|DBMS|Driver|Dns example|
|-----|--------|----------|
|MySQL|[mysql](https://github.com/go-sql-driver/mysql)|myuser@tcp(192.168.0.101)/mydb|
|PosgreSQL|[postgres](https://github.com/lib/pq)|host=192.168.0.101 user=home password=password dbname=mydb sslmode=disable|
|MSSQL|[mssql](https://github.com/denisenkom/go-mssqldb)|sqlserver://username:password@host/instance?param1=value&param2=value<br>sqlserver://username:password@host:port?param1=value&param2=value|
|ADODB|[adodb](https://github.com/mattn/go-adodb)|Provider=Microsoft.Jet.OLEDB.4.0;Data Source=my.mdb;|
|ODBC|[odbc](https://github.com/alexbrainman/odbc)|Driver={Microsoft ODBC for Oracle};Server=ORACLE8i7;Persist Security Info=False;Trusted_Connection=Yes|
|ClickHouse|[clickhouse](https://github.com/kshvakov/clickhouse)|tcp://127.0.0.1:9000?username=&debug=true|
|Firebird|[firebirdsql](https://github.com/nakagami/firebirdsql)|user:password@servername/foo/bar.fdb|
|SQLite3|[sqlite3](https://github.com/mattn/go-sqlite3)|D:/aaa/bbb/mydb.sqlite|

> Notice: most databases require additional configuration for remote connections

You can add other [drivers](https://github.com/golang/go/wiki/SQLDrivers) but some of them requires additional software.
248 changes: 248 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package main
import (
"fmt"
"regexp"
"strings"
"net/http"
"encoding/json"
"io/ioutil"
"database/sql"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/denisenkom/go-mssqldb"
_ "github.com/mattn/go-adodb"
_ "github.com/alexbrainman/odbc"
_ "github.com/kshvakov/clickhouse"
_ "github.com/nakagami/firebirdsql"
_ "github.com/mattn/go-sqlite3"

// _ "github.com/mattn/go-oci8"
)

var config Config
var connections map[string]*sql.DB
var reSafe = regexp.MustCompile("[^a-zA-Z0-9_\\.$]+")

type Config struct {
Port string
Databases map[string]Database
Metrics map[string]Metric
}

type Database struct {
Driver string
Dns string
Metrics []string
}

type Metric struct {
Query string
Description string
}

func sendJson(w http.ResponseWriter, data interface{}) {
jsonData, err := json.Marshal(data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
w.Write(jsonData)
return
}

func sendText(w http.ResponseWriter, data []map[string]interface {}, columns []string) {
if len(data) == 0 {
fmt.Fprintf(w, "")
return
}

text := ""
for i, row := range (data) {
values := make([]string, 0);
for _, col := range(columns) {
if row[col] != nil {
values = append(values, fmt.Sprintf("%v", row[col]))
} else {
values = append(values, "null")
}
}

text = text + strings.Join(values, ";")
if i != len(data) - 1 {
text = text + "\n"
}
}

fmt.Fprintf(w, "%v", text)
return
}

func httpHandler(w http.ResponseWriter, r *http.Request) {
url := strings.Split(r.URL.Path, "/")
if (len(url) < 2) {
http.Error(w, "UNKNOWN", http.StatusInternalServerError)
return
}

alias := url[1]
metric := ""
if len(url) > 2 {
metric = url[2]
}

if alias == "" {
keys := []string{}
for k := range config.Databases {
keys = append(keys, k)
}

sendJson(w, keys)
return
}

prop, ok := config.Databases[alias];
if !ok {
http.Error(w, "NOT_FOUND", http.StatusNotFound)
return
}

if metric == "" {
mdesc := make(map[string]string)
for _, name := range (prop.Metrics) {
m, ok := config.Metrics[name];
if ok {
mdesc[name] = m.Description
}
}
sendJson(w, mdesc)
return
}

hasMetric := false
for _, m := range(config.Databases[alias].Metrics) {
hasMetric = hasMetric || m == metric
}

if !hasMetric {
http.Error(w, "NOT_FOUND", http.StatusNotFound)
return
}

mprop, ok := config.Metrics[metric];
if !ok {
http.Error(w, "NOT_FOUND", http.StatusNotFound)
return
}

var err error
db, ok := connections[alias]
if !ok || db == nil {
db, err = sql.Open(prop.Driver, prop.Dns)
if err != nil {
http.Error(w, "ECONNREFUSED", http.StatusInternalServerError)
fmt.Println(alias +"/" + metric + "\n", err.Error())
return
}
connections[alias] = db
} else {
err = db.Ping()
if err != nil {
http.Error(w, "ECONNREFUSED", http.StatusInternalServerError)
fmt.Println(alias +"/" + metric + "\n", err.Error())
defer db.Close()
return
}
}

query := mprop.Query
var params []interface{}
for param, value := range (r.URL.Query()) {
if len(value) > 0 && len(value[0]) < 64 {
val := reSafe.ReplaceAllString(value[0], "")
query = strings.Replace(query, "#" + param, val, -1)
}

if len(value) > 0 && strings.Contains(query, "$" + param) {
params = append(params, value[0]);
query = strings.Replace(query, "$" + param, "?", 1)
}
}

rows, err := db.Query(query, params...)
if err != nil {
http.Error(w, "SQL_ERROR", http.StatusInternalServerError)
fmt.Println(alias +"/" + metric + "\n", "Query: ", query, "\n", "Params: ", params, "\n", err.Error())
return
}
defer rows.Close()

columns, err := rows.Columns()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

count := len(columns)
tableData := make([]map[string]interface{}, 0)
values := make([]interface{}, count)
valuePtrs := make([]interface{}, count)
for rows.Next() {
for i := 0; i < count; i++ {
valuePtrs[i] = &values[i]
}
rows.Scan(valuePtrs...)
entry := make(map[string]interface{})
for i, col := range columns {
var v interface{}
val := values[i]
b, ok := val.([]byte)
if ok {
v = string(b)
} else {
v = val
}
entry[col] = v
}
tableData = append(tableData, entry)
}

_, jsonok := r.URL.Query()["json"]
_, textok := r.URL.Query()["text"]
if textok || !jsonok && strings.HasPrefix(r.Header.Get("Accept"), "text") {
sendText(w, tableData, columns)
return
}

sendJson(w, tableData)
return
}

func main() {
file, err := ioutil.ReadFile("./config.json")
if err != nil {
fmt.Printf("Couldn't read config.json\n", err.Error())
return
}

err = json.Unmarshal(file, &config)
if err != nil {
fmt.Println("Couldn't parse config.json\n", err.Error())
return
}

connections = make(map[string]*sql.DB, 0)

if config.Port == "" {
config.Port = "9000"
}

http.HandleFunc("/", httpHandler)
fmt.Println("Httpsql running on " + config.Port + " port\n")
err = http.ListenAndServe(":" + config.Port, nil)
if err != nil {
fmt.Println("Couldn't start http server\n", err.Error())
return
}
}

0 comments on commit 15d7277

Please sign in to comment.