1
0
Fork 0

added initial skeleton

This commit is contained in:
Sangbum Kim 2017-08-21 17:58:21 +09:00
commit b71f88cbf7
41 changed files with 4186 additions and 0 deletions

127
.gitignore vendored Normal file
View File

@ -0,0 +1,127 @@
# Created by https://www.gitignore.io/api/intellij,go,linux,osx,windows
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea
*.iml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### Go ###
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
### Linux ###
*~
*.swp
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Windows ###
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
build/
bind/
vendor/
mecab/
*.log
rooibos

21
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,21 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "rooibos dev",
"type": "go",
"request": "launch",
"mode": "debug",
"remotePath": "",
"port": 2345,
"host": "127.0.0.1",
"program": "${workspaceRoot}/rooibos",
"env": {},
"args": [
"-C",
"dev.yml"
],
"showLog": true
}
]
}

53
Makefile Normal file
View File

@ -0,0 +1,53 @@
# This how we want to name the binary output
BINARY=rooibos
# These are the values we want to pass for VERSION and BUILD
# git tag 1.0.1
# git commit -am "One more change after the tags"
VERSION=`git describe --always`
BUILD=`date +%FT%T%z`
# Setup the -ldflags option for go build here, interpolate the variable values
LDFLAGS=-ldflags "-w -s -X amuz.es/gogs/infra/rooibos/util.version=${VERSION} -X amuz.es/gogs/infra/rooibos/util.buildDate=${BUILD} "
CGO_ENABLED=0
build:
go build ${LDFLAGS} -tags=jsoniter -o ${BINARY} .
strip -x ${BINARY}
setup:
go get -u github.com/kardianos/govendor
# go get -u github.com/gogo/protobuf/proto
# go get -u github.com/gogo/protobuf/protoc-gen-gogo
# go get -u github.com/gogo/protobuf/gogoproto
# go get -u github.com/gogo/protobuf/protoc-gen-gofast
# go get -u github.com/gogo/protobuf/protoc-gen-gogofaster
# go get -u github.com/golang/protobuf/proto
${GOPATH}/bin/govendor fetch -v +missing
#libjpeg-turbo
generate:
go get -u github.com/gogo/protobuf/proto
go get -u github.com/gogo/protobuf/protoc-gen-gogo
go get -u github.com/gogo/protobuf/gogoproto
go get -u github.com/gogo/protobuf/protoc-gen-gofast
go get -u github.com/gogo/protobuf/protoc-gen-gogofaster
go get -u github.com/golang/protobuf/proto
protoc --gogofaster_out=. --proto_path=../../../../:. subsys/redis/app_token_data.proto
#${GOPATH}/bin/ffjson subsys/http/iface/rest.go
#${GOPATH}/bin/easyjson subsys/http/iface/rest.go
strip:
upx rooibos
# Installs our project: copies binaries
install:
go install ${LDFLAGS}
# Cleans our project: deletes binaries
clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
.PHONY: clean install

98
README.md Normal file
View File

@ -0,0 +1,98 @@
![alt tag](https://upload.wikimedia.org/wikipedia/commons/2/23/Golang.png)
[![Build Status](https://travis-ci.org/Massad/gin-boilerplate.svg?branch=master)](https://travis-ci.org/Massad/gin-boilerplate)
[![Join the chat at https://gitter.im/Massad/gin-boilerplate](https://badges.gitter.im/Massad/gin-boilerplate.svg)](https://gitter.im/Massad/gin-boilerplate?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
Welcome to **Golang Gin boilerplate**!
The fastest way to deploy a restful api's with [Gin Framework](https://gin-gonic.github.io/gin/) with a structured project that defaults to **PostgreSQL** database and **Redis** as the session storage.
## Configured with
* [go-gorp](github.com/go-gorp/gorp): Go Relational Persistence
* [RedisStore](https://github.com/gin-gonic/contrib/tree/master/sessions): Gin middleware for session management with multi-backend support (currently cookie, Redis).
* Built-in **CORS Middleware**
* Feature **PostgreSQL 9.4** JSON queries
* Unit test
### Installation
```
$ go get github.com/Massad/gin-boilerplate
```
```
$ cd $GOPATH/src/github.com/Massad/gin-boilerplate
```
```
$ go get -t -v ./...
```
> Sometimes you need to get this package manually
```
$ go get github.com/bmizerany/assert
```
You will find the **database.sql** in `db/database.sql`
And you can import the postgres database using this command:
```
$ psql -U postgres -h localhost < ./db/database.sql
```
## Running Your Application
```
$ go run *.go
```
## Building Your Application
```
$ go build -v
```
```
$ ./gin-boilerplate
```
## Testing Your Application
```
$ go test -v ./tests/*
```
## Import Postman Collection (API's)
You can import from this [link](https://www.getpostman.com/collections/ac0680f90961bafd5de7). If you don't have **Postman**, check this link [https://www.getpostman.com](https://www.getpostman.com/)
## Contribution
You are welcome to contribute to keep it up to date and always improving!
If you have any question or need help, drop a message at [https://gitter.im/Massad/gin-boilerplate](https://gitter.im/Massad/gin-boilerplate)
---
## License
(The MIT License)
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.

36
bench.py Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
import os
import re
import time
def report(c, lines):
rtv = []
for str in lines:
str=re.sub("^[ \t]+",'', str)
if str.find("Latency") == 0 or str.find("Requests/sec") == 0:
token = re.split("[ \t]+", str)
value = re.sub("[^0-9\.]", '', token[1])
if re.match("[0-9\.]+us", token[1]):
rtv.append(float(value)/1000)
elif re.match("[0-9\.]+s", token[1]):
rtv.append(float(value)*1000)
else:
rtv.append(float(value))
print c,"\t", rtv[0],"\t",rtv[1]
def run(cmd):
stdin, stdout, stderr = os.popen3(cmd)
return stdout.readlines()
offset = 5
maxConcurrency = offset * 61
duration = 10
for num in range(1, maxConcurrency/offset):
concurrency = num * offset
thread = 16 if concurrency > 16 else concurrency
cmd = (
'''wrk --timeout 7 -t %d -c %d -d %d http://localhost:8080/api/v1/media/admin/2/thumb'''
% (thread, concurrency, duration))
result =run(cmd)
report(concurrency, result)
time.sleep(5)

80
enums/member_status.go Normal file
View File

@ -0,0 +1,80 @@
package enums
import (
"bytes"
"errors"
)
type MemberStatus int8
const (
MemberStatusInvalid MemberStatus = 0
MemberStatusDisabled MemberStatus = 1
MemberStatusLocked MemberStatus = 2
MemberStatusEnabled MemberStatus = 99
)
func MemberStatusNameOf(value string) MemberStatus {
switch value {
case "INVALID":
return MemberStatusInvalid
case "DISABLED":
return MemberStatusDisabled
case "LOCKED":
return MemberStatusLocked
case "ENABLED":
return MemberStatusEnabled
}
return MemberStatusInvalid
}
func (status MemberStatus) Name() string {
switch status {
case MemberStatusInvalid:
return "INVALID"
case MemberStatusDisabled:
return "DISABLED"
case MemberStatusLocked:
return "LOCKED"
case MemberStatusEnabled:
return "ENABLED"
}
return "INVALID"
}
func (status MemberStatus) String() string {
switch status {
case MemberStatusInvalid:
return "알 수 없음"
case MemberStatusDisabled:
return "사용중지"
case MemberStatusLocked:
return "사용일시중지"
case MemberStatusEnabled:
return "사용중"
}
return "알 수 없음"
}
func (status *MemberStatus) UnmarshalJSON(from []byte) error {
quote := []byte("\"")
quoteSize := len(quote)
if len(from) < quoteSize*2 {
return errors.New("invalid quote notation")
}
if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) {
return errors.New("invalid quote notation")
}
*status = MemberStatusNameOf(string(from[quoteSize:len(from)-quoteSize]))
return nil
}
func (status MemberStatus) MarshalJSON() ([]byte, error) {
var buffer bytes.Buffer
buffer.WriteRune('"')
buffer.WriteString(status.Name())
buffer.WriteRune('"')
return buffer.Bytes(), nil
}

149
main.go Normal file
View File

@ -0,0 +1,149 @@
package main
import (
"os"
"os/signal"
"runtime"
"syscall"
"time"
"amuz.es/gogs/infra/rooibos/subsys/http"
"amuz.es/gogs/infra/rooibos/subsys/periodic"
"amuz.es/gogs/infra/rooibos/util"
"gopkg.in/alecthomas/kingpin.v2"
"sync"
"context"
)
var (
app = kingpin.New(util.Config.Name(), "graceful API server").Author("tom.cat")
verbose = app.Flag("verbose", "Enable verbose mode.").Short('v').Bool()
logDir = app.Flag("log-dir", "logstore directory.").Short('L').String()
configFile = app.Flag("config-file", "config file path").Default("settings.yml").HintOptions("FILENAME").Short('C').String()
logger = util.NewLogger("main")
)
func systemStart(ctx context.Context, errorSignal chan error, waiter *sync.WaitGroup) {
defer func() {
if err := recover(); err != nil {
errorSignal <- err.(error)
}
}()
start := time.Now()
app.Version(util.Config.Version())
if _, err := app.Parse(os.Args[1:]); err != nil {
panic(err)
}
setMaxProcs()
if err := util.LoadConfig(*configFile); err != nil {
panic(err)
}
util.InitLogger(*verbose, *logDir, &util.Config.Logging.Application)
showBanner(util.Config.Phase, util.Config.Version(), util.Config.BuildDate())
// init subsystem
//db.InitDB(&util.Config.Db)
//redis.InitRedis(&util.Config.Redis)
//redis.AppTokenInitService(util.Config.Phase, util.Config.AppToken.Expires, util.Config.AppToken.RefreshExpires)
//redis.UploadFileInitService(util.Config.Phase, util.Config.AppToken.UploadRetryExpires)
periodic.Start(errorSignal, ctx, waiter)
http.Start(util.Config.Bind, &util.Config.Logging.Access, errorSignal, ctx, waiter)
logger.Info("bootstrapped application ", time.Since(start))
util.NotifyDaemon(util.DaemonStarted)
}
func systemReload() {
util.RotateLogger()
}
func systemTeardown(exitCode *int, waiter *sync.WaitGroup) {
logger.Info("closing application")
util.NotifyDaemon(util.DaemonStopping)
waiter.Wait()
//redis.CloseRedis()
logger.Info("bye")
os.Exit(*exitCode)
}
func main() {
var (
exitCode = 0
ctx, canceled = context.WithCancel(context.Background())
waiter = &sync.WaitGroup{}
exitSignal = make(chan os.Signal, 1)
errorSignal = make(chan error, 10)
)
systemStart(ctx, errorSignal, waiter)
defer systemTeardown(&exitCode, waiter)
signal.Notify(exitSignal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
for {
select {
case <-ctx.Done():
logger.Info("Service request to close this application")
return
case sysSignal := <-exitSignal:
switch sysSignal {
case syscall.SIGHUP:
systemReload()
default:
canceled()
logger.Info("SYSCALL! ", sysSignal.String())
return
}
case err := <-errorSignal:
canceled()
logger.Error("exception raised! ", err)
exitCode = -1
return
}
}
}
func setMaxProcs() {
// TODO(vmarmol): Consider limiting if we have a CPU mask in effect.
// Allow as many threads as we have cores unless the user specified a value.
var numProcs int
// if *maxProcs < 1 {
numProcs = runtime.NumCPU()
// } else {
// numProcs = *maxProcs
// }
runtime.GOMAXPROCS(numProcs)
// Check if the setting was successful.
actualNumProcs := runtime.GOMAXPROCS(0)
if actualNumProcs != numProcs {
logger.Warn("Specified max procs of %v but using %v", numProcs, actualNumProcs)
}
}
func showBanner(phase string, version string, buildDate string) {
if util.LoggerIsStd() {
logger.Info(`
{
{ }
}_{ __{
.-{ } }-. ` + util.Config.Name() + `
( } { ) version: ` + version + `
` + "|`-.._____..-'| buildDate: " + buildDate + `
| ;--. phase: ` + phase + `
| (__ \
| | ) )
| |/ /
| / /
| ( /
\ y'
` + "`-.._____..-'")
} else {
logger.
WithField("version", version).
WithField("buildDate", buildDate).
WithField("phase", phase).
Info("##", util.Config.Name())
}
}

28
rooibos-dev.service Normal file
View File

@ -0,0 +1,28 @@
[Unit]
Description=Rooibos API Daemon
Documentation=https://amuz.es
After=syslog.target network-online.target
Wants=network-online.target
[Service]
Type=notify
Restart=always
RestartSec=15
ExecStart=/opt/eden/service/rooibos/cur-dev/rooibos -C dev.yml -L /opt/eden/service/rooibos/logs/dev
WorkingDirectory=/opt/eden/service/rooibos/cur-dev
#ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=5
# Disable timeout logic and wait until process is stopped
TimeoutStopSec=0
# SIGTERM signal is used to stop Minio
KillSignal=SIGTERM
SendSIGKILL=no
SuccessExitStatus=0
# kill only the docker process, not all processes in the cgroup
KillMode=process
[Install]
WantedBy=default.target

29
settings.yml Normal file
View File

@ -0,0 +1,29 @@
phase: development
db:
network: tcp
host: db.gear.amuz.es
driver: mysql
user: db-dev
password: "ttt4t4"
port: 3306
db: yori-dev
args: "readTimeout=3s&parseTime=true&collation=utf8mb4_unicode_ci&strict=true&sql_notes=false&time_zone=%27UTC%27&loc=UTC"
redis:
host: db.gear.amuz.es
port: 6379
password: fgfgdgf
db: 1
maxretries: 7
poolsize: 30
logging:
application:
filename: "Stdout"
maxsizemb: 500
maxbackup: 30
maxday: 14
access:
filename: "access.log"
maxsizemb: 500
maxbackup: 30
maxday: 14
bind: "0.0.0.0:8082"

74
subsys/db/conn.go Normal file
View File

@ -0,0 +1,74 @@
package db
import (
"errors"
"fmt"
"amuz.es/gogs/infra/rooibos/util"
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
errs "github.com/pkg/errors"
)
var (
db *sqlx.DB = nil
logger = util.NewLogger("sql")
)
func InitDB(config *util.DbConfig) {
connectStr := fmt.Sprintf(
"%s:%s@%s(%s:%d)/%s?%s",
config.User,
config.Password,
config.Network,
config.Host,
config.Port,
config.DB,
config.Args)
logger.Debugf("connect to %s@%s(%s:%d)/%s?%s",
config.User,
config.Network,
config.Host,
config.Port,
config.DB,
config.Args)
if conn, err := sqlx.Connect(config.Driver, connectStr); err != nil {
panic(errs.Wrap(err, "failed to connect db"))
} else {
db = conn
//https://github.com/go-sql-driver/mysql/issues/257 무조건 0
db.SetMaxIdleConns(0)
db.SetMaxOpenConns(25)
logger.Info("DB connected")
prepareSqls()
}
}
func prepareSqls() {
memberPrepare()
}
func Begin() (tx *sqlx.Tx, err error) {
tx, err = db.Beginx()
return
}
func prepareSql(sql string) (*sqlx.Stmt, error) {
if stmt, err := db.Preparex(sql); err != nil {
return nil, errs.Wrapf(err, "failed to prepare sql %s", sql)
} else {
return stmt, nil
}
}
func tablecheck(tablename string) error {
var fetched int
if db.Get(
&fetched, `
SELECT COUNT(*)
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = ?`,
tablename) != nil || fetched != 1 {
return errors.New(fmt.Sprintf("table %s not exist", tablename))
}
return nil
}

24
subsys/db/create.my.sql Normal file
View File

@ -0,0 +1,24 @@
BEGIN;
CREATE TABLE IF NOT EXISTS member
(
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NULL,
contact VARCHAR(128) NOT NULL,
name VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
status TINYINT NOT NULL,
company_seq BIGINT UNSIGNED NOT NULL,
UNIQUE INDEX uk_member_01 (company_seq,contact),
INDEX idx_member_01 (company_seq),
INDEX idx_member_02 (contact),
INDEX idx_member_03 (status)
)
ENGINE=InnoDB
ROW_FORMAT=COMPRESSED
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
;

191
subsys/db/member.go Normal file
View File

@ -0,0 +1,191 @@
package db
import (
"bytes"
"amuz.es/gogs/infra/rooibos/enums"
"database/sql"
"strconv"
"time"
"github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
)
var (
memberInsertQuery *sqlx.Stmt
memberSelectByContactQuery *sqlx.Stmt
memberSelectByIdQuery *sqlx.Stmt
)
type member struct {
In_Id uint64 `db:"id"`
In_CreatedAt time.Time `db:"created_at"`
In_UpdatedAt mysql.NullTime `db:"updated_at"`
In_Contact string `db:"contact"`
In_Name string `db:"name"`
In_Password string `db:"password"`
In_Status enums.MemberStatus `db:"status"`
In_CompanySeq uint64 `db:"company_seq"`
}
func (member *member) Id() uint64 { return member.In_Id }
func (member *member) CreatedAt() time.Time { return member.In_CreatedAt }
func (member *member) UpdatedAt() *time.Time {
if member.In_UpdatedAt.Valid {
return &member.In_UpdatedAt.Time
} else {
return nil
}
}
func (member *member) Contact() string { return member.In_Contact }
func (member *member) Name() string { return member.In_Name }
func (member *member) Status() enums.MemberStatus { return member.In_Status }
func (member *member) CompanySeq() uint64 { return member.In_CompanySeq }
func (member *member) String() string {
var buffer bytes.Buffer
buffer.WriteString("Member(")
if member != nil {
buffer.WriteString(strconv.FormatUint(member.In_Id, 10))
} else {
buffer.WriteString("nil")
}
buffer.WriteString(")")
return buffer.String()
}
func NewMember(
id uint64,
createdAt time.Time,
updatedAt *time.Time,
contact string,
name string,
password string,
status enums.MemberStatus,
companySeq uint64) (Member){
var updatedAtWrapped mysql.NullTime
if updatedAt != nil{
updatedAtWrapped=mysql.NullTime{*updatedAt, true}
}
return &member{
id,
createdAt,
updatedAtWrapped,
contact,
name,
password,
status,
companySeq,
}
}
type Member interface {
Id() uint64
CreatedAt() time.Time
UpdatedAt() *time.Time
Contact() string
Name() string
Status() enums.MemberStatus
CompanySeq() uint64
String() string
}
func memberPrepare() {
if err := tablecheck("member"); err != nil {
panic(err)
}
if query, err := prepareSql(
`INSERT INTO member (
created_at, contact, name,
password, status, company_seq
)
VALUES
(NOW(), ?,?,?,?,?)
`); err != nil {
panic(err)
} else {
memberInsertQuery = query
}
if query, err := prepareSql(` select * from member where contact=? and status=? order by id desc limit 1`); err != nil {
panic(err)
} else {
memberSelectByContactQuery = query
}
if query, err := prepareSql(`Select * from member where id=?`); err != nil {
panic(err)
} else {
memberSelectByIdQuery = query
}
logger.Debug("Member query prepared")
}
func MemberFindByContactWithTx(tx *sqlx.Tx, contact string, status enums.MemberStatus) (Member, error) {
obj := member{}
if err := tx.Stmtx(memberSelectByContactQuery).Get(&obj, contact, status); err != nil {
return nil, err
}
return &obj, nil
}
func MemberFindByContactOne(contact string, status enums.MemberStatus) (Member, error) {
obj := member{}
if err := memberSelectByContactQuery.Get(&obj, contact, status); err != nil {
return nil, err
}
return &obj, nil
}
func MemberFindOneWithTx(tx *sqlx.Tx, pk uint64) (Member, error) {
obj := member{}
if err := tx.Stmtx(memberSelectByIdQuery).Get(&obj, pk); err != nil {
return nil, err
}
return &obj, nil
}
func MemberFindOne(pk uint64) (Member, error) {
obj := member{}
if err := memberSelectByIdQuery.Get(&obj, pk); err != nil {
return nil, err
}
return &obj, nil
}
func MemberInsertWithTx(tx *sqlx.Tx, contact string, name string, password string, status enums.MemberStatus, companySeq uint64) (uint64, error) {
var (
err error
result sql.Result
pk int64
)
if result, err = tx.Stmtx(memberInsertQuery).Exec(
contact,
name,
password,
status,
companySeq); err != nil {
return 0, err
}
if pk, err = result.LastInsertId(); err != nil {
return 0, err
}
return (uint64)(pk), nil
}
func MemberInsert(contact string, name string, password string, status enums.MemberStatus, companySeq uint64) (uint64, error) {
var (
err error
result sql.Result
pk int64
)
if result, err = memberInsertQuery.Exec(
contact,
name,
password,
status,
companySeq); err != nil {
return 0, err
}
if pk, err = result.LastInsertId(); err != nil {
return 0, err
}
return (uint64)(pk), nil
}

47
subsys/db/util.go Normal file
View File

@ -0,0 +1,47 @@
package db
import (
"database/sql/driver"
"fmt"
"strconv"
)
// NullUint64 represents an uint64 that may be null.
// NullUint64 implements the Scanner interface so
// it can be used as a scan destination, similar to NullString.
// NullUint64 is a sql.Scanner for unsigned ints.
type NullUint64 struct {
Uint64 uint64
Valid bool
}
func (n NullUint64) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Uint64, nil
}
// Scan implements the sql.Scanner interface.
func (n *NullUint64) Scan(src interface{}) error {
if src == nil {
n.Uint64, n.Valid = 0, false
return nil
}
n.Valid = true
s := asString(src)
var err error
n.Uint64, err = strconv.ParseUint(s, 10, 64)
return err
}
func asString(src interface{}) string {
switch v := src.(type) {
case string:
return v
case []byte:
return string(v)
}
return fmt.Sprintf("%v", src)
}

176
subsys/http/iface/metric.go Normal file
View File

@ -0,0 +1,176 @@
package iface
import (
"time"
)
type KeyValuePair struct {
Key string `json:"key" binding:"required"`
Value interface{} `json:"value" binding:"required"`
}
type WrappedResponse struct {
Message string `json:"message"`
Info *PageInfo `json:"info,omitempty"`
Content interface{} `json:"content,omitempty"`
}
type PageInfo struct {
Offset uint32 `json:"offset" binding:"required"`
Size uint16 `json:"size" binding:"required"`
}
type Tuple struct {
Path string
Code int
}
type CounterMetric struct {
Inc chan Tuple `json:"-"`
internalRequestsSum uint64
internalRequests map[string]uint64
internalRequestCodes map[string]uint64
RequestsSum uint64 `json:"request_sum_per_minute"`
Requests map[string]uint64 `json:"requests_per_minute"`
RequestCodes map[string]uint64 `json:"request_codes_per_minute"`
}
type DurationMetricReadable struct {
Min string `json:"min"`
Max string `json:"max"`
Mean string `json:"mean"`
Stdev string `json:"stdev"`
P90 string `json:"p90"`
P95 string `json:"p95"`
P99 string `json:"p99"`
Timestamp time.Time `json:"timestamp"`
}
type DurationMetric struct {
Min uint64 `json:"min"`
Max uint64 `json:"max"`
Mean uint64 `json:"mean"`
Stdev uint64 `json:"stdev"`
P90 uint64 `json:"p90"`
P95 uint64 `json:"p95"`
P99 uint64 `json:"p99"`
Timestamp time.Time `json:"timestamp"`
}
type RequestMetricReadable struct {
Duration DurationMetricReadable `json:"duration"`
Count CounterMetric `json:"count"`
}
type RequestMetric struct {
Duration DurationMetric `json:"duration"`
Count CounterMetric `json:"count"`
}
type RuntimeMetric struct {
GoVersion string `json:"go_version"`
GoOs string `json:"go_os"`
GoArch string `json:"go_arch"`
CpuNum int `json:"cpu_num"`
GoroutineNum int `json:"goroutine_num"`
Gomaxprocs int `json:"go_maxprocs"`
CgoCallNum int64 `json:"cgo_call_num"`
}
type MemoryMetric struct {
MemAllocated uint64 `json:"mem_allocated"`
MemTotal uint64 `json:"mem_total"`
MemSys uint64 `json:"mem_sys"`
Lookups uint64 `json:"lookups"`
MemMallocs uint64 `json:"mem_mallocs"`
MemFrees uint64 `json:"mem_frees"`
HeapAlloc uint64 `json:"heap_alloc"`
HeapSys uint64 `json:"heap_sys"`
HeapIdle uint64 `json:"heap_idle"`
HeapInuse uint64 `json:"heap_inuse"`
HeapReleased uint64 `json:"heap_released"`
HeapObjects uint64 `json:"heap_objects"`
StackInuse uint64 `json:"stack_inuse"`
StackSys uint64 `json:"stack_sys"`
MSpanInuse uint64 `json:"m_span_inuse"`
MSpanSys uint64 `json:"m_span_sys"`
MCacheInuse uint64 `json:"m_cache_inuse"`
MCacheSys uint64 `json:"m_cache_sys"`
BuckHashSys uint64 `json:"buck_hash_sys"`
GCSys uint64 `json:"gc_sys"`
OtherSys uint64 `json:"other_sys"`
NextGC uint64 `json:"next_gc"`
LastGC uint64 `json:"last_gc"`
PauseTotalNs uint64 `json:"pause_total_ns"`
PauseNs uint64 `json:"pause_ns"`
NumGC uint32 `json:"num_gc"`
}
type Metric struct {
Cmdline []string `json:"cmd_line"`
Uptime uint64 `json:"uptime"`
Request RequestMetric `json:"request"`
Runtime RuntimeMetric `json:"runtime"`
Memory MemoryMetric `json:"memory"`
}
type MemoryMetricReadable struct {
MemAllocated string `json:"mem_allocated"`
MemTotal string `json:"mem_total"`
MemSys string `json:"mem_sys"`
Lookups uint64 `json:"lookups"`
MemMallocs uint64 `json:"mem_mallocs"`
MemFrees uint64 `json:"mem_frees"`
HeapAlloc string `json:"heap_alloc"`
HeapSys string `json:"heap_sys"`
HeapIdle string `json:"heap_idle"`
HeapInuse string `json:"heap_inuse"`
HeapReleased string `json:"heap_released"`
HeapObjects uint64 `json:"heap_objects"`
StackInuse string `json:"stack_inuse"`
StackSys string `json:"stack_sys"`
MSpanInuse string `json:"m_span_inuse"`
MSpanSys string `json:"m_span_sys"`
MCacheInuse string `json:"m_cache_inuse"`
MCacheSys string `json:"m_cache_sys"`
BuckHashSys string `json:"buck_hash_sys"`
GCSys string `json:"gc_sys"`
OtherSys string `json:"other_sys"`
NextGC string `json:"next_gc"`
LastGC string `json:"last_gc"`
PauseTotalNs string `json:"pause_total_ns"`
PauseNs string `json:"pause_ns"`
NumGC uint32 `json:"num_gc"`
}
type MetricReadable struct {
Cmdline []string `json:"cmd_line"`
Uptime string `json:"uptime"`
Request RequestMetricReadable `json:"request"`
Runtime RuntimeMetric `json:"runtime"`
Memory MemoryMetricReadable `json:"memory"`
}
// -vars="mem:memory.mem_allocated,duration:request.duration.min,duration:request.duration.max,duration:request.duration.mean,duration:request.duration.stdev,duration:request.duration.p90,duration:request.duration.p95,duration:request.duration.p99,mem:memory.mem_allocated,mem:memory.heap_alloc,mem:memory.heap_inuse,mem:memory.stack_inuse,duration:memory.next_gc,duration:memory.last_gc,duration:memory.pause_total_ns,duration:memory.pause_ns"

View File

@ -0,0 +1,64 @@
package iface
import (
"strconv"
"time"
)
//turns a new initialized CounterMetric object.
func NewCounterMetric() *CounterMetric {
ca := &CounterMetric{}
ca.Inc = make(chan Tuple)
ca.internalRequestsSum = 0
ca.internalRequests = make(map[string]uint64, 0)
ca.internalRequestCodes = make(map[string]uint64, 0)
return ca
}
// StartTimer will call a forever loop in a goroutine to calculate
// metrics for measurements every d ticks. The parameter of this
// function should normally be 1 * time.Minute, if not it will expose
// unintuive JSON keys (requests_per_minute and
// request_sum_per_minute).
func (ca *CounterMetric) StartTimer(d time.Duration) {
timer := time.Tick(d)
go func() {
for {
select {
case tup := <-ca.Inc:
ca.internalRequestsSum++
ca.internalRequests[tup.Path]++
ca.internalRequestCodes[strconv.FormatInt(int64(tup.Code), 10)]++
case <-timer:
ca.reset()
}
}
}()
}
// GetStats to fulfill aspects.Aspect interface, it returns the data
// that will be served as JSON.
func (ca *CounterMetric) GetStats() interface{} {
return *ca
}
// Name to fulfill aspects.Aspect interface, it will return the name
// of the JSON object that will be served.
func (ca *CounterMetric) Name() string {
return "Counter"
}
// InRoot to fulfill aspects.Aspect interface, it will return where to
// put the JSON object into the monitoring endpoint.
func (ca *CounterMetric) InRoot() bool {
return false
}
func (ca *CounterMetric) reset() {
ca.RequestsSum = ca.internalRequestsSum
ca.Requests = ca.internalRequests
ca.RequestCodes = ca.internalRequestCodes
ca.internalRequestsSum = 0
ca.internalRequests = make(map[string]uint64, ca.RequestsSum)
ca.internalRequestCodes = make(map[string]uint64, len(ca.RequestCodes))
}

View File

@ -0,0 +1,118 @@
package iface
import (
"math"
"sort"
"time"
)
type RequestDurationAggr struct {
lastMinuteRequestTimes []float64
Min float64 `json:"min"`
Max float64 `json:"max"`
Mean float64 `json:"mean"`
Stdev float64 `json:"stdev"`
P90 float64 `json:"p90"`
P95 float64 `json:"p95"`
P99 float64 `json:"p99"`
Timestamp time.Time `json:"timestamp"`
}
// NewRequestDurationAggr returns a new initialized RequestDurationAggr
// object.
func NewRequestDurationAggr() *RequestDurationAggr {
rt := &RequestDurationAggr{}
rt.lastMinuteRequestTimes = make([]float64, 0)
rt.Timestamp = time.Now()
return rt
}
// StartTimer will call a forever loop in a goroutine to calculate
// metrics for measurements every d ticks.
func (rt *RequestDurationAggr) StartTimer(d time.Duration) {
timer := time.Tick(d)
go func() {
for {
<-timer
rt.calculate()
}
}()
}
// GetStats to fulfill Metrics.Metric interface, it returns the data
// that will be served as JSON.
func (rt *RequestDurationAggr) GetStats() interface{} {
return rt
}
// Name to fulfill Metrics.Metric interface, it will return the name
// of the JSON object that will be served.
func (rt *RequestDurationAggr) Name() string {
return "RequestTime"
}
// InRoot to fulfill Metrics.Metric interface, it will return where to
// put the JSON object into the monitoring endpoint.
func (rt *RequestDurationAggr) InRoot() bool {
return false
}
func (rt *RequestDurationAggr) Add(n float64) {
rt.lastMinuteRequestTimes = append(rt.lastMinuteRequestTimes, n)
}
func (rt *RequestDurationAggr) calculate() {
sortedSlice := rt.lastMinuteRequestTimes[:]
rt.lastMinuteRequestTimes = make([]float64, 0)
l := len(sortedSlice)
if l <= 1 {
return
}
sort.Float64s(sortedSlice)
rt.Timestamp = time.Now()
rt.Min = sortedSlice[0]
rt.Max = sortedSlice[l - 1]
rt.Mean = mean(sortedSlice, l)
rt.Stdev = correctedStdev(sortedSlice, rt.Mean, l)
rt.P90 = p90(sortedSlice, l)
rt.P95 = p95(sortedSlice, l)
rt.P99 = p99(sortedSlice, l)
}
func mean(orderedObservations []float64, l int) float64 {
res := 0.0
for i := 0; i < l; i++ {
res += orderedObservations[i]
}
return res / float64(l)
}
func p90(orderedObservations []float64, l int) float64 {
return percentile(orderedObservations, l, 0.9)
}
func p95(orderedObservations []float64, l int) float64 {
return percentile(orderedObservations, l, 0.95)
}
func p99(orderedObservations []float64, l int) float64 {
return percentile(orderedObservations, l, 0.99)
}
// percentile with argument p \in (0,1), l is the length of given orderedObservations
// It does a simple apporximation of an ordered list of observations.
// Formula: sortedSlice[0.95*length(sortedSlice)]
func percentile(orderedObservations []float64, l int, p float64) float64 {
return orderedObservations[int(p * float64(l))]
}
func correctedStdev(observations []float64, mean float64, l int) float64 {
var omega float64
for i := 0; i < l; i++ {
omega += math.Pow(observations[i]-mean, 2)
}
stdev := math.Sqrt(1 / (float64(l) - 1) * omega)
return stdev
}

122
subsys/http/init.go Normal file
View File

@ -0,0 +1,122 @@
// +build go1.8
package http
import (
"time"
"net/http"
"amuz.es/gogs/infra/rooibos/subsys/http/middleware/logging"
"amuz.es/gogs/infra/rooibos/subsys/http/middleware/recovery"
"amuz.es/gogs/infra/rooibos/util"
"github.com/gin-gonic/gin"
errs "github.com/pkg/errors"
"amuz.es/gogs/infra/rooibos/subsys/http/route"
"github.com/mkevac/debugcharts"
"log"
"context"
"sync"
)
var (
logger = util.NewLogger("http")
)
func Start(bindAddress string, accessLogConfig *util.LogConfig,
errorSignal chan error, ctx context.Context, waiter *sync.WaitGroup) {
server, err := initialize(bindAddress, accessLogConfig)
if err != nil {
errorSignal <- err
return
}
go bind(server, errorSignal, waiter)
go gracefulCloser(server, ctx, waiter)
logger.Info(bindAddress + " bound")
}
func initialize(bindAddress string, accessLogConfig *util.LogConfig) (server *http.Server, err error) {
defer func() {
if recov := recover(); recov != nil {
err = recov.(error)
}
}()
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
initMiddleware(engine, accessLogConfig)
initRoutes(engine)
logWriter := logger.Writer()
server = &http.Server{
Addr: bindAddress,
Handler: engine,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
ErrorLog: log.New(logWriter, "", 0),
}
return
}
func gracefulCloser(server *http.Server, ctx context.Context, waiter *sync.WaitGroup) {
defer waiter.Done()
<-ctx.Done()
logger.Info("stopping HTTP")
if err := server.Shutdown(context.Background()); err != nil {
logger.Info("HTTP closed:", err)
}
logger.Info("HTTP shutdowned safely.")
}
func bind(server *http.Server, errorSignal chan error, waiter *sync.WaitGroup) {
waiter.Add(1)
if err := server.ListenAndServe(); err != nil {
errorSignal <- errs.Wrap(err, "failed to listen or serve http")
}
}
func initRoutes(engine *gin.Engine) {
suburl := ""
path := engine.Group(suburl)
// handlers
//debug handler
debug := path.Group("/debug")
debug.GET("/metric", route.Metric)
debugcharts.GinDebugRouter(engine)
debug.GET("/pprof/", route.ProfileIndexHandler())
debug.GET("/pprof/heap", route.ProfileHeapHandler())
debug.GET("/pprof/goroutine", route.ProfileGoroutineHandler())
debug.GET("/pprof/block", route.ProfileBlockHandler())
debug.GET("/pprof/threadcreate", route.ProfileThreadCreateHandler())
debug.GET("/pprof/cmdline", route.ProfileCmdlineHandler())
debug.GET("/pprof/profile", route.ProfileProfileHandler())
debug.GET("/pprof/symbol", route.ProfileSymbolHandler())
debug.POST("/pprof/symbol", route.ProfileSymbolHandler())
debug.GET("/pprof/trace", route.ProfileTraceHandler())
path.GET("/favicon.ico", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/static/favicon/images/favicon.ico")
})
path.GET("/manifest.json", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/static/favicon/manifest.json")
})
path.GET("/browserconfig.xml", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/static/favicon/browserconfig.xml")
})
path.GET("/", route.Index)
}
func initMiddleware(engine *gin.Engine, accessLogConfig *util.LogConfig) {
engine.Use(logging.AccessLog(accessLogConfig))
engine.Use(recovery.RecoveryJSON())
// not found handler
engine.NoRoute(recovery.RecoveryHttpError(http.StatusNotFound))
engine.NoMethod(recovery.RecoveryHttpError(http.StatusMethodNotAllowed))
}

View File

@ -0,0 +1,154 @@
// middleware
package logging
import (
"bytes"
"io"
"net"
"net/http"
"strconv"
"time"
"amuz.es/gogs/infra/rooibos/subsys/http/iface"
"amuz.es/gogs/infra/rooibos/util"
"github.com/gin-gonic/gin"
)
const (
dateFormat = "02/Jan/2006:15:04:05 -0700"
)
var DurationAspect = iface.NewRequestDurationAggr()
var CounterAspect = iface.NewCounterMetric()
type logItem struct {
host string
method string
uri string
proto string
referer string
userAgent string
requestedHost string
requestedPath string
status int
responseSize int
requestTime time.Time
duration time.Duration
}
func AccessLog(config *util.LogConfig) gin.HandlerFunc {
_, logWriter := util.NewLogWriter(config)
DurationAspect.StartTimer(10 * time.Second)
CounterAspect.StartTimer(1 * time.Minute)
logChan := make(chan logItem)
go sendLogging(logWriter, logChan)
return func(c *gin.Context) {
now := time.Now()
req := c.Request
defer func() {
dur := time.Now().Sub(now)
logItemData := logItem{
status: c.Writer.Status(),
responseSize: c.Writer.Size(),
requestTime: now,
duration: dur,
}
if req != nil {
logItemData.host = remoteHost(req)
logItemData.method = req.Method
logItemData.uri = req.RequestURI
logItemData.proto = req.Proto
logItemData.referer = req.Referer()
logItemData.userAgent = req.UserAgent()
logItemData.requestedHost = req.Host
logItemData.requestedPath = req.URL.Path
} else {
logItemData.host = "-"
logItemData.method = ""
logItemData.uri = ""
logItemData.proto = ""
logItemData.referer = ""
logItemData.userAgent = ""
logItemData.requestedHost = ""
logItemData.requestedPath = ""
}
logChan <- logItemData
}()
c.Next()
}
}
func sendLogging(accesslog io.Writer, logChan chan logItem) {
for {
select {
case logItemData := <-logChan:
DurationAspect.Add(float64(logItemData.duration.Nanoseconds()))
CounterAspect.Inc <- iface.Tuple{
Path: logItemData.requestedPath,
Code: logItemData.status,
}
// Logs an access event in Apache combined log format (with a minor customization with the duration).
buf := bytes.NewBufferString("")
buf.WriteString(logItemData.host)
buf.WriteString(` - - [`)
buf.WriteString(logItemData.requestTime.Format(dateFormat))
buf.WriteString(`] "`)
buf.WriteString(logItemData.method)
buf.WriteByte(' ')
buf.WriteString(logItemData.uri)
buf.WriteByte(' ')
buf.WriteString(logItemData.proto)
buf.WriteString(`" `)
buf.WriteString(strconv.Itoa(logItemData.status))
buf.WriteByte(' ')
buf.WriteString(strconv.Itoa(logItemData.responseSize))
buf.WriteString(` "`)
buf.WriteString(logItemData.referer)
buf.WriteString(`" "`)
buf.WriteString(logItemData.userAgent)
buf.WriteString(`" `)
buf.WriteString(strconv.FormatInt((logItemData.duration.Nanoseconds() / time.Millisecond.Nanoseconds()), 10))
buf.WriteByte(' ')
buf.WriteString(logItemData.requestedHost)
buf.WriteByte('\n')
accesslog.Write(buf.Bytes())
}
}
}
// strip port from addresses with hostname, ipv4 or ipv6
func stripPort(address string) string {
if h, _, err := net.SplitHostPort(address); err == nil {
return h
}
return address
}
// The remote address of the client. When the 'X-Forwarded-For'
// header is set, then it is used instead.
func remoteAddr(r *http.Request) string {
ff := r.Header.Get("X-Forwarded-For")
if ff != "" {
return ff
}
return r.RemoteAddr
}
func remoteHost(r *http.Request) string {
a := remoteAddr(r)
h := stripPort(a)
if h != "" {
return h
}
return "-"
}

View File

@ -0,0 +1,143 @@
package recovery
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httputil"
"runtime"
"strings"
"amuz.es/gogs/infra/rooibos/subsys/http/iface"
"amuz.es/gogs/infra/rooibos/util"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
var (
dunno = []byte("???")
centerDot = []byte("·")
dot = []byte(".")
slash = []byte("/")
logger = util.NewLogger("recovery")
color = "danger"
preText = "루이보스 API 오류 :bomb:"
title = "API 요청을 처리하는 중에 의도하지 않은 오류가 발행하였습니다."
footer = "확인해 주세요! :hugging_face:"
)
func recoveryInternal(stack []byte, code int, req *http.Request, err *string) {
httprequest, _ := httputil.DumpRequest(req, false)
detailInfo := map[string]interface{}{
"method": req.Method,
"statusCode": code,
"path": req.URL.Path,
"message": err,
"request": string(httprequest),
"stack": string(stack),
}
logger.WithFields(logrus.Fields(detailInfo)).Error("[Recovery] panic recovered:\n")
}
func recoveryError(code int, _ *gin.Context) *iface.WrappedResponse {
return &iface.WrappedResponse{
Message: http.StatusText(code),
}
}
func RecoveryHttpError(code int) gin.HandlerFunc {
return func(c *gin.Context) {
info := recoveryError(code, c)
util.DumpJSON(c, code, info)
}
}
func RecoveryJSON() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
code := http.StatusInternalServerError
stack := stack(5)
systemErrMessage := err.(error).Error()
go recoveryInternal(stack, code, c.Copy().Request, &systemErrMessage)
util.DumpJSON(c, code, &iface.WrappedResponse{
Message: systemErrMessage,
})
c.Abort()
}
}()
c.Next()
}
}
// stack returns a nicely formated stack frame, skipping skip frames
func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data
// As we loop, we open files and read them. These variables record the currently
// loaded file.
var lines [][]byte
var lastFile string
for i := skip; ; i++ {
// Skip the expected number of frames
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
if paths := strings.SplitN(file, "src/", 2); len(paths) == 1 {
// Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
} else if vendors := strings.SplitN(paths[1], "vendor/", 2); len(vendors) == 1 {
// Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", paths[1], line, pc)
} else {
// Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", vendors[1], line, pc)
}
if file != lastFile {
data, err := ioutil.ReadFile(file)
if err != nil {
continue
}
lines = bytes.Split(data, []byte{'\n'})
lastFile = file
}
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
}
return buf.Bytes()
}
// source returns a space-trimmed slice of the n'th line.
func source(lines [][]byte, n int) []byte {
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
if n < 0 || n >= len(lines) {
return dunno
}
return bytes.TrimSpace(lines[n])
}
// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
fn := runtime.FuncForPC(pc)
if fn == nil {
return dunno
}
name := []byte(fn.Name())
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
// runtime/debug.*T·ptrmethod
// and want
// *T.ptrmethod
// Also the package path might contains dot (e.g. code.google.com/...),
// so first eliminate the path prefix
if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 {
name = name[lastslash+1:]
}
if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:]
}
name = bytes.Replace(name, centerDot, dot, -1)
return name
}

View File

@ -0,0 +1,56 @@
package route
import (
"net/http"
"amuz.es/gogs/infra/rooibos/subsys/http/iface"
"amuz.es/gogs/infra/rooibos/util"
common_iface "amuz.es/gogs/infra/rooibos/util/iface"
"github.com/gin-gonic/gin"
"amuz.es/gogs/infra/rooibos/subsys/db"
"time"
"amuz.es/gogs/infra/rooibos/enums"
)
func Index(c *gin.Context) {
uuidTest,_ :=common_iface.NewUUIDV4()
resp := &iface.WrappedResponse{
Message: "Pong!",
Content: []iface.KeyValuePair{
{
Key: "name",
Value: util.Config.Name(),
},
{
Key: "version",
Value: util.Config.Version(),
},
{
Key: "buildDate",
Value: util.Config.BuildDate(),
},
{
Key: "phase",
Value: util.Config.Phase,
},
{
Key:"uuid",
Value: uuidTest,
},
{
Key: "model",
Value: db.NewMember(
uint64(0),
time.Now(),
nil,
"hello",
"name",
"hello",
enums.MemberStatusEnabled,
uint64(0),
),
},
},
}
util.DumpJSON(c, http.StatusOK, resp)
}

151
subsys/http/route/metric.go Normal file
View File

@ -0,0 +1,151 @@
package route
import (
"os"
"runtime"
"time"
"amuz.es/gogs/infra/rooibos/subsys/http/iface"
"amuz.es/gogs/infra/rooibos/subsys/http/middleware/logging"
"amuz.es/gogs/infra/rooibos/util"
"github.com/gin-gonic/gin"
)
var (
startTime = time.Now()
)
func Metric(c *gin.Context) {
_, readable := c.Request.URL.Query()["readable"]
if readable {
memory := serializeReadableStat()
util.DumpJSON(c, 200, memory)
} else {
memory := serializeMemoryStat()
util.DumpJSON(c, 200, memory)
}
}
func serializeMemoryStat() *iface.Metric {
m := new(runtime.MemStats)
runtime.ReadMemStats(m)
return &iface.Metric{
Cmdline: os.Args,
Uptime: uint64(time.Now().UnixNano() - startTime.UnixNano()),
Request:iface.RequestMetric{
Duration:iface.DurationMetric{
Min: uint64(logging.DurationAspect.Min),
Max: uint64(logging.DurationAspect.Max),
Mean: uint64(logging.DurationAspect.Mean),
Stdev: uint64(logging.DurationAspect.Stdev),
P90: uint64(logging.DurationAspect.P90),
P95: uint64(logging.DurationAspect.P95),
P99: uint64(logging.DurationAspect.P99),
Timestamp: logging.DurationAspect.Timestamp,
},
Count:*logging.CounterAspect,
},
Runtime:iface.RuntimeMetric{
GoVersion: runtime.Version(),
GoOs: runtime.GOOS,
GoArch: runtime.GOARCH,
CpuNum: runtime.NumCPU(),
GoroutineNum: runtime.NumGoroutine(),
Gomaxprocs: runtime.GOMAXPROCS(0),
CgoCallNum: runtime.NumCgoCall(),
},
Memory:iface.MemoryMetric{
MemAllocated: m.Alloc,
MemTotal: m.TotalAlloc,
MemSys: m.Sys,
Lookups: m.Lookups,
MemMallocs: m.Mallocs,
MemFrees: m.Frees,
HeapAlloc: m.HeapAlloc,
HeapSys: m.HeapSys,
HeapIdle: m.HeapIdle,
HeapInuse: m.HeapInuse,
HeapReleased: m.HeapReleased,
HeapObjects: m.HeapObjects,
StackInuse: m.StackInuse,
StackSys: m.StackSys,
MSpanInuse: m.MSpanInuse,
MSpanSys: m.MSpanSys,
MCacheInuse: m.MCacheInuse,
MCacheSys: m.MCacheSys,
BuckHashSys: m.BuckHashSys,
GCSys: m.GCSys,
OtherSys: m.OtherSys,
NextGC: m.NextGC,
LastGC: uint64(time.Now().UnixNano()) - m.LastGC,
PauseTotalNs: m.PauseTotalNs,
PauseNs: m.PauseNs[(m.NumGC + 255) % 256],
NumGC: m.NumGC,
},
}
}
//
func serializeReadableStat() *iface.MetricReadable {
m := new(runtime.MemStats)
runtime.ReadMemStats(m)
return &iface.MetricReadable{
Uptime: util.TimeSincePro(startTime),
Request:iface.RequestMetricReadable{
Duration:iface.DurationMetricReadable{
Min: util.TimeSinceDuration(uint64(logging.DurationAspect.Min)),
Max: util.TimeSinceDuration(uint64(logging.DurationAspect.Max)),
Mean: util.TimeSinceDuration(uint64(logging.DurationAspect.Mean)),
Stdev: util.TimeSinceDuration(uint64(logging.DurationAspect.Stdev)),
P90: util.TimeSinceDuration(uint64(logging.DurationAspect.P90)),
P95: util.TimeSinceDuration(uint64(logging.DurationAspect.P95)),
P99: util.TimeSinceDuration(uint64(logging.DurationAspect.P99)),
Timestamp: logging.DurationAspect.Timestamp,
},
Count:*logging.CounterAspect,
},
Runtime:iface.RuntimeMetric{
GoVersion: runtime.Version(),
GoOs: runtime.GOOS,
GoArch: runtime.GOARCH,
CpuNum: runtime.NumCPU(),
GoroutineNum: runtime.NumGoroutine(),
Gomaxprocs: runtime.GOMAXPROCS(0),
CgoCallNum: runtime.NumCgoCall(),
},
Memory:iface.MemoryMetricReadable{
MemAllocated: util.FileSize(m.Alloc),
MemTotal: util.FileSize(m.TotalAlloc),
MemSys: util.FileSize(m.Sys),
Lookups: m.Lookups,
MemMallocs: m.Mallocs,
MemFrees: m.Frees,
HeapAlloc: util.FileSize(m.HeapAlloc),
HeapSys: util.FileSize(m.HeapSys),
HeapIdle: util.FileSize(m.HeapIdle),
HeapInuse: util.FileSize(m.HeapInuse),
HeapReleased: util.FileSize(m.HeapReleased),
HeapObjects: m.HeapObjects,
StackInuse: util.FileSize(m.StackInuse),
StackSys: util.FileSize(m.StackSys),
MSpanInuse: util.FileSize(m.MSpanInuse),
MSpanSys: util.FileSize(m.MSpanSys),
MCacheInuse: util.FileSize(m.MCacheInuse),
MCacheSys: util.FileSize(m.MCacheSys),
BuckHashSys: util.FileSize(m.BuckHashSys),
GCSys: util.FileSize(m.GCSys),
OtherSys: util.FileSize(m.OtherSys),
NextGC: util.FileSize(m.NextGC),
LastGC: util.TimeSinceDuration(uint64(time.Now().UnixNano()) - m.LastGC) + " ago",
PauseTotalNs: util.TimeSinceDuration(m.PauseTotalNs),
PauseNs: util.TimeSinceDuration(m.PauseNs[(m.NumGC + 255) % 256]),
NumGC: m.NumGC,
},
}
}

View File

@ -0,0 +1,69 @@
package route
import (
"net/http/pprof"
"github.com/gin-gonic/gin"
)
// IndexHandler will pass the call from /debug/pprof to pprof
func ProfileIndexHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Index(ctx.Writer, ctx.Request)
}
}
// HeapHandler will pass the call from /debug/pprof/heap to pprof
func ProfileHeapHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Handler("heap").ServeHTTP(ctx.Writer, ctx.Request)
}
}
// GoroutineHandler will pass the call from /debug/pprof/goroutine to pprof
func ProfileGoroutineHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Handler("goroutine").ServeHTTP(ctx.Writer, ctx.Request)
}
}
// BlockHandler will pass the call from /debug/pprof/block to pprof
func ProfileBlockHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Handler("block").ServeHTTP(ctx.Writer, ctx.Request)
}
}
// ThreadCreateHandler will pass the call from /debug/pprof/threadcreate to pprof
func ProfileThreadCreateHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Handler("threadcreate").ServeHTTP(ctx.Writer, ctx.Request)
}
}
// CmdlineHandler will pass the call from /debug/pprof/cmdline to pprof
func ProfileCmdlineHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Cmdline(ctx.Writer, ctx.Request)
}
}
// ProfileHandler will pass the call from /debug/pprof/profile to pprof
func ProfileProfileHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Profile(ctx.Writer, ctx.Request)
}
}
// SymbolHandler will pass the call from /debug/pprof/symbol to pprof
func ProfileSymbolHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Symbol(ctx.Writer, ctx.Request)
}
}
// TraceHandler will pass the call from /debug/pprof/trace to pprof
func ProfileTraceHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
pprof.Trace(ctx.Writer, ctx.Request)
}
}

View File

@ -0,0 +1,27 @@
package route
import (
"net/http"
"time"
"amuz.es/gogs/infra/rooibos/util"
)
var logger = util.NewLogger("route")
func extractLastModified(hh http.Header) (modified *time.Time) {
if modifiedStr := extractHeader("If-Modified-Since", hh); modifiedStr == nil {
} else if modifiedTime, err := http.ParseTime(*modifiedStr); err != nil {
} else {
modified = &modifiedTime
}
return
}
func extractHeader(key string, hh http.Header) (ret *string) {
if hdrStr := hh.Get(key); hdrStr == "" {
} else {
ret = &hdrStr
}
return
}

110
subsys/periodic/init.go Normal file
View File

@ -0,0 +1,110 @@
package periodic
import (
"time"
cronLib "github.com/robfig/cron"
"sync"
"sync/atomic"
"amuz.es/gogs/infra/rooibos/util"
"context"
)
var (
logger = util.NewLogger("periodic")
runningJobGroup = sync.WaitGroup{}
runningJobCount = int32(0)
)
type Job struct {
started time.Time
Name string
Execution func()
isRunning int32
}
func (j *Job) Run() {
if ok := atomic.CompareAndSwapInt32(&j.isRunning, 0, -1); !ok {
return
}
// ENTER CRITICAL SECTION
j.started = time.Now()
runningJobGroup.Add(1)
atomic.AddInt32(&runningJobCount, 1)
defer func() {
// EXIT CRITICAL SECTION
atomic.AddInt32(&runningJobCount, -1)
runningJobGroup.Done()
atomic.StoreInt32(&j.isRunning, 0)
logger.Debugf("Job %s Stopped in %s", j.Name, time.Now().Sub(j.started))
}()
j.Execution()
}
func Start(errorSignal chan error, ctx context.Context, waiter *sync.WaitGroup) {
cron, err := initialize(waiter)
if err != nil {
errorSignal <- err
return
}
go gracefulCloser(cron, ctx, waiter)
logger.Info("scheduler started")
}
func initialize(waiter *sync.WaitGroup) (cron *cronLib.Cron, err error) {
defer func() {
if recov := recover(); recov != nil {
err = recov.(error)
}
}()
cron = cronLib.New()
waiter.Add(1)
logger.Debug("starting scheduler")
defer cron.Start()
return
}
func gracefulCloser(cron *cronLib.Cron, ctx context.Context, waiter *sync.WaitGroup) {
<-ctx.Done()
defer waiter.Done()
logger.Info("stopping scheduler")
cron.Stop()
waitChannel := make(chan struct{}, 1)
go func() {
runningJobGroup.Wait()
close(waitChannel)
}()
warningTicker := time.Tick(1 * time.Second)
for {
select {
case <-waitChannel:
logger.Info("Periodic Jobs shutdowned safely.")
return
case <-warningTicker:
logger.Infof(
"Waiting Periodic Jobs (remains %d job(s))",
runningJobCount,
)
}
}
}
func initJobs(closeSignal chan struct{}) {
// cron.AddJob("* * * * * *", &Job{
// Name: "job 1",
// Execution: func() {
// logger.Debugf("job 1 (executing %d job(s))", runningJobCount)
// select {
// case <-closeSignal:
// logger.Debugf("job 1 quit signal received!")
// case <-time.After(time.Millisecond * 2000):
// }
// },
// })
// todo upload 실패한건, 오래된건 관리.
}

View File

@ -0,0 +1,458 @@
// Code generated by protoc-gen-gogo. DO NOT EDIT.
// source: subsys/redis/app_token_data.proto
/*
Package redis is a generated protocol buffer package.
It is generated from these files:
subsys/redis/app_token_data.proto
It has these top-level messages:
TokenData
*/
package redis
import proto "github.com/gogo/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
import _ "github.com/golang/protobuf/ptypes/timestamp"
import amuz_es_gogs_infra_rooibos_util_iface "amuz.es/gogs/infra/rooibos/util/iface"
import time "time"
import github_com_gogo_protobuf_types "github.com/gogo/protobuf/types"
import io "io"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
var _ = time.Kitchen
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
type TokenData struct {
ClientId amuz_es_gogs_infra_rooibos_util_iface.UUID `protobuf:"bytes,1,opt,name=ClientId,proto3,customtype=amuz.es/gogs/infra/rooibos/util/iface.UUID" json:"client_id"`
RefreshToken amuz_es_gogs_infra_rooibos_util_iface.UUID `protobuf:"bytes,2,opt,name=RefreshToken,proto3,customtype=amuz.es/gogs/infra/rooibos/util/iface.UUID" json:"refresh_token"`
MemberId uint64 `protobuf:"fixed64,3,opt,name=MemberId,proto3" json:"member_id"`
CreatedAt time.Time `protobuf:"bytes,4,opt,name=CreatedAt,stdtime" json:"created_at"`
}
func (m *TokenData) Reset() { *m = TokenData{} }
func (m *TokenData) String() string { return proto.CompactTextString(m) }
func (*TokenData) ProtoMessage() {}
func (*TokenData) Descriptor() ([]byte, []int) { return fileDescriptorAppTokenData, []int{0} }
func (m *TokenData) GetMemberId() uint64 {
if m != nil {
return m.MemberId
}
return 0
}
func (m *TokenData) GetCreatedAt() time.Time {
if m != nil {
return m.CreatedAt
}
return time.Time{}
}
func init() {
proto.RegisterType((*TokenData)(nil), "redis.TokenData")
}
func (m *TokenData) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *TokenData) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
dAtA[i] = 0xa
i++
i = encodeVarintAppTokenData(dAtA, i, uint64(m.ClientId.Size()))
n1, err := m.ClientId.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n1
dAtA[i] = 0x12
i++
i = encodeVarintAppTokenData(dAtA, i, uint64(m.RefreshToken.Size()))
n2, err := m.RefreshToken.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n2
if m.MemberId != 0 {
dAtA[i] = 0x19
i++
i = encodeFixed64AppTokenData(dAtA, i, uint64(m.MemberId))
}
dAtA[i] = 0x22
i++
i = encodeVarintAppTokenData(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.CreatedAt)))
n3, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.CreatedAt, dAtA[i:])
if err != nil {
return 0, err
}
i += n3
return i, nil
}
func encodeFixed64AppTokenData(dAtA []byte, offset int, v uint64) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
dAtA[offset+4] = uint8(v >> 32)
dAtA[offset+5] = uint8(v >> 40)
dAtA[offset+6] = uint8(v >> 48)
dAtA[offset+7] = uint8(v >> 56)
return offset + 8
}
func encodeFixed32AppTokenData(dAtA []byte, offset int, v uint32) int {
dAtA[offset] = uint8(v)
dAtA[offset+1] = uint8(v >> 8)
dAtA[offset+2] = uint8(v >> 16)
dAtA[offset+3] = uint8(v >> 24)
return offset + 4
}
func encodeVarintAppTokenData(dAtA []byte, offset int, v uint64) int {
for v >= 1<<7 {
dAtA[offset] = uint8(v&0x7f | 0x80)
v >>= 7
offset++
}
dAtA[offset] = uint8(v)
return offset + 1
}
func (m *TokenData) Size() (n int) {
var l int
_ = l
l = m.ClientId.Size()
n += 1 + l + sovAppTokenData(uint64(l))
l = m.RefreshToken.Size()
n += 1 + l + sovAppTokenData(uint64(l))
if m.MemberId != 0 {
n += 9
}
l = github_com_gogo_protobuf_types.SizeOfStdTime(m.CreatedAt)
n += 1 + l + sovAppTokenData(uint64(l))
return n
}
func sovAppTokenData(x uint64) (n int) {
for {
n++
x >>= 7
if x == 0 {
break
}
}
return n
}
func sozAppTokenData(x uint64) (n int) {
return sovAppTokenData(uint64((x << 1) ^ uint64((int64(x) >> 63))))
}
func (m *TokenData) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: TokenData: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: TokenData: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ClientId", wireType)
}
var byteLen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
byteLen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if byteLen < 0 {
return ErrInvalidLengthAppTokenData
}
postIndex := iNdEx + byteLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := m.ClientId.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field RefreshToken", wireType)
}
var byteLen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
byteLen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if byteLen < 0 {
return ErrInvalidLengthAppTokenData
}
postIndex := iNdEx + byteLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := m.RefreshToken.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 3:
if wireType != 1 {
return fmt.Errorf("proto: wrong wireType = %d for field MemberId", wireType)
}
m.MemberId = 0
if (iNdEx + 8) > l {
return io.ErrUnexpectedEOF
}
iNdEx += 8
m.MemberId = uint64(dAtA[iNdEx-8])
m.MemberId |= uint64(dAtA[iNdEx-7]) << 8
m.MemberId |= uint64(dAtA[iNdEx-6]) << 16
m.MemberId |= uint64(dAtA[iNdEx-5]) << 24
m.MemberId |= uint64(dAtA[iNdEx-4]) << 32
m.MemberId |= uint64(dAtA[iNdEx-3]) << 40
m.MemberId |= uint64(dAtA[iNdEx-2]) << 48
m.MemberId |= uint64(dAtA[iNdEx-1]) << 56
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field CreatedAt", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthAppTokenData
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.CreatedAt, dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipAppTokenData(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthAppTokenData
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func skipAppTokenData(dAtA []byte) (n int, err error) {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
wireType := int(wire & 0x7)
switch wireType {
case 0:
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
iNdEx++
if dAtA[iNdEx-1] < 0x80 {
break
}
}
return iNdEx, nil
case 1:
iNdEx += 8
return iNdEx, nil
case 2:
var length int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
length |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
iNdEx += length
if length < 0 {
return 0, ErrInvalidLengthAppTokenData
}
return iNdEx, nil
case 3:
for {
var innerWire uint64
var start int = iNdEx
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return 0, ErrIntOverflowAppTokenData
}
if iNdEx >= l {
return 0, io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
innerWire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
innerWireType := int(innerWire & 0x7)
if innerWireType == 4 {
break
}
next, err := skipAppTokenData(dAtA[start:])
if err != nil {
return 0, err
}
iNdEx = start + next
}
return iNdEx, nil
case 4:
return iNdEx, nil
case 5:
iNdEx += 4
return iNdEx, nil
default:
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
}
}
panic("unreachable")
}
var (
ErrInvalidLengthAppTokenData = fmt.Errorf("proto: negative length found during unmarshaling")
ErrIntOverflowAppTokenData = fmt.Errorf("proto: integer overflow")
)
func init() { proto.RegisterFile("subsys/redis/app_token_data.proto", fileDescriptorAppTokenData) }
var fileDescriptorAppTokenData = []byte{
// 336 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x90, 0xb1, 0x4e, 0xf3, 0x30,
0x14, 0x85, 0xeb, 0xfe, 0x3f, 0x55, 0x6b, 0x5a, 0x09, 0x65, 0x40, 0x51, 0x87, 0xa4, 0x30, 0x15,
0x24, 0x6c, 0x09, 0x1e, 0x00, 0x91, 0x76, 0xe9, 0x80, 0x84, 0xa2, 0x76, 0x62, 0x88, 0x9c, 0xe4,
0x26, 0xb5, 0x68, 0xea, 0xc8, 0x76, 0x06, 0x78, 0x8a, 0x3e, 0x56, 0x27, 0xc4, 0xcc, 0x10, 0x50,
0xd9, 0xfa, 0x14, 0x28, 0x8e, 0x42, 0xc5, 0xca, 0xe6, 0xeb, 0x73, 0xef, 0x77, 0x8e, 0x0e, 0x3e,
0x53, 0x45, 0xa8, 0x9e, 0x15, 0x95, 0x10, 0x73, 0x45, 0x59, 0x9e, 0x07, 0x5a, 0x3c, 0xc1, 0x3a,
0x88, 0x99, 0x66, 0x24, 0x97, 0x42, 0x0b, 0xeb, 0xc8, 0x68, 0xc3, 0xab, 0x94, 0xeb, 0x65, 0x11,
0x92, 0x48, 0x64, 0x34, 0x15, 0xa9, 0xa0, 0x46, 0x0d, 0x8b, 0xc4, 0x4c, 0x66, 0x30, 0xaf, 0xfa,
0x6a, 0xe8, 0xa6, 0x42, 0xa4, 0x2b, 0x38, 0x6c, 0x69, 0x9e, 0x81, 0xd2, 0x2c, 0xcb, 0xeb, 0x85,
0xf3, 0xd7, 0x36, 0xee, 0xcd, 0x2b, 0xaf, 0x29, 0xd3, 0xcc, 0x7a, 0xc4, 0xdd, 0xc9, 0x8a, 0xc3,
0x5a, 0xcf, 0x62, 0x1b, 0x8d, 0xd0, 0xb8, 0xef, 0xdd, 0x6e, 0x4b, 0xb7, 0xf5, 0x5e, 0xba, 0x97,
0x2c, 0x2b, 0x5e, 0x08, 0xa8, 0x0a, 0xae, 0x28, 0x5f, 0x27, 0x92, 0x51, 0x29, 0x04, 0x0f, 0x85,
0xa2, 0x85, 0xe6, 0x2b, 0xca, 0x13, 0x16, 0x01, 0x59, 0x2c, 0x66, 0xd3, 0x7d, 0xe9, 0xf6, 0x22,
0x43, 0x09, 0x78, 0xec, 0xff, 0x00, 0xad, 0x14, 0xf7, 0x7d, 0x48, 0x24, 0xa8, 0xa5, 0x31, 0xb4,
0xdb, 0xc6, 0x60, 0xf2, 0x27, 0x83, 0x81, 0xac, 0x49, 0x75, 0x4f, 0xfe, 0x2f, 0xb0, 0x75, 0x81,
0xbb, 0xf7, 0x90, 0x85, 0x20, 0x67, 0xb1, 0xfd, 0x6f, 0x84, 0xc6, 0x1d, 0x6f, 0x50, 0x65, 0xca,
0xcc, 0x9f, 0xc9, 0xd4, 0xc8, 0xd6, 0x03, 0xee, 0x4d, 0x24, 0x30, 0x0d, 0xf1, 0x9d, 0xb6, 0xff,
0x8f, 0xd0, 0xf8, 0xf8, 0x7a, 0x48, 0xea, 0xce, 0x48, 0xd3, 0x19, 0x99, 0x37, 0x9d, 0x79, 0xa7,
0x55, 0xd8, 0x7d, 0xe9, 0xe2, 0xa8, 0x3e, 0x0a, 0x98, 0xde, 0x7c, 0xb8, 0xc8, 0x3f, 0x40, 0xbc,
0x93, 0xed, 0xce, 0x41, 0x6f, 0x3b, 0x07, 0x7d, 0xee, 0x1c, 0xb4, 0xf9, 0x72, 0x5a, 0x61, 0xc7,
0x80, 0x6e, 0xbe, 0x03, 0x00, 0x00, 0xff, 0xff, 0x88, 0x6c, 0x3e, 0xdb, 0xe5, 0x01, 0x00, 0x00,
}

View File

@ -0,0 +1,27 @@
syntax = "proto3";
package redis;
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";
message TokenData {
bytes ClientId =1 [
(gogoproto.customtype) = "amuz.es/gogs/infra/rooibos/util/iface.UUID",
(gogoproto.jsontag) = "client_id",
(gogoproto.nullable) = false
];
bytes RefreshToken =2 [
(gogoproto.customtype) = "amuz.es/gogs/infra/rooibos/util/iface.UUID",
(gogoproto.jsontag) = "refresh_token",
(gogoproto.nullable) = false
];
fixed64 MemberId =3 [
(gogoproto.jsontag) = "member_id"
];
google.protobuf.Timestamp CreatedAt =4 [
(gogoproto.jsontag) = "created_at",
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false
];
}

View File

@ -0,0 +1,275 @@
package redis
import (
"errors"
"fmt"
"strconv"
"amuz.es/gogs/infra/rooibos/util/iface"
"github.com/garyburd/redigo/redis"
)
var (
tokenStoreBaseKey []byte
refreshTokenStoreBaseKey []byte
sessionStoreBaseKey []byte
tokenExpireTime int64
refreshTokenExpireTime int64
)
func AppTokenInitService(phase string, tokenExpireTimeData, refreshTokenExpireTimeData int64) {
tokenStoreBaseKey = []byte(fmt.Sprint(phase, "/RB", "/TK/"))
refreshTokenStoreBaseKey = []byte(fmt.Sprint(phase, "/RB", "/RK/"))
sessionStoreBaseKey = []byte(fmt.Sprint(phase, "/RB", "/SE/"))
tokenExpireTime = tokenExpireTimeData / 1000
refreshTokenExpireTime = refreshTokenExpireTimeData / 1000
}
func AppTokenCreateSession(clientId iface.UUID, memberId uint64) (out *SessionData, err error) {
var client resourceConn
if client, err = getConn(); err != nil {
return
}
defer putConn(client)
var (
refreshToken, _ = iface.NewUUIDV4()
token, _ = iface.NewUUIDV4()
tokenStoreKey = append(tokenStoreBaseKey, token.String()...)
refreshTokenStoreKey = append(refreshTokenStoreBaseKey, refreshToken.String()...)
sessionStoreKey = strconv.AppendUint(sessionStoreBaseKey, memberId, 10)
refreshTokenData []byte
tokenData []byte
sessionData []byte
newSession = &SessionData{
Token: token,
RefreshToken: refreshToken,
}
)
if refreshTokenData, err = (&RefreshTokenData{
ClientId: clientId,
MemberId: memberId,
}).Marshal(); err != nil {
return
}
if tokenData, err = (&TokenData{
ClientId: clientId,
MemberId: memberId,
RefreshToken: refreshToken,
}).Marshal(); err != nil {
return
}
if sessionData, err = newSession.Marshal(); err != nil {
return
}
var prevSession = new(SessionData)
var tx = func() (err error) {
if _, err = client.Do("WATCH", sessionStoreKey); err != nil {
return
}
if fetchErr := extractDataFromRedis(prevSession, sessionStoreKey, client); fetchErr != nil {
prevSession = nil
}
client.Send("MULTI")
if prevSession != nil {
previousTokenStoreKey := append(tokenStoreBaseKey, prevSession.Token.String()...)
previousRefreshTokenStoreKey := append(refreshTokenStoreBaseKey, prevSession.RefreshToken.String()...)
client.Send("DEL", previousTokenStoreKey, previousRefreshTokenStoreKey)
}
client.Send("SETEX", tokenStoreKey, tokenExpireTime, tokenData)
client.Send("SETEX", refreshTokenStoreKey, refreshTokenExpireTime, refreshTokenData)
client.Send("SETEX", sessionStoreKey, refreshTokenExpireTime, sessionData)
if _, err = redis.Values(client.Do("EXEC")); err != nil {
return newTxError(err)
}
out = newSession
return
}
err = retries(tx)
return
}
func AppTokenFindMemberIdByToken(clientId iface.UUID, token iface.UUID) (memberId uint64, err error) {
var client resourceConn
if client, err = getConn(); err != nil {
return
}
defer putConn(client)
var (
tokenStoreKey = append(tokenStoreBaseKey, token.String()...)
matchedTokenData = new(TokenData)
)
if err = extractDataFromRedis(matchedTokenData, tokenStoreKey, client); err != nil {
return
}
if clientId.Equal(matchedTokenData.ClientId) {
memberId = matchedTokenData.MemberId
} else {
client.Send("DEL", tokenStoreKey)
err = errors.New("matched token is invalid")
}
return
}
func AppTokenFindMemberIdByRefreshToken(clientId iface.UUID, refreshToken iface.UUID) (memberId uint64, err error) {
var client resourceConn
if client, err = getConn(); err != nil {
return
}
defer putConn(client)
var (
refreshTokenStoreKey = append(refreshTokenStoreBaseKey, refreshToken.String()...)
matchedRefreshTokenData = new(RefreshTokenData)
)
if err = extractDataFromRedis(matchedRefreshTokenData, refreshTokenStoreKey, client); err != nil {
return
}
if clientId.Equal(matchedRefreshTokenData.ClientId) {
memberId = matchedRefreshTokenData.MemberId
} else {
sessionStoreKey := strconv.AppendUint(sessionStoreBaseKey, matchedRefreshTokenData.MemberId, 10)
sessionData := new(SessionData) //newSessionData
if errFetchSession := extractDataFromRedis(sessionData, sessionStoreKey, client); errFetchSession != nil {
logger.Info("failed to AppTokenFindMemberIdByRefreshToken", errFetchSession)
client.Send("DEL", refreshTokenStoreKey, sessionStoreKey)
} else {
tokenStoreKey := append(tokenStoreBaseKey, sessionData.Token.String()...)
client.Send("DEL", refreshTokenStoreKey, sessionStoreKey, tokenStoreKey)
}
err = errors.New("matched token is invalid")
}
return
}
func AppTokenRefreshSessionByRefreshToken(clientId iface.UUID, refreshToken iface.UUID) (out *SessionData, err error) {
var client resourceConn
if client, err = getConn(); err != nil {
return
}
defer putConn(client)
var (
refreshTokenStoreKey = append(refreshTokenStoreBaseKey, refreshToken.String()...)
prevRefreshTokenData = new(RefreshTokenData) //refreshTokenData
prevSessionData = new(SessionData) //previousSessionData
prevTokenData = new(TokenData) //previousTokenData
memberId uint64
sessionStoreKey []byte
prevTokenStoreKey []byte
prevAnotherRefreshTokenStoreKey []byte
)
if fetchErr := extractDataFromRedis(prevRefreshTokenData, refreshTokenStoreKey, client); fetchErr != nil {
err = errors.New("matched refreshToken not found")
return
} else {
memberId = prevRefreshTokenData.MemberId
sessionStoreKey = strconv.AppendUint(sessionStoreBaseKey, memberId, 10)
}
var tx = func() (err error) {
if _, err = client.Do("WATCH", sessionStoreKey); err != nil {
return
}
if err = extractDataFromRedis(prevSessionData, sessionStoreKey, client); err != nil {
client.Send("MULTI")
client.Send("DEL", refreshTokenStoreKey)
if _, err = redis.Values(client.Do("EXEC")); err != nil {
logger.Info("failed to AppTokenRefreshSessionByRefreshToken", err)
}
err = errors.New("matched sessionData not found")
return
} else {
prevTokenStoreKey = append(tokenStoreBaseKey, prevSessionData.Token.String()...)
prevAnotherRefreshTokenStoreKey = append(refreshTokenStoreBaseKey, prevSessionData.RefreshToken.String()...)
}
if fetchErr := extractDataFromRedis(prevTokenData, prevTokenStoreKey, client); fetchErr != nil {
prevTokenData = nil
}
if prevRefreshTokenData.ClientId.Equal(clientId) &&
prevSessionData.RefreshToken.Equal(refreshToken) &&
(prevTokenData == nil || refreshToken.Equal(prevTokenData.RefreshToken)) {
// 검증성공!
var (
newToken, _ = iface.NewUUIDV4() //token
newTokenStoreKey = append(tokenStoreBaseKey, newToken.String()...) //tokenData
newTokenData []byte //tokenData
newSessionData []byte //newSessionData
newSession = &SessionData{ //newSessionData
Token: newToken,
RefreshToken: refreshToken,
}
)
//newSession
if newTokenData, err = (&TokenData{
ClientId: clientId,
MemberId: memberId,
RefreshToken: refreshToken,
}).Marshal(); err != nil {
return
}
if newSessionData, err = newSession.Marshal(); err != nil {
return
}
client.Send("MULTI")
if prevTokenData != nil {
client.Send("DEL", prevTokenStoreKey)
}
client.Send("SETEX", newTokenStoreKey, tokenExpireTime, newTokenData)
client.Send("SETEX", sessionStoreKey, refreshTokenExpireTime, newSessionData)
client.Send("EXPIRE", refreshTokenStoreKey, refreshTokenExpireTime)
if _, err = redis.Values(client.Do("EXEC")); err != nil {
return
}
out = newSession
return
} else {
// 부정사용의혹!
client.Send("MULTI")
// 부정사용의혹!
if prevTokenData != nil {
client.Send("DEL", refreshTokenStoreKey, sessionStoreKey, prevTokenStoreKey, prevAnotherRefreshTokenStoreKey)
} else {
client.Send("DEL", refreshTokenStoreKey, sessionStoreKey, prevAnotherRefreshTokenStoreKey)
}
if _, err = redis.Values(client.Do("EXEC")); err != nil {
logger.Info("failed to AppTokenRefreshSessionByRefreshToken", err)
}
err = errors.New("cannot refresh token")
}
return
}
err = retries(tx)
return
}

65
subsys/redis/conn.go Normal file
View File

@ -0,0 +1,65 @@
package redis
import (
"fmt"
"time"
"amuz.es/gogs/infra/rooibos/util"
"github.com/garyburd/redigo/redis"
"github.com/youtube/vitess/go/pools"
"golang.org/x/net/context"
)
var (
pool *pools.ResourcePool = nil
logger = util.NewLogger("redis")
)
type resourceConn struct {
redis.Conn
}
func (r resourceConn) Close() {
r.Conn.Close()
}
func InitRedis(config *util.RedisConfig) {
connectStr := fmt.Sprintf("%s:%d",
config.Host,
config.Port)
logger.Debugf("connect to redis://%s:%d/%d",
config.Host,
config.Port,
config.DB)
pool = pools.NewResourcePool(func() (pools.Resource, error) {
c, err := redis.Dial("tcp", connectStr,
redis.DialDatabase(config.DB),
redis.DialPassword(config.Password),
)
return resourceConn{c}, err
}, 10, 10, time.Minute)
logger.Info("Redis connected")
}
func getConn() (conn resourceConn, err error) {
ctx := context.TODO()
var resource pools.Resource
if resource, err = pool.Get(ctx); err == nil {
res := resource.(resourceConn)
conn = res
}
return
}
func putConn(conn resourceConn) {
pool.Put(conn)
}
func CloseRedis() {
if pool != nil {
pool.Close()
logger.Info("Redis closed")
}
}

46
subsys/redis/redis_tx.go Normal file
View File

@ -0,0 +1,46 @@
package redis
import (
"math/rand"
"time"
retry "github.com/giantswarm/retry-go"
errs "github.com/pkg/errors"
)
type txError error
func newTxError(err error) error {
return newTxErrorWithMessage("key had changed during this tx", err)
}
func newTxErrorWithMessage(message string, err error) error {
var wrapped error
if err == nil {
wrapped = errs.New(message)
} else {
wrapped = errs.Wrap(err, message)
}
return wrapped
}
func retries(tx func() error) error {
return retry.Do(tx,
retry.RetryChecker(func(err error) (ret bool) {
switch err.(type) {
case txError:
ret = true
}
return
}),
retry.Timeout(400*time.Millisecond),
retry.AfterRetry(func(err error) {
if err == nil {
return
}
sleeps := 37 + rand.Intn(51)
time.Sleep(time.Duration(sleeps) * time.Millisecond)
}),
retry.MaxTries(5),
)
}

16
subsys/redis/serialize.go Normal file
View File

@ -0,0 +1,16 @@
package redis
import (
"amuz.es/gogs/infra/rooibos/util/iface"
"github.com/garyburd/redigo/redis"
)
func extractDataFromRedis(target iface.Serializable, key []byte, fetcher resourceConn) error {
if resp, err := redis.Bytes(fetcher.Do("GET", key)); err != nil {
return err
} else if err = target.Unmarshal(resp); err != nil {
return err
} else {
return nil
}
}

81
util/config.go Normal file
View File

@ -0,0 +1,81 @@
package util
import (
"io/ioutil"
"gopkg.in/yaml.v2"
)
type LogConfig struct {
FileName string
MaxSizeMb int
MaxBackup int
MaxDay int
}
type DbConfig struct {
Network string
Driver string
Host string
User string
Password string
Port uint16
DB string
Args string
}
type RedisConfig struct {
Host string
Port uint16
Password string
DB int
MaxRetries int
PoolSize int
}
type config struct {
Phase string
Bind string
Db DbConfig
Redis RedisConfig
Logging struct {
Application LogConfig
Access LogConfig
}
}
func (c config) Name() string {
return name
}
func (c config) Version() string {
return version
}
func (c config) BuildDate() string {
return buildDate
}
const name = "Rooibos"
var (
buildDate string
version string
Config = config{}
)
func LoadConfig(path string) error {
var (
source []byte
err error
)
if source, err = ioutil.ReadFile(path); err != nil {
return err
}
if err = yaml.Unmarshal(source, &Config); err != nil {
return err
}
return nil
}

16
util/daemon.go Normal file
View File

@ -0,0 +1,16 @@
package util
import "github.com/coreos/go-systemd/daemon"
type notifyType string
const (
DaemonStarted notifyType = "READY=1"
DaemonStopping notifyType = "STOPPING=1"
)
var sockPath string
func NotifyDaemon(status notifyType) {
daemon.SdNotify(false, string(status))
}

101
util/iface/bytes16.go Normal file
View File

@ -0,0 +1,101 @@
package iface
import (
"bytes"
"database/sql/driver"
"encoding/hex"
"errors"
"fmt"
)
func (b Bytes16) Marshal() ([]byte, error) {
return b[:], nil
}
func (b Bytes16) MarshalTo(buf []byte) (n int, err error) {
copy(buf, b[:])
return len(b), nil
}
func (b *Bytes16) Unmarshal(buf []byte) error {
if len(buf) != Bytes16Length {
return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf))
}
copy(b[:], buf)
return nil
}
func (b *Bytes16) UnmarshalHex(buf []byte) (err error) {
hexLen := hex.DecodedLen(len(buf))
if hexLen != Bytes16Length {
return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf))
}
_, err = hex.Decode(b[:], buf)
return
}
func (b Bytes16) MarshalHex() ([]byte, error) {
hexLen := hex.EncodedLen(Bytes16Length)
hexBuf := make([]byte,hexLen)
if encodedLen := hex.Encode(hexBuf, b[:]); encodedLen != hexLen {
return nil, fmt.Errorf("invalid bytes16 length: %d", len(hexBuf))
}
return hexBuf, nil
}
func (b Bytes16) MarshalJSON() (encoded []byte, err error) {
encoded, err = b.MarshalHex()
if err != nil {
return
}
var buffer bytes.Buffer
buffer.WriteRune('"')
buffer.Write(encoded)
buffer.WriteRune('"')
return buffer.Bytes(), nil
}
func (b *Bytes16) UnmarshalJSON(from []byte) error {
quote := []byte("\"")
quoteSize := len(quote)
if len(from) < quoteSize*2 {
return errors.New("invalid quote notation")
}
if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) {
return errors.New("invalid quote notation")
}
return b.UnmarshalHex(from[quoteSize:len(from)-quoteSize])
}
func (b Bytes16) Compare(other Bytes16) int {
return bytes.Compare(b[:], other[:])
}
func (b Bytes16) Equal(other Bytes16) bool {
return b.Compare(other) == 0
}
func (b *Bytes16) Size() int {
if b == nil {
return 0
}
return Bytes16Length
}
// Scan implements the Scanner interface.
func (b *Bytes16) Scan(src interface{}) error {
switch src.(type) {
case string:
b.UnmarshalHex([]byte(src.(string)))
default:
return errors.New("Incompatible type for UUID")
}
return nil
}
// Value implements the driver Valuer interface.
func (b Bytes16) Value() (driver.Value, error) {
return b.MarshalHex()
}

101
util/iface/bytes32.go Normal file
View File

@ -0,0 +1,101 @@
package iface
import (
"bytes"
"database/sql/driver"
"encoding/hex"
"errors"
"fmt"
)
func (b Bytes32) Marshal() ([]byte, error) {
return b[:], nil
}
func (b Bytes32) MarshalTo(buf []byte) (n int, err error) {
copy(buf, b[:])
return len(b), nil
}
func (b *Bytes32) Unmarshal(buf []byte) error {
if len(buf) != Bytes32Length {
return fmt.Errorf("invalid bytes32 (got %d bytes)", len(buf))
}
copy(b[:], buf)
return nil
}
func (b *Bytes32) UnmarshalHex(buf []byte) (err error) {
hexLen := hex.DecodedLen(len(buf))
if hexLen != Bytes32Length {
return fmt.Errorf("invalid bytes32 (got %d bytes)", len(buf))
}
_, err = hex.Decode(b[:], buf)
return
}
func (b Bytes32) MarshalHex() ([]byte, error) {
hexLen := hex.EncodedLen(Bytes32Length)
hexBuf := make([]byte,hexLen)
if encodedLen := hex.Encode(hexBuf, b[:]); encodedLen != hexLen {
return nil, fmt.Errorf("invalid bytes32 length: %d", len(hexBuf))
}
return hexBuf, nil
}
func (b Bytes32) MarshalJSON() (encoded []byte, err error) {
encoded, err = b.MarshalHex()
if err != nil {
return
}
var buffer bytes.Buffer
buffer.WriteRune('"')
buffer.Write(encoded)
buffer.WriteRune('"')
return buffer.Bytes(), nil
}
func (b *Bytes32) UnmarshalJSON(from []byte) error {
quote:=[]byte("\"")
quoteSize:=len(quote)
if len(from) < quoteSize*2{
return errors.New("invalid quote notation")
}
if !bytes.HasPrefix(from,quote) ||!bytes.HasSuffix(from,quote){
return errors.New("invalid quote notation")
}
return b.UnmarshalHex(from[quoteSize:len(from)-quoteSize])
}
func (b Bytes32) Compare(other Bytes32) int {
return bytes.Compare(b[:], other[:])
}
func (b Bytes32) Equal(other Bytes32) bool {
return b.Compare(other) == 0
}
func (b *Bytes32) Size() int {
if b == nil {
return 0
}
return Bytes32Length
}
// Scan implements the Scanner interface.
func (b *Bytes32) Scan(src interface{}) error {
switch src.(type) {
case string:
b.UnmarshalHex([]byte(src.(string)))
default:
return errors.New("Incompatible type for UUID")
}
return nil
}
// Value implements the driver Valuer interface.
func (b Bytes32) Value() (driver.Value, error) {
return b.MarshalHex()
}

100
util/iface/bytes64.go Normal file
View File

@ -0,0 +1,100 @@
package iface
import (
"bytes"
"database/sql/driver"
"encoding/hex"
"errors"
"fmt"
)
func (b Bytes64) Marshal() ([]byte, error) {
return b[:], nil
}
func (b Bytes64) MarshalTo(buf []byte) (n int, err error) {
copy(buf, b[:])
return len(b), nil
}
func (b *Bytes64) Unmarshal(buf []byte) error {
if len(buf) != Bytes64Length {
return fmt.Errorf("invalid bytes64 (got %d bytes)", len(buf))
}
copy(b[:], buf)
return nil
}
func (b *Bytes64) UnmarshalHex(buf []byte) (err error) {
hexLen := hex.DecodedLen(len(buf))
if hexLen != Bytes64Length {
return fmt.Errorf("invalid bytes64 (got %d bytes)", len(buf))
}
_, err = hex.Decode(b[:], buf)
return
}
func (b Bytes64) MarshalHex() ([]byte, error) {
hexLen := hex.EncodedLen(Bytes64Length)
hexBuf := make([]byte,hexLen)
if encodedLen := hex.Encode(hexBuf, b[:]); encodedLen != hexLen {
return nil, fmt.Errorf("invalid bytes64 length: %d", len(hexBuf))
}
return hexBuf, nil
}
func (b Bytes64) MarshalJSON()(encoded []byte, err error) {
encoded, err = b.MarshalHex()
if err != nil {
return
}
var buffer bytes.Buffer
buffer.WriteRune('"')
buffer.Write(encoded)
buffer.WriteRune('"')
return buffer.Bytes(), nil
}
func (b *Bytes64) UnmarshalJSON(from []byte) error {
quote:=[]byte("\"")
quoteSize:=len(quote)
if len(from) < quoteSize*2{
return errors.New("invalid quote notation")
}
if !bytes.HasPrefix(from,quote) ||!bytes.HasSuffix(from,quote){
return errors.New("invalid quote notation")
}
return b.UnmarshalHex(from[quoteSize:len(from)-quoteSize])
}
func (b Bytes64) Compare(other Bytes64) int {
return bytes.Compare(b[:], other[:])
}
func (b Bytes64) Equal(other Bytes64) bool {
return b.Compare(other) == 0
}
func (b *Bytes64) Size() int {
if b == nil {
return 0
}
return Bytes64Length
}
// Scan implements the Scanner interface.
func (b *Bytes64) Scan(src interface{}) error {
switch src.(type) {
case string:
b.UnmarshalHex([]byte(src.(string)))
default:
return errors.New("Incompatible type for UUID")
}
return nil
}
// Value implements the driver Valuer interface.
func (b Bytes64) Value() (driver.Value, error) {
return b.MarshalHex()
}

33
util/iface/types.go Normal file
View File

@ -0,0 +1,33 @@
package iface
import (
"amuz.es/gogs/infra/rooibos/util"
"github.com/google/uuid"
)
const Bytes16Length = 16
const Bytes32Length = 32
const Bytes64Length = 64
const UUIDLength = 16
type Bytes16 [Bytes16Length]byte
type Bytes32 [Bytes32Length]byte
type Bytes64 [Bytes64Length]byte
type UUID struct {
uuid.UUID
}
var logger = util.NewLogger("common_iface")
type Serializable interface {
Marshal() ([]byte, error)
Unmarshal(buf []byte) error
String() string
}
type JSONSerializable interface {
UnmarshalJSON(b []byte) error
MarshalJSON() ([]byte, error)
}

115
util/iface/uuid.go Normal file
View File

@ -0,0 +1,115 @@
package iface
import (
"bytes"
"database/sql/driver"
"encoding/hex"
"errors"
"github.com/google/uuid"
"fmt"
)
func (u UUID) Marshal() ([]byte, error) {
return u.UUID[:], nil
}
func (u UUID) MarshalTo(buf []byte) (n int, err error) {
copy(buf, u.UUID[:])
return len(u.UUID), nil
}
func (u *UUID) Unmarshal(buf []byte) error {
if len(buf) != UUIDLength {
return fmt.Errorf("invalid bytes16 (got %d bytes)", len(buf))
}
copy(u.UUID[:], buf)
return nil
}
func (u *UUID) UnmarshalHex(buf []byte) (err error) {
hexLen := hex.DecodedLen(len(buf))
if hexLen != UUIDLength {
return fmt.Errorf("invalid UUID (got %d bytes)", len(buf))
}
_, err = hex.Decode(u.UUID[:], buf)
return
}
func (u *UUID) MarshalHex() ([]byte, error) {
hexLen := hex.EncodedLen(UUIDLength)
hexBuf := make([]byte,hexLen)
if encodedLen := hex.Encode(hexBuf, u.UUID[:]); encodedLen != hexLen {
return nil, fmt.Errorf("invalid bytes16 length: %d", len(hexBuf))
}
return hexBuf, nil
}
func (u UUID) MarshalJSON() (encoded []byte, err error) {
encoded, err = u.MarshalHex()
if err != nil {
return
}
var buffer bytes.Buffer
buffer.WriteRune('"')
buffer.Write(encoded)
buffer.WriteRune('"')
return buffer.Bytes(), nil
}
func (u *UUID) UnmarshalJSON(from []byte) error {
quote := []byte("\"")
quoteSize := len(quote)
if len(from) < quoteSize*2 {
return errors.New("invalid quote notation")
}
if !bytes.HasPrefix(from, quote) || !bytes.HasSuffix(from, quote) {
return errors.New("invalid quote notation")
}
return u.UnmarshalHex(from[quoteSize:len(from)-quoteSize])
}
func (u UUID) Compare(other UUID) int {
return bytes.Compare(u.UUID[:], other.UUID[:])
}
func (u UUID) Equal(other UUID) bool {
return u.Compare(other) == 0
}
func (u *UUID) Size() int {
if u == nil {
return 0
}
return UUIDLength
}
func NewUUIDV4() (u UUID, err error) {
var ui uuid.UUID
if ui, err = uuid.NewRandom(); err == nil {
u.UUID = ui
}
return
}
// Scan implements the Scanner interface.
func (u *UUID) Scan(src interface{}) error {
switch src.(type) {
case string:
u.UnmarshalHex([]byte(src.(string)))
default:
return errors.New("Incompatible type for UUID")
}
return nil
}
// Value implements the driver Valuer interface.
func (u UUID) Value() (driver.Value, error) {
return u.MarshalHex()
}
// Value implements the driver Valuer interface.
func (u UUID) Random() {
u.UUID = uuid.New()
}

81
util/json.go Normal file
View File

@ -0,0 +1,81 @@
package util
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
var jsonContentType = []string{"application/json; charset=utf-8"}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
func DumpJSONHeaderOnly(c *gin.Context, code int) {
c.Status(code)
// Encode
header := c.Writer.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = jsonContentType
}
}
func DumpJSON(c *gin.Context, code int, serializable interface{}) {
c.Status(code)
// Encode
header := c.Writer.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = jsonContentType
}
if enc := json.NewEncoder(c.Writer); enc == nil {
panic(errors.New("empty json encoder"))
} else if err := enc.Encode(serializable); err != nil {
panic(err)
}
}
func LoadJSON(data []byte, deserializable interface{}) (err error) {
if reader := bytes.NewReader(data); reader == nil {
err = errors.New("empty json data")
} else {
err = LoadJSONReader(reader, deserializable)
}
return
}
func LoadJSONReader(r io.Reader, deserializable interface{}) (err error) {
decoder := json.NewDecoder(r)
return decoder.Decode(deserializable)
}
func BindJSON(c *gin.Context, deserializable interface{}) (err error) {
defer func() {
if err != nil {
c.AbortWithError(400, err).SetType(gin.ErrorTypeBind)
}
}()
if err = LoadJSONReader(c.Request.Body, deserializable); err != nil {
return
}
// validate(obj)
if binding.Validator == nil {
return nil
}
return binding.Validator.ValidateStruct(deserializable)
}

73
util/logger.go Normal file
View File

@ -0,0 +1,73 @@
package util
import (
"io"
"os"
"path"
"github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
var (
logger = logrus.StandardLogger()
rotaters []*lumberjack.Logger
logDir string
formatter = prefixed.TextFormatter{}
)
func init() {
logger.Formatter = &formatter
}
func LoggerIsStd() bool {
return !formatter.DisableColors
}
func InitLogger(verbose bool, logDirArg string, config *LogConfig) {
logDir = logDirArg
colorSupport, writer := NewLogWriter(config)
formatter.DisableColors = !colorSupport
logger.Out = writer
if verbose {
logger.Level = logrus.DebugLevel
} else {
logger.Level = logrus.InfoLevel
}
}
func NewLogger(prefix string) *logrus.Entry {
return logrus.NewEntry(logger).WithField("prefix", prefix)
}
func NewLogWriter(config *LogConfig) (bool, io.Writer) {
switch config.FileName {
case "Stdout":
return true, os.Stdout
case "Stderr":
return true, os.Stderr
default:
logpath := config.FileName
if logDir != "" {
logpath = path.Join(logDir, config.FileName)
}
logger.Info(" Attention!! log writes to ", config.FileName)
rotater := &lumberjack.Logger{
Filename: logpath,
MaxSize: config.MaxSizeMb, // megabytes
MaxBackups: config.MaxBackup,
MaxAge: config.MaxDay, //days
}
rotaters = append(rotaters, rotater)
return false, rotater
}
}
func RotateLogger() {
if len(rotaters) == 0 {
return
}
logger.Info("rotating logger")
for _, rotater := range rotaters {
rotater.Rotate()
}
logger.Info("rotated")
}

138
util/parse.go Normal file
View File

@ -0,0 +1,138 @@
package util
import (
"strconv"
)
func mustParseInt(value string, bits int) int64 {
if parsed, err := strconv.ParseInt(value, 10, bits); err != nil {
panic(err)
} else {
return parsed
}
}
func mustParseUint(value string, bits int) uint64 {
if parsed, err := strconv.ParseUint(value, 10, bits); err != nil {
panic(err)
} else {
return parsed
}
}
func ParseUint8(value string) (ret *uint8) {
if parsed, err := strconv.ParseUint(value, 10, 8); err == nil {
ret = new(uint8)
*ret = uint8(parsed)
}
return
}
func ParseUint16(value string) (ret *uint16) {
if parsed, err := strconv.ParseUint(value, 10, 16); err == nil {
ret = new(uint16)
*ret = uint16(parsed)
}
return
}
func ParseUint32(value string) (ret *uint32) {
if parsed, err := strconv.ParseUint(value, 10, 32); err == nil {
ret = new(uint32)
*ret = uint32(parsed)
}
return
}
func ParseUint64(value string) (ret *uint64) {
if parsed, err := strconv.ParseUint(value, 10, 64); err == nil {
ret = new(uint64)
*ret = uint64(parsed)
}
return
}
func ParseUint(value string) (ret *uint) {
if parsed, err := strconv.ParseUint(value, 10, 0); err == nil {
ret = new(uint)
*ret = uint(parsed)
}
return
}
func ParseInt8(value string) (ret *int8) {
if parsed, err := strconv.ParseInt(value, 10, 8); err == nil {
ret = new(int8)
*ret = int8(parsed)
}
return
}
func ParseInt16(value string) (ret *int16) {
if parsed, err := strconv.ParseInt(value, 10, 16); err == nil {
ret = new(int16)
*ret = int16(parsed)
}
return
}
func ParseInt32(value string) (ret *int32) {
if parsed, err := strconv.ParseInt(value, 10, 32); err == nil {
ret = new(int32)
*ret = int32(parsed)
}
return
}
func ParseInt64(value string) (ret *int64) {
if parsed, err := strconv.ParseInt(value, 10, 64); err == nil {
ret = new(int64)
*ret = int64(parsed)
}
return
}
func ParseInt(value string) (ret *int) {
if parsed, err := strconv.ParseInt(value, 10, 0); err == nil {
ret = new(int)
*ret = int(parsed)
}
return
}
func ParseUint8Must(value string) uint8 {
return uint8(mustParseUint(value, 8))
}
func ParseUint16Must(value string) uint16 {
return uint16(mustParseUint(value, 16))
}
func ParseUint32Must(value string) uint32 {
return uint32(mustParseUint(value, 32))
}
func ParseUint64Must(value string) uint64 {
return mustParseUint(value, 64)
}
func ParseUintMust(value string) uint {
return uint(mustParseUint(value, 0))
}
func ParseInt8Must(value string) int8 {
return int8(mustParseInt(value, 8))
}
func ParseInt16Must(value string) int16 {
return int16(mustParseInt(value, 16))
}
func ParseInt32Must(value string) int32 {
return int32(mustParseInt(value, 32))
}
func ParseInt64Must(value string) int64 {
return mustParseInt(value, 64)
}
func ParseIntMust(value string) int {
return int(mustParseInt(value, 0))
}

313
util/tool.go Normal file
View File

@ -0,0 +1,313 @@
package util
import (
"bytes"
"crypto/rand"
"fmt"
"image"
"math"
"reflect"
"strings"
"time"
"unsafe"
)
func Int64ToBytes(s int64) []byte {
return []byte{
byte(s >> 0),
byte(s >> 8),
byte(s >> 16),
byte(s >> 24),
byte(s >> 32),
byte(s >> 40),
byte(s >> 48),
byte(s >> 56),
}
}
func BytesToInt64(s []byte) int64 {
return int64(0) |
(int64(s[0]) << 0) |
(int64(s[1]) << 8) |
(int64(s[2]) << 16) |
(int64(s[3]) << 24) |
(int64(s[4]) << 32) |
(int64(s[5]) << 40) |
(int64(s[6]) << 48) |
(int64(s[7]) << 56)
}
func BytesToString(b []byte) string {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: bh.Data,
Len: bh.Len}
return *(*string)(unsafe.Pointer(&sh))
}
func StringToBytes(s string) []byte {
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: 0}
return *(*[]byte)(unsafe.Pointer(&bh))
}
// GetRandomString generate random string by specify chars.
func GetRandomString(n int, alphabets ...byte) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make([]byte, 0, n)
rand.Read(bytes)
for _, b := range bytes {
if len(alphabets) == 0 {
bytes = append(bytes, alphanum[b%byte(len(alphanum))])
} else {
bytes = append(bytes, alphabets[b%byte(len(alphabets))])
}
}
return string(bytes)
}
// Seconds-based time units
var (
NanoSecond uint64 = 1
MicroSecond = 1000 * NanoSecond
MilliSecond = 1000 * MicroSecond
Second = 1000 * MilliSecond
Minute = 60 * Second
Hour = 60 * Minute
Day = 24 * Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
)
func computeTimeDiff(buff *bytes.Buffer, diff uint64) uint64 {
switch {
case diff < 1*NanoSecond:
diff = 0
case diff < 2*NanoSecond:
diff = 0
buff.WriteString("001ns")
case diff < 1*MicroSecond:
buff.WriteString(fmt.Sprintf("%03dns", diff))
diff -= diff / NanoSecond * NanoSecond
case diff < 2*MicroSecond:
diff -= 1 * MilliSecond
buff.WriteString("001us")
case diff < 1*MilliSecond:
buff.WriteString(fmt.Sprintf("%03dus", diff/MicroSecond))
diff -= diff / MicroSecond * MicroSecond
case diff < 2*MilliSecond:
diff -= 1 * Second
buff.WriteString("001ms")
case diff < 1*Second:
buff.WriteString(fmt.Sprintf("%03dms", diff/MilliSecond))
diff -= diff / MilliSecond * MilliSecond
case diff < 2*Second:
diff -= 1 * Minute
buff.WriteString("01s")
case diff < 1*Minute:
buff.WriteString(fmt.Sprintf("%02ds", diff/Second))
diff -= diff / Second * Second
case diff < 2*Minute:
diff -= 1 * Minute
buff.WriteString("01m")
case diff < 1*Hour:
buff.WriteString(fmt.Sprintf("%02dm", diff/Minute))
diff -= diff / Minute * Minute
case diff < 2*Hour:
diff -= 1 * Hour
buff.WriteString("01h")
case diff < 1*Day:
buff.WriteString(fmt.Sprintf("%02dh", diff/Hour))
diff -= diff / Hour * Hour
case diff < 2*Day:
diff -= 1 * Day
buff.WriteString("01d")
case diff < 1*Week:
buff.WriteString(fmt.Sprintf("%02dd", diff/Day))
diff -= diff / Day * Day
case diff < 2*Week:
diff -= 1 * Week
buff.WriteString("1w")
case diff < 1*Month:
buff.WriteString(fmt.Sprintf("%01dw", diff/Week))
diff -= diff / Week * Week
case diff < 2*Month:
diff -= 1 * Month
buff.WriteString("01M")
case diff < 1*Year:
buff.WriteString(fmt.Sprintf("%02dM", diff/Month))
diff -= diff / Month * Month
case diff < 2*Year:
diff -= 1 * Year
buff.WriteString("01y")
default:
buff.WriteString(fmt.Sprintf("%02dy", diff/Year))
diff = 0
}
return diff
}
// TimeSincePro calculates the time interval and generate full user-friendly string.
func TimeSincePro(then time.Time) string {
now := time.Now()
diff := uint64(now.UnixNano() - then.UnixNano())
if then.After(now) {
return "future"
}
return TimeSinceDuration(diff)
}
// TimeSincePro calculates the time interval and generate full user-friendly string.
func TimeSinceDuration(then uint64) string {
var (
timeStr bytes.Buffer
diff = then
)
for {
diff = computeTimeDiff(&timeStr, diff)
if diff == 0 {
break
} else {
timeStr.WriteRune(' ')
}
}
if timeStr.Len() == 0 {
return "instant!"
} else {
return timeStr.String()
}
}
const (
Byte = 1
KByte = Byte * 1024
MByte = KByte * 1024
GByte = MByte * 1024
TByte = GByte * 1024
PByte = TByte * 1024
EByte = PByte * 1024
)
var bytesSizeTable = map[string]uint64{
"b": Byte,
"kb": KByte,
"mb": MByte,
"gb": GByte,
"tb": TByte,
"pb": PByte,
"eb": EByte,
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%dB", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := float64(s) / math.Pow(base, math.Floor(e))
f := "%.0f"
if val < 10 {
f = "%.1f"
}
return fmt.Sprintf(f+"%s", val, suffix)
}
// FileSize calculates the file size and generate user-friendly string.
func FileSize(s uint64) string {
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
return humanateBytes(s, 1024, sizes)
}
// Subtract deals with subtraction of all types of number.
func Subtract(left interface{}, right interface{}) interface{} {
var rleft, rright int64
var fleft, fright float64
var isInt bool = true
switch left.(type) {
case int:
rleft = int64(left.(int))
case int8:
rleft = int64(left.(int8))
case int16:
rleft = int64(left.(int16))
case int32:
rleft = int64(left.(int32))
case int64:
rleft = left.(int64)
case float32:
fleft = float64(left.(float32))
isInt = false
case float64:
fleft = left.(float64)
isInt = false
}
switch right.(type) {
case int:
rright = int64(right.(int))
case int8:
rright = int64(right.(int8))
case int16:
rright = int64(right.(int16))
case int32:
rright = int64(right.(int32))
case int64:
rright = right.(int64)
case float32:
fright = float64(left.(float32))
isInt = false
case float64:
fleft = left.(float64)
isInt = false
}
if isInt {
return rleft - rright
} else {
return fleft + float64(rleft) - (fright + float64(rright))
}
}
// EllipsisString returns a truncated short string,
// it appends '...' in the end of the length of string is too large.
func EllipsisString(str string, length int) string {
if len(str) < length {
return str
}
return str[:length-3] + "..."
}
func AspectRatio(srcRect image.Point, toResize uint64) image.Point {
w, h := int(toResize), getRatioSize(int(toResize), srcRect.Y, srcRect.X)
if srcRect.X < srcRect.Y {
w, h = getRatioSize(int(toResize), srcRect.X, srcRect.Y), int(toResize)
}
return image.Point{w, h}
}
func getRatioSize(a, b, c int) int {
d := a * b / c
return (d + 1) & -1
}
func Basename(s string) string {
n := strings.LastIndexByte(s, '.')
if n >= 0 {
return s[:n]
}
return s
}