Commit a005219c authored by Darren Shepherd's avatar Darren Shepherd

Update vendor

parent 1ae0c540
......@@ -146,8 +146,7 @@ import:
- package: github.com/hashicorp/golang-lru
version: v0.5.0
- package: github.com/ibuildthecloud/kvsql
version: 9f00ccc82235f0433c736306d091abd2939b7449
repo: https://github.com/erikwilson/rancher-kvsql.git
version: 0e798b1475327aadf3b8da5d2d1f99bb93dfd667
- package: github.com/imdario/mergo
version: v0.3.5
- package: github.com/inconshreveable/mousetrap
......@@ -175,7 +174,7 @@ import:
- package: github.com/mattn/go-shellwords
version: v1.0.3-20-gf8471b0a71ded0
- package: github.com/mattn/go-sqlite3
version: v1.9.0
version: v1.10.0
- package: github.com/matttproud/golang_protobuf_extensions
version: v1.0.0
- package: github.com/miekg/dns
......@@ -231,6 +230,9 @@ import:
repo: https://github.com/erikwilson/rancher-dynamiclistener.git
- package: github.com/rancher/helm-controller
version: v0.2.2
- package: github.com/rancher/kine
version: c308515dee837fd00d480e482f763d76e7572df5
repo: https://github.com/ibuildthecloud/kine.git
- package: github.com/rancher/remotedialer
version: 7c71ffa8f5d7a181704d92bb8a33b0c7d07dccaa
repo: https://github.com/erikwilson/rancher-remotedialer.git
......@@ -251,7 +253,7 @@ import:
- package: github.com/sigma/go-inotify
version: c87b6cf5033d2c6486046f045eeebdc3d910fd38
- package: github.com/sirupsen/logrus
version: v1.0.3
version: v1.4.1
- package: github.com/spf13/cobra
version: v0.0.1-34-gc439c4fa093711
- package: github.com/spf13/pflag
......@@ -278,8 +280,6 @@ import:
version: 1d523034197ff1f222f6429836dd36a2457a1874
- package: go.etcd.io/bbolt
version: v1.3.1-etcd.8
- package: go.etcd.io/etcd
version: v3.3.11
- package: golang.org/x/crypto
version: a49355c7e3f8fe157a85be2f77e6e269a0f89602
- package: golang.org/x/net
......
......@@ -22,7 +22,7 @@ github.com/coreos/flannel 823afe66b2266bf71f5bec24e6e28b26d70cfc7c ht
github.com/natefinch/lumberjack aee4629129445bbdfb69aa565537dcfa16544311
github.com/gorilla/mux v1.6.2
github.com/gorilla/websocket v1.2.0
github.com/mattn/go-sqlite3 v1.9.0
github.com/mattn/go-sqlite3 v1.10.0
github.com/go-sql-driver/mysql v1.4.1
github.com/lib/pq v1.1.1
go.etcd.io/etcd v3.3.11
......@@ -71,7 +71,7 @@ github.com/gogo/googleapis 08a7655d27152912db7aaf4f983275eaf8d128ef
github.com/golang/protobuf v1.1.0
#github.com/opencontainers/runtime-spec eba862dc2470385a233c7507392675cbeadf7353 # v1.0.1-45-geba862d
github.com/opencontainers/runc v1.0.0-rc8
github.com/sirupsen/logrus v1.0.3
github.com/sirupsen/logrus v1.4.1
#github.com/urfave/cli 7bc6a0acffa589f415f88aca16cc1de5ffd66f9c
golang.org/x/net cdfb69ac37fc6fa907650654115ebebb3aae2087
google.golang.org/grpc v1.12.0
......@@ -126,7 +126,8 @@ golang.org/x/oauth2 a6bd8cefa1811bd24b86f8902872e4e8225f74c4
golang.org/x/time f51c12702a4d776e4c1fa9b0fabab841babae631
gopkg.in/inf.v0 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
gopkg.in/yaml.v2 v2.2.1
github.com/ibuildthecloud/kvsql 9f00ccc82235f0433c736306d091abd2939b7449 https://github.com/erikwilson/rancher-kvsql.git
github.com/rancher/kine c308515dee837fd00d480e482f763d76e7572df5 https://github.com/ibuildthecloud/kine.git
google.golang.org/grpc v1.20.1
# rootless
github.com/rootless-containers/rootlesskit v0.4.1
......
......@@ -18,7 +18,6 @@ import (
"crypto/tls"
"time"
"github.com/coreos/etcd/pkg/transport"
"google.golang.org/grpc"
)
......@@ -40,6 +39,4 @@ type Config struct {
DialTimeout time.Duration
DialOptions []grpc.DialOption
TLSInfo *transport.TLSInfo
}
......@@ -16,7 +16,7 @@ import (
type Generic struct {
// revision must be first to ensure that this is properly aligned for atomic.LoadInt64
revision int64
revision int64
db *sql.DB
......@@ -64,7 +64,7 @@ func (g *Generic) Start(ctx context.Context, db *sql.DB) error {
err = g.cleanup(ctx)
if err != nil {
logrus.Errorf("Failed to cleanup duplicate entries: %v", err)
logrus.Errorf("Failed to cleanup duplicate entries")
}
}
}
......@@ -209,12 +209,14 @@ func (g *Generic) Update(ctx context.Context, key string, value []byte, revision
func (g *Generic) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
trace := utiltrace.New(fmt.Sprintf("SQL DB ExecContext query: %s keys: %v", query, args))
defer trace.LogIfLong(500 * time.Millisecond)
return g.db.ExecContext(ctx, query, args...)
}
func (g *Generic) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
trace := utiltrace.New(fmt.Sprintf("SQL DB QueryContext query: %s keys: %v", query, args))
defer trace.LogIfLong(500 * time.Millisecond)
return g.db.QueryContext(ctx, query, args...)
}
......
......@@ -2,6 +2,7 @@ package driver
import (
"context"
"io"
"strings"
)
......@@ -93,9 +94,10 @@ func start(watchResponses chan Event) {
}
func sendErrorAndClose(watchResponses chan Event, err error) {
if err != nil {
watchResponses <- Event{Err: err}
if err == nil {
err = io.EOF
}
watchResponses <- Event{Err: err}
close(watchResponses)
}
......
......@@ -21,13 +21,11 @@ import (
"strings"
"sync"
"github.com/ibuildthecloud/kvsql/clientv3/driver"
"github.com/ibuildthecloud/kvsql/clientv3/driver/sqlite"
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/mvcc/mvccpb"
"github.com/docker/docker/pkg/locker"
"github.com/ibuildthecloud/kvsql/clientv3/driver"
"github.com/ibuildthecloud/kvsql/clientv3/driver/mysql"
"github.com/ibuildthecloud/kvsql/clientv3/driver/pgsql"
"github.com/ibuildthecloud/kvsql/clientv3/driver/sqlite"
"golang.org/x/net/context"
)
......@@ -40,9 +38,9 @@ type (
)
var (
connections map[string]*kv
connectionsCtx context.Context
CloseDB func()
connections map[string]*kv
connectionsCtx context.Context
CloseDB func()
connectionsLock sync.Mutex
)
......@@ -103,9 +101,9 @@ func newKV(cfg Config) (*kv, error) {
}
var (
db *sql.DB
db *sql.DB
driver *driver.Generic
err error
err error
)
switch parts[0] {
......@@ -114,18 +112,6 @@ func newKV(cfg Config) (*kv, error) {
return nil, err
}
driver = sqlite.NewSQLite()
case "mysql":
if db, err = mysql.Open(parts[1], cfg.TLSInfo); err != nil {
return nil, err
}
driver = mysql.NewMySQL()
case "postgres":
if db, err = pgsql.Open(parts[1], cfg.TLSInfo); err != nil {
return nil, err
}
driver = pgsql.NewPGSQL()
default:
return nil, fmt.Errorf("unknown driver type [%s]", parts[0])
}
if err := driver.Start(context.TODO(), db); err != nil {
......@@ -134,11 +120,11 @@ func newKV(cfg Config) (*kv, error) {
}
kv := &kv{
d: driver,
d:driver,
}
connections[key] = kv
return kv, nil
return kv, nil
}
func (k *kv) Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error) {
......
......@@ -22,9 +22,8 @@ import (
"sync/atomic"
"time"
"github.com/coreos/etcd/pkg/transport"
"github.com/ibuildthecloud/kvsql/clientv3"
etcd3 "github.com/ibuildthecloud/kvsql/storage"
"github.com/ibuildthecloud/kvsql/storage"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/storagebackend"
......@@ -66,14 +65,8 @@ func NewKVSQLHealthCheck(c storagebackend.Config) (func() error, error) {
}
func newETCD3Client(c storagebackend.Config) (*clientv3.Client, error) {
tlsInfo := &transport.TLSInfo{
CertFile: c.Transport.CertFile,
KeyFile: c.Transport.KeyFile,
CAFile: c.Transport.CAFile,
}
cfg := clientv3.Config{
Endpoints: c.Transport.ServerList,
TLSInfo: tlsInfo,
}
if len(cfg.Endpoints) == 0 {
......
......@@ -12,18 +12,18 @@ env:
matrix:
- GOTAGS=
- GOTAGS=libsqlite3
- GOTAGS="sqlite_allow_uri_authority sqlite_app_armor sqlite_foreign_keys sqlite_fts5 sqlite_icu sqlite_introspect sqlite_json sqlite_secure_delete sqlite_see sqlite_stat4 sqlite_trace sqlite_userauth sqlite_vacuum_incr sqlite_vtable"
- GOTAGS="sqlite_allow_uri_authority sqlite_app_armor sqlite_foreign_keys sqlite_fts5 sqlite_icu sqlite_introspect sqlite_json sqlite_secure_delete sqlite_see sqlite_stat4 sqlite_trace sqlite_userauth sqlite_vacuum_incr sqlite_vtable sqlite_unlock_notify"
- GOTAGS=sqlite_vacuum_full
go:
- 1.9.x
- 1.10.x
- 1.11.x
before_install:
- |
if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
brew update
brew upgrade icu4c
fi
- |
go get github.com/smartystreets/goconvey
......
......@@ -67,6 +67,7 @@ This is also known as a DSN string. (Data Source Name).
Options are append after the filename of the SQLite database.
The database filename and options are seperated by an `?` (Question Mark).
Options should be URL-encoded (see [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)).
This also applies when using an in-memory database instead of a file.
......@@ -198,7 +199,7 @@ Additional information:
# Google Cloud Platform
Building on GCP is not possible because `Google Cloud Platform does not allow `gcc` to be executed.
Building on GCP is not possible because Google Cloud Platform does not allow `gcc` to be executed.
Please work only with compiled final binaries.
......@@ -290,7 +291,7 @@ For example the TDM-GCC Toolchain can be found [here](ttps://sourceforge.net/pro
When receiving a compile time error referencing recompile with `-FPIC` then you
are probably using a hardend system.
You can copile the library on a hardend system with the following command.
You can compile the library on a hardend system with the following command.
```bash
go build -ldflags '-extldflags=-fno-PIC'
......@@ -473,7 +474,7 @@ For an example see [shaxbee/go-spatialite](https://github.com/shaxbee/go-spatial
For more information see [#289](https://github.com/mattn/go-sqlite3/issues/289)
- Trying to execure a `.` (dot) command throws an error.
- Trying to execute a `.` (dot) command throws an error.
Error: `Error: near ".": syntax error`
Dot command are part of SQLite3 CLI not of this library.
......
......@@ -77,6 +77,12 @@ func updateHookTrampoline(handle uintptr, op int, db *C.char, table *C.char, row
callback(op, C.GoString(db), C.GoString(table), rowid)
}
//export authorizerTrampoline
func authorizerTrampoline(handle uintptr, op int, arg1 *C.char, arg2 *C.char, arg3 *C.char) int {
callback := lookupHandle(handle).(func(int, string, string, string) int)
return callback(op, C.GoString(arg1), C.GoString(arg2), C.GoString(arg3))
}
// Use handles to avoid passing Go pointers to C.
type handleVal struct {
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -78,8 +78,38 @@ _sqlite3_exec(sqlite3* db, const char* pcmd, long long* rowid, long long* change
return rv;
}
#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
extern int _sqlite3_step_blocking(sqlite3_stmt *stmt);
extern int _sqlite3_step_row_blocking(sqlite3_stmt* stmt, long long* rowid, long long* changes);
extern int _sqlite3_prepare_v2_blocking(sqlite3 *db, const char *zSql, int nBytes, sqlite3_stmt **ppStmt, const char **pzTail);
static int
_sqlite3_step_internal(sqlite3_stmt *stmt)
{
return _sqlite3_step_blocking(stmt);
}
static int
_sqlite3_step_row_internal(sqlite3_stmt* stmt, long long* rowid, long long* changes)
{
return _sqlite3_step_row_blocking(stmt, rowid, changes);
}
static int
_sqlite3_prepare_v2_internal(sqlite3 *db, const char *zSql, int nBytes, sqlite3_stmt **ppStmt, const char **pzTail)
{
return _sqlite3_prepare_v2_blocking(db, zSql, nBytes, ppStmt, pzTail);
}
#else
static int
_sqlite3_step_internal(sqlite3_stmt *stmt)
{
return sqlite3_step(stmt);
}
static int
_sqlite3_step(sqlite3_stmt* stmt, long long* rowid, long long* changes)
_sqlite3_step_row_internal(sqlite3_stmt* stmt, long long* rowid, long long* changes)
{
int rv = sqlite3_step(stmt);
sqlite3* db = sqlite3_db_handle(stmt);
......@@ -88,6 +118,13 @@ _sqlite3_step(sqlite3_stmt* stmt, long long* rowid, long long* changes)
return rv;
}
static int
_sqlite3_prepare_v2_internal(sqlite3 *db, const char *zSql, int nBytes, sqlite3_stmt **ppStmt, const char **pzTail)
{
return sqlite3_prepare_v2(db, zSql, nBytes, ppStmt, pzTail);
}
#endif
void _sqlite3_result_text(sqlite3_context* ctx, const char* s) {
sqlite3_result_text(ctx, s, -1, &free);
}
......@@ -119,6 +156,8 @@ int commitHookTrampoline(void*);
void rollbackHookTrampoline(void*);
void updateHookTrampoline(void*, int, char*, char*, sqlite3_int64);
int authorizerTrampoline(void*, int, char*, char*, char*, char*);
#ifdef SQLITE_LIMIT_WORKER_THREADS
# define _SQLITE_HAS_LIMIT
# define SQLITE_LIMIT_LENGTH 0
......@@ -200,18 +239,57 @@ func Version() (libVersion string, libVersionNumber int, sourceID string) {
}
const (
// used by authorizer and pre_update_hook
SQLITE_DELETE = C.SQLITE_DELETE
SQLITE_INSERT = C.SQLITE_INSERT
SQLITE_UPDATE = C.SQLITE_UPDATE
// used by authorzier - as return value
SQLITE_OK = C.SQLITE_OK
SQLITE_IGNORE = C.SQLITE_IGNORE
SQLITE_DENY = C.SQLITE_DENY
// different actions query tries to do - passed as argument to authorizer
SQLITE_CREATE_INDEX = C.SQLITE_CREATE_INDEX
SQLITE_CREATE_TABLE = C.SQLITE_CREATE_TABLE
SQLITE_CREATE_TEMP_INDEX = C.SQLITE_CREATE_TEMP_INDEX
SQLITE_CREATE_TEMP_TABLE = C.SQLITE_CREATE_TEMP_TABLE
SQLITE_CREATE_TEMP_TRIGGER = C.SQLITE_CREATE_TEMP_TRIGGER
SQLITE_CREATE_TEMP_VIEW = C.SQLITE_CREATE_TEMP_VIEW
SQLITE_CREATE_TRIGGER = C.SQLITE_CREATE_TRIGGER
SQLITE_CREATE_VIEW = C.SQLITE_CREATE_VIEW
SQLITE_CREATE_VTABLE = C.SQLITE_CREATE_VTABLE
SQLITE_DROP_INDEX = C.SQLITE_DROP_INDEX
SQLITE_DROP_TABLE = C.SQLITE_DROP_TABLE
SQLITE_DROP_TEMP_INDEX = C.SQLITE_DROP_TEMP_INDEX
SQLITE_DROP_TEMP_TABLE = C.SQLITE_DROP_TEMP_TABLE
SQLITE_DROP_TEMP_TRIGGER = C.SQLITE_DROP_TEMP_TRIGGER
SQLITE_DROP_TEMP_VIEW = C.SQLITE_DROP_TEMP_VIEW
SQLITE_DROP_TRIGGER = C.SQLITE_DROP_TRIGGER
SQLITE_DROP_VIEW = C.SQLITE_DROP_VIEW
SQLITE_DROP_VTABLE = C.SQLITE_DROP_VTABLE
SQLITE_PRAGMA = C.SQLITE_PRAGMA
SQLITE_READ = C.SQLITE_READ
SQLITE_SELECT = C.SQLITE_SELECT
SQLITE_TRANSACTION = C.SQLITE_TRANSACTION
SQLITE_ATTACH = C.SQLITE_ATTACH
SQLITE_DETACH = C.SQLITE_DETACH
SQLITE_ALTER_TABLE = C.SQLITE_ALTER_TABLE
SQLITE_REINDEX = C.SQLITE_REINDEX
SQLITE_ANALYZE = C.SQLITE_ANALYZE
SQLITE_FUNCTION = C.SQLITE_FUNCTION
SQLITE_SAVEPOINT = C.SQLITE_SAVEPOINT
SQLITE_COPY = C.SQLITE_COPY
/*SQLITE_RECURSIVE = C.SQLITE_RECURSIVE*/
)
// SQLiteDriver implement sql.Driver.
// SQLiteDriver implements driver.Driver.
type SQLiteDriver struct {
Extensions []string
ConnectHook func(*SQLiteConn) error
}
// SQLiteConn implement sql.Conn.
// SQLiteConn implements driver.Conn.
type SQLiteConn struct {
mu sync.Mutex
db *C.sqlite3
......@@ -221,12 +299,12 @@ type SQLiteConn struct {
aggregators []*aggInfo
}
// SQLiteTx implemen sql.Tx.
// SQLiteTx implements driver.Tx.
type SQLiteTx struct {
c *SQLiteConn
}
// SQLiteStmt implement sql.Stmt.
// SQLiteStmt implements driver.Stmt.
type SQLiteStmt struct {
mu sync.Mutex
c *SQLiteConn
......@@ -236,13 +314,13 @@ type SQLiteStmt struct {
cls bool
}
// SQLiteResult implement sql.Result.
// SQLiteResult implements sql.Result.
type SQLiteResult struct {
id int64
changes int64
}
// SQLiteRows implement sql.Rows.
// SQLiteRows implements driver.Rows.
type SQLiteRows struct {
s *SQLiteStmt
nc int
......@@ -440,6 +518,20 @@ func (c *SQLiteConn) RegisterUpdateHook(callback func(int, string, string, int64
}
}
// RegisterAuthorizer sets the authorizer for connection.
//
// The parameters to the callback are the operation (one of the constants
// SQLITE_INSERT, SQLITE_DELETE, or SQLITE_UPDATE), and 1 to 3 arguments,
// depending on operation. More details see:
// https://www.sqlite.org/c3ref/c_alter_table.html
func (c *SQLiteConn) RegisterAuthorizer(callback func(int, string, string, string) int) {
if callback == nil {
C.sqlite3_set_authorizer(c.db, nil, nil)
} else {
C.sqlite3_set_authorizer(c.db, (*[0]byte)(C.authorizerTrampoline), unsafe.Pointer(newHandle(c, callback)))
}
}
// RegisterFunc makes a Go function available as a SQLite function.
//
// The Go function can have arguments of the following types: any
......@@ -1582,7 +1674,7 @@ func (c *SQLiteConn) prepare(ctx context.Context, query string) (driver.Stmt, er
defer C.free(unsafe.Pointer(pquery))
var s *C.sqlite3_stmt
var tail *C.char
rv := C.sqlite3_prepare_v2(c.db, pquery, -1, &s, &tail)
rv := C._sqlite3_prepare_v2_internal(c.db, pquery, -1, &s, &tail)
if rv != C.SQLITE_OK {
return nil, c.lastError()
}
......@@ -1816,7 +1908,7 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
}
var rowid, changes C.longlong
rv := C._sqlite3_step(s.s, &rowid, &changes)
rv := C._sqlite3_step_row_internal(s.s, &rowid, &changes)
if rv != C.SQLITE_ROW && rv != C.SQLITE_OK && rv != C.SQLITE_DONE {
err := s.c.lastError()
C.sqlite3_reset(s.s)
......@@ -1883,12 +1975,12 @@ func (rc *SQLiteRows) DeclTypes() []string {
// Next move cursor to next.
func (rc *SQLiteRows) Next(dest []driver.Value) error {
rc.s.mu.Lock()
defer rc.s.mu.Unlock()
if rc.s.closed {
return io.EOF
}
rc.s.mu.Lock()
defer rc.s.mu.Unlock()
rv := C.sqlite3_step(rc.s.s)
rv := C._sqlite3_step_internal(rc.s.s)
if rv == C.SQLITE_DONE {
return io.EOF
}
......
......@@ -83,13 +83,13 @@ func CryptEncoderSSHA256(salt string) func(pass []byte, hash interface{}) []byte
}
}
// CryptEncoderSHA384 encodes a password with SHA256
// CryptEncoderSHA384 encodes a password with SHA384
func CryptEncoderSHA384(pass []byte, hash interface{}) []byte {
h := sha512.Sum384(pass)
return h[:]
}
// CryptEncoderSSHA384 encodes a password with SHA256
// CryptEncoderSSHA384 encodes a password with SHA384
// with the configured salt
func CryptEncoderSSHA384(salt string) func(pass []byte, hash interface{}) []byte {
return func(pass []byte, hash interface{}) []byte {
......@@ -100,13 +100,13 @@ func CryptEncoderSSHA384(salt string) func(pass []byte, hash interface{}) []byte
}
}
// CryptEncoderSHA512 encodes a password with SHA256
// CryptEncoderSHA512 encodes a password with SHA512
func CryptEncoderSHA512(pass []byte, hash interface{}) []byte {
h := sha512.Sum512(pass)
return h[:]
}
// CryptEncoderSSHA512 encodes a password with SHA256
// CryptEncoderSSHA512 encodes a password with SHA512
// with the configured salt
func CryptEncoderSSHA512(salt string) func(pass []byte, hash interface{}) []byte {
return func(pass []byte, hash interface{}) []byte {
......
// Copyright (C) 2018 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
#ifdef SQLITE_ENABLE_UNLOCK_NOTIFY
#include <stdio.h>
#include <sqlite3-binding.h>
extern int unlock_notify_wait(sqlite3 *db);
int
_sqlite3_step_blocking(sqlite3_stmt *stmt)
{
int rv;
sqlite3* db;
db = sqlite3_db_handle(stmt);
for (;;) {
rv = sqlite3_step(stmt);
if (rv != SQLITE_LOCKED) {
break;
}
if (sqlite3_extended_errcode(db) != SQLITE_LOCKED_SHAREDCACHE) {
break;
}
rv = unlock_notify_wait(db);
if (rv != SQLITE_OK) {
break;
}
sqlite3_reset(stmt);
}
return rv;
}
int
_sqlite3_step_row_blocking(sqlite3_stmt* stmt, long long* rowid, long long* changes)
{
int rv;
sqlite3* db;
db = sqlite3_db_handle(stmt);
for (;;) {
rv = sqlite3_step(stmt);
if (rv!=SQLITE_LOCKED) {
break;
}
if (sqlite3_extended_errcode(db) != SQLITE_LOCKED_SHAREDCACHE) {
break;
}
rv = unlock_notify_wait(db);
if (rv != SQLITE_OK) {
break;
}
sqlite3_reset(stmt);
}
*rowid = (long long) sqlite3_last_insert_rowid(db);
*changes = (long long) sqlite3_changes(db);
return rv;
}
int
_sqlite3_prepare_v2_blocking(sqlite3 *db, const char *zSql, int nBytes, sqlite3_stmt **ppStmt, const char **pzTail)
{
int rv;
for (;;) {
rv = sqlite3_prepare_v2(db, zSql, nBytes, ppStmt, pzTail);
if (rv!=SQLITE_LOCKED) {
break;
}
if (sqlite3_extended_errcode(db) != SQLITE_LOCKED_SHAREDCACHE) {
break;
}
rv = unlock_notify_wait(db);
if (rv != SQLITE_OK) {
break;
}
}
return rv;
}
#endif
// Copyright (C) 2018 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// +build cgo
// +build sqlite_unlock_notify
package sqlite3
/*
#cgo CFLAGS: -DSQLITE_ENABLE_UNLOCK_NOTIFY
#include <stdlib.h>
#include <sqlite3-binding.h>
extern void unlock_notify_callback(void *arg, int argc);
*/
import "C"
import (
"fmt"
"math"
"sync"
"unsafe"
)
type unlock_notify_table struct {
sync.Mutex
seqnum uint
table map[uint]chan struct{}
}
var unt unlock_notify_table = unlock_notify_table{table: make(map[uint]chan struct{})}
func (t *unlock_notify_table) add(c chan struct{}) uint {
t.Lock()
defer t.Unlock()
h := t.seqnum
t.table[h] = c
t.seqnum++
return h
}
func (t *unlock_notify_table) remove(h uint) {
t.Lock()
defer t.Unlock()
delete(t.table, h)
}
func (t *unlock_notify_table) get(h uint) chan struct{} {
t.Lock()
defer t.Unlock()
c, ok := t.table[h]
if !ok {
panic(fmt.Sprintf("Non-existent key for unlcok-notify channel: %d", h))
}
return c
}
//export unlock_notify_callback
func unlock_notify_callback(argv unsafe.Pointer, argc C.int) {
for i := 0; i < int(argc); i++ {
parg := ((*(*[(math.MaxInt32 - 1) / unsafe.Sizeof((*C.uint)(nil))]*[1]uint)(argv))[i])
arg := *parg
h := arg[0]
c := unt.get(h)
c <- struct{}{}
}
}
//export unlock_notify_wait
func unlock_notify_wait(db *C.sqlite3) C.int {
// It has to be a bufferred channel to not block in sqlite_unlock_notify
// as sqlite_unlock_notify could invoke the callback before it returns.
c := make(chan struct{}, 1)
defer close(c)
h := unt.add(c)
defer unt.remove(h)
pargv := C.malloc(C.sizeof_uint)
defer C.free(pargv)
argv := (*[1]uint)(pargv)
argv[0] = h
if rv := C.sqlite3_unlock_notify(db, (*[0]byte)(C.unlock_notify_callback), unsafe.Pointer(pargv)); rv != C.SQLITE_OK {
return rv
}
<-c
return C.SQLITE_OK
}
......@@ -10,5 +10,8 @@ package sqlite3
/*
#cgo CFLAGS: -I.
#cgo linux LDFLAGS: -ldl
#cgo linux,ppc LDFLAGS: -lpthread
#cgo linux,ppc64 LDFLAGS: -lpthread
#cgo linux,ppc64le LDFLAGS: -lpthread
*/
import "C"
......@@ -311,6 +311,12 @@ struct sqlite3_api_routines {
int (*str_errcode)(sqlite3_str*);
int (*str_length)(sqlite3_str*);
char *(*str_value)(sqlite3_str*);
int (*create_window_function)(sqlite3*,const char*,int,int,void*,
void (*xStep)(sqlite3_context*,int,sqlite3_value**),
void (*xFinal)(sqlite3_context*),
void (*xValue)(sqlite3_context*),
void (*xInv)(sqlite3_context*,int,sqlite3_value**),
void(*xDestroy)(void*));
};
/*
......@@ -596,6 +602,8 @@ typedef int (*sqlite3_loadext_entry)(
#define sqlite3_str_errcode sqlite3_api->str_errcode
#define sqlite3_str_length sqlite3_api->str_length
#define sqlite3_str_value sqlite3_api->str_value
/* Version 3.25.0 and later */
#define sqlite3_create_window_function sqlite3_api->create_window_function
#endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
#if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
......
......@@ -176,27 +176,3 @@
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
module github.com/rancher/kine
go 1.12
require (
github.com/coreos/etcd v3.3.13+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/gogo/protobuf v1.2.1 // indirect
github.com/lib/pq v1.1.1
github.com/mattn/go-sqlite3 v1.10.0
github.com/pkg/errors v0.8.1
github.com/rancher/wrangler v0.0.0-20190512193419-40fa298578b9
github.com/sirupsen/logrus v1.4.1
github.com/urfave/cli v1.21.0
google.golang.org/grpc v1.20.1
)
package broadcaster
import (
"context"
"sync"
)
type ConnectFunc func() (chan interface{}, error)
type Broadcaster struct {
sync.Mutex
running bool
subs map[chan interface{}]struct{}
}
func (b *Broadcaster) Subscribe(ctx context.Context, connect ConnectFunc) (<-chan interface{}, error) {
b.Lock()
defer b.Unlock()
if !b.running {
if err := b.start(connect); err != nil {
return nil, err
}
}
sub := make(chan interface{}, 100)
if b.subs == nil {
b.subs = map[chan interface{}]struct{}{}
}
b.subs[sub] = struct{}{}
go func() {
<-ctx.Done()
b.unsub(sub, true)
}()
return sub, nil
}
func (b *Broadcaster) unsub(sub chan interface{}, lock bool) {
if lock {
b.Lock()
}
if _, ok := b.subs[sub]; ok {
close(sub)
delete(b.subs, sub)
}
if lock {
b.Unlock()
}
}
func (b *Broadcaster) start(connect ConnectFunc) error {
c, err := connect()
if err != nil {
return err
}
go b.stream(c)
b.running = true
return nil
}
func (b *Broadcaster) stream(input chan interface{}) {
for item := range input {
b.Lock()
for sub := range b.subs {
select {
case sub <- item:
default:
// Slow consumer, drop
go b.unsub(sub, true)
}
}
b.Unlock()
}
b.Lock()
for sub := range b.subs {
b.unsub(sub, false)
}
b.running = false
b.Unlock()
}
package client
import (
"context"
"fmt"
"time"
"github.com/coreos/etcd/clientv3"
"github.com/rancher/kine/pkg/endpoint"
)
type Value struct {
Data []byte
Modified int64
}
type Client interface {
Get(ctx context.Context, key string) (Value, error)
Put(ctx context.Context, key string, value []byte) error
Create(ctx context.Context, key string, value []byte) error
Update(ctx context.Context, key string, revision int64, value []byte) error
Close() error
}
type client struct {
c *clientv3.Client
}
func New(config endpoint.ETCDConfig) (Client, error) {
tlsConfig, err := config.TLSConfig.ClientConfig()
if err != nil {
return nil, err
}
c, err := clientv3.New(clientv3.Config{
Endpoints: config.Endpoints,
DialTimeout: 5 * time.Second,
TLS: tlsConfig,
})
if err != nil {
return nil, err
}
return &client{
c: c,
}, nil
}
func (c *client) Get(ctx context.Context, key string) (Value, error) {
resp, err := c.c.Get(ctx, key)
if err != nil {
return Value{}, err
}
if len(resp.Kvs) == 1 {
return Value{
Data: resp.Kvs[0].Value,
Modified: resp.Kvs[0].ModRevision,
}, nil
}
return Value{}, nil
}
func (c *client) Put(ctx context.Context, key string, value []byte) error {
val, err := c.Get(ctx, key)
if err != nil {
return err
}
if val.Modified == 0 {
return c.Create(ctx, key, value)
}
return c.Update(ctx, key, val.Modified, value)
}
func (c *client) Create(ctx context.Context, key string, value []byte) error {
resp, err := c.c.Txn(ctx).
If(clientv3.Compare(clientv3.ModRevision(key), "=", 0)).
Then(clientv3.OpPut(key, string(value))).
Commit()
if err != nil {
return err
}
if !resp.Succeeded {
return fmt.Errorf("key exists")
}
return nil
}
func (c *client) Update(ctx context.Context, key string, revision int64, value []byte) error {
resp, err := c.c.Txn(ctx).
If(clientv3.Compare(clientv3.ModRevision(key), "=", revision)).
Then(clientv3.OpPut(key, string(value))).
Else(clientv3.OpGet(key)).
Commit()
if err != nil {
return err
}
if !resp.Succeeded {
return fmt.Errorf("revision %d doesnt match", revision)
}
return nil
}
func (c *client) Close() error {
return c.c.Close()
}
package generic
import (
"context"
"database/sql"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/sirupsen/logrus"
)
var (
columns = "kv.id as theid, kv.name, kv.created, kv.deleted, kv.create_revision, kv.prev_revision, kv.lease, kv.value, kv.old_value"
revSQL = `
SELECT rkv.id
FROM kine rkv
ORDER BY rkv.id
DESC LIMIT 1`
compactRevSQL = `
SELECT crkv.prev_revision
FROM kine crkv
WHERE crkv.name = 'compact_rev_key'
ORDER BY crkv.id DESC LIMIT 1`
idOfKey = `
AND mkv.id <= ? AND mkv.id > (
SELECT ikv.id
FROM kine ikv
WHERE
ikv.name = ? AND
ikv.id <= ?
ORDER BY ikv.id DESC LIMIT 1)`
listSQL = fmt.Sprintf(`SELECT (%s), (%s), %s
FROM kine kv
JOIN (
SELECT MAX(mkv.id) as id
FROM kine mkv
WHERE
mkv.name LIKE ?
%%s
GROUP BY mkv.name) maxkv
ON maxkv.id = kv.id
WHERE
(kv.deleted = 0 OR ?)
ORDER BY kv.id ASC
`, revSQL, compactRevSQL, columns)
)
type Stripped string
func (s Stripped) String() string {
str := strings.ReplaceAll(string(s), "\n", "")
return regexp.MustCompile("[\t ]+").ReplaceAllString(str, " ")
}
type Generic struct {
LastInsertID bool
DB *sql.DB
GetCurrentSQL string
GetRevisionSQL string
RevisionSQL string
ListRevisionStartSQL string
GetRevisionAfterSQL string
CountSQL string
AfterSQL string
DeleteSQL string
UpdateCompactSQL string
InsertSQL string
InsertLastInsertIDSQL string
}
func q(sql, param string, numbered bool) string {
if param == "?" && !numbered {
return sql
}
regex := regexp.MustCompile(`\?`)
n := 0
return regex.ReplaceAllStringFunc(sql, func(string) string {
if numbered {
n++
return param + strconv.Itoa(n)
}
return param
})
}
func (d *Generic) Migrate(ctx context.Context) {
var (
count = 0
countKV = d.queryRow(ctx, "SELECT COUNT(*) FROM key_value")
countKine = d.queryRow(ctx, "SELECT COUNT(*) FROM kine")
)
if err := countKV.Scan(&count); err != nil || count == 0 {
return
}
if err := countKine.Scan(&count); err != nil || count != 0 {
return
}
logrus.Infof("Migrating content from old table")
_, err := d.execute(ctx,
`INSERT INTO kine(deleted, create_revision, prev_revision, name, value, created, lease)
SELECT 0, 0, 0, kv.name, kv.value, 1, CASE WHEN kv.ttl > 0 THEN 15 ELSE 0 END
FROM key_value kv
WHERE kv.id IN (SELECT MAX(kvd.id) FROM key_value kvd GROUP BY kvd.name)`)
if err != nil {
logrus.Errorf("Migration failed: %v", err)
}
}
func Open(driverName, dataSourceName string, paramCharacter string, numbered bool) (*Generic, error) {
db, err := sql.Open(driverName, dataSourceName)
if err != nil {
return nil, err
}
return &Generic{
DB: db,
GetRevisionSQL: q(fmt.Sprintf(`
SELECT
0, 0, %s
FROM kine kv
WHERE kv.id = ?`, columns), paramCharacter, numbered),
GetCurrentSQL: q(fmt.Sprintf(listSQL, ""), paramCharacter, numbered),
ListRevisionStartSQL: q(fmt.Sprintf(listSQL, "AND mkv.id <= ?"), paramCharacter, numbered),
GetRevisionAfterSQL: q(fmt.Sprintf(listSQL, idOfKey), paramCharacter, numbered),
CountSQL: q(fmt.Sprintf(`
SELECT (%s), COUNT(c.theid)
FROM (
%s
) c`, revSQL, fmt.Sprintf(listSQL, "")), paramCharacter, numbered),
AfterSQL: q(fmt.Sprintf(`
SELECT (%s), (%s), %s
FROM kine kv
WHERE
kv.name LIKE ? AND
kv.id > ?
ORDER BY kv.id ASC`, revSQL, compactRevSQL, columns), paramCharacter, numbered),
DeleteSQL: q(`
DELETE FROM kine
WHERE id = ?`, paramCharacter, numbered),
UpdateCompactSQL: q(`
UPDATE kine
SET prev_revision = ?
WHERE name = 'compact_rev_key'`, paramCharacter, numbered),
InsertLastInsertIDSQL: q(`INSERT INTO kine(name, created, deleted, create_revision, prev_revision, lease, value, old_value)
values(?, ?, ?, ?, ?, ?, ?, ?)`, paramCharacter, numbered),
InsertSQL: q(`INSERT INTO kine(name, created, deleted, create_revision, prev_revision, lease, value, old_value)
values(?, ?, ?, ?, ?, ?, ?, ?) RETURNING id`, paramCharacter, numbered),
}, nil
}
func (d *Generic) query(ctx context.Context, sql string, args ...interface{}) (*sql.Rows, error) {
logrus.Tracef("QUERY %v : %s", args, Stripped(sql))
return d.DB.QueryContext(ctx, sql, args...)
}
func (d *Generic) queryRow(ctx context.Context, sql string, args ...interface{}) *sql.Row {
logrus.Tracef("QUERY ROW %v : %s", args, Stripped(sql))
return d.DB.QueryRowContext(ctx, sql, args...)
}
func (d *Generic) execute(ctx context.Context, sql string, args ...interface{}) (sql.Result, error) {
logrus.Tracef("EXEC %v : %s", args, Stripped(sql))
return d.DB.ExecContext(ctx, sql, args...)
}
func (d *Generic) GetCompactRevision(ctx context.Context) (int64, error) {
var id int64
row := d.queryRow(ctx, compactRevSQL)
err := row.Scan(&id)
if err == sql.ErrNoRows {
return 0, nil
}
return id, err
}
func (d *Generic) SetCompactRevision(ctx context.Context, revision int64) error {
result, err := d.execute(ctx, d.UpdateCompactSQL, revision)
if err != nil {
return err
}
num, err := result.RowsAffected()
if err != nil {
return err
}
if num != 0 {
return nil
}
_, err = d.Insert(ctx, "compact_rev_key", false, false, 0, revision, 0, []byte(""), nil)
return err
}
func (d *Generic) GetRevision(ctx context.Context, revision int64) (*sql.Rows, error) {
return d.query(ctx, d.GetRevisionSQL, revision)
}
func (d *Generic) DeleteRevision(ctx context.Context, revision int64) error {
_, err := d.execute(ctx, d.DeleteSQL, revision)
return err
}
func (d *Generic) ListCurrent(ctx context.Context, prefix string, limit int64, includeDeleted bool) (*sql.Rows, error) {
sql := d.GetCurrentSQL
if limit > 0 {
sql = fmt.Sprintf("%s LIMIT %d", sql, limit)
}
return d.query(ctx, sql, prefix, includeDeleted)
}
func (d *Generic) List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeleted bool) (*sql.Rows, error) {
if startKey == "" {
sql := d.ListRevisionStartSQL
if limit > 0 {
sql = fmt.Sprintf("%s LIMIT %d", sql, limit)
}
return d.query(ctx, sql, prefix, revision, includeDeleted)
}
sql := d.GetRevisionAfterSQL
if limit > 0 {
sql = fmt.Sprintf("%s LIMIT %d", sql, limit)
}
return d.query(ctx, sql, prefix, revision, startKey, revision, includeDeleted)
}
func (d *Generic) Count(ctx context.Context, prefix string) (int64, int64, error) {
var (
rev sql.NullInt64
id int64
)
row := d.queryRow(ctx, d.CountSQL, prefix, false)
err := row.Scan(&rev, &id)
return rev.Int64, id, err
}
func (d *Generic) CurrentRevision(ctx context.Context) (int64, error) {
var id int64
row := d.queryRow(ctx, revSQL)
err := row.Scan(&id)
if err == sql.ErrNoRows {
return 0, nil
}
return id, err
}
func (d *Generic) After(ctx context.Context, prefix string, rev int64) (*sql.Rows, error) {
sql := d.AfterSQL
return d.query(ctx, sql, prefix, rev)
}
func (d *Generic) Insert(ctx context.Context, key string, create, delete bool, createRevision, previousRevision int64, ttl int64, value, prevValue []byte) (id int64, err error) {
cVal := 0
dVal := 0
if create {
cVal = 1
}
if delete {
dVal = 1
}
if d.LastInsertID {
row, err := d.execute(ctx, d.InsertLastInsertIDSQL, key, cVal, dVal, createRevision, previousRevision, ttl, value, prevValue)
if err != nil {
return 00, err
}
return row.LastInsertId()
}
row := d.queryRow(ctx, d.InsertSQL, key, cVal, dVal, createRevision, previousRevision, ttl, value, prevValue)
err = row.Scan(&id)
return id, err
}
package mysql
import (
"crypto/tls"
"context"
cryptotls "crypto/tls"
"database/sql"
"strings"
"github.com/coreos/etcd/pkg/transport"
"github.com/go-sql-driver/mysql"
"github.com/ibuildthecloud/kvsql/clientv3/driver"
"github.com/rancher/kine/pkg/drivers/generic"
"github.com/rancher/kine/pkg/logstructured"
"github.com/rancher/kine/pkg/logstructured/sqllog"
"github.com/rancher/kine/pkg/server"
"github.com/rancher/kine/pkg/tls"
)
const (
......@@ -16,87 +19,63 @@ const (
)
var (
fieldList = "name, value, old_value, old_revision, create_revision, revision, ttl, version, del"
baseList = `
SELECT kv.id, kv.name, kv.value, kv.old_value, kv.old_revision, kv.create_revision, kv.revision, kv.ttl, kv.version, kv.del
FROM key_value kv
INNER JOIN
(
SELECT MAX(revision) revision, kvi.name
FROM key_value kvi
%REV%
GROUP BY kvi.name
) AS r
ON r.name = kv.name AND r.revision = kv.revision
WHERE kv.name like ? %RES% ORDER BY kv.name ASC limit ?
`
insertSQL = `
INSERT INTO key_value(` + fieldList + `)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
schema = []string{
`create table if not exists key_value
`create table if not exists kine
(
id INTEGER AUTO_INCREMENT,
name TEXT,
value BLOB,
created INTEGER,
deleted INTEGER,
create_revision INTEGER,
revision INTEGER,
ttl INTEGER,
version INTEGER,
del INTEGER,
old_value BLOB,
old_revision INTEGER,
id INTEGER AUTO_INCREMENT,
prev_revision INTEGER,
lease INTEGER,
value BLOB,
old_value BLOB,
PRIMARY KEY (id)
)`,
);`,
}
nameIdx = "create index name_idx on key_value (name(100))"
revisionIdx = "create index revision_idx on key_value (revision)"
nameIdx = "create index kine_name_index on kine (name(100))"
revisionIdx = "create index kine_name_prev_revision_uindex on kine (name(100), prev_revision)"
createDB = "create database if not exists "
)
func NewMySQL() *driver.Generic {
return &driver.Generic{
CleanupSQL: "DELETE FROM key_value WHERE ttl > 0 AND ttl < ?",
GetSQL: "SELECT id, " + fieldList + " FROM key_value WHERE name = ? ORDER BY revision DESC limit ?",
ListSQL: strings.Replace(strings.Replace(baseList, "%REV%", "", -1), "%RES%", "", -1),
ListRevisionSQL: strings.Replace(strings.Replace(baseList, "%REV%", "WHERE kvi.revision >= ?", -1), "%RES%", "", -1),
ListResumeSQL: strings.Replace(strings.Replace(baseList, "%REV%", "WHERE kvi.revision <= ?", -1),
"%RES%", "and kv.name > ? ", -1),
InsertSQL: insertSQL,
ReplaySQL: "SELECT id, " + fieldList + " FROM key_value WHERE name like ? and revision >= ? ORDER BY revision ASC",
GetRevisionSQL: "SELECT MAX(revision) FROM key_value",
ToDeleteSQL: "SELECT count(*), name, max(revision) FROM key_value GROUP BY name,del HAVING count(*) > 1 or (count(*)=1 and del=1)",
DeleteOldSQL: "DELETE FROM key_value WHERE name = ? AND (revision < ? OR (revision = ? AND del = 1))",
}
}
func Open(dataSourceName string, tlsInfo *transport.TLSInfo) (*sql.DB, error) {
func New(dataSourceName string, tlsInfo tls.Config) (server.Backend, error) {
tlsConfig, err := tlsInfo.ClientConfig()
if err != nil {
return nil, err
}
tlsConfig.MinVersion = tls.VersionTLS11
if len(tlsInfo.CertFile) == 0 && len(tlsInfo.KeyFile) == 0 && len(tlsInfo.CAFile) == 0 {
tlsConfig = nil
if tlsConfig != nil {
tlsConfig.MinVersion = cryptotls.VersionTLS11
}
parsedDSN, err := prepareDSN(dataSourceName, tlsConfig)
if err != nil {
return nil, err
}
if err := createDBIfNotExist(parsedDSN); err != nil {
return nil, err
}
db, err := sql.Open("mysql", parsedDSN)
dialect, err := generic.Open("mysql", parsedDSN, "?", false)
if err != nil {
return nil, err
}
dialect.LastInsertID = true
if err := setup(dialect.DB); err != nil {
return nil, err
}
dialect.Migrate(context.Background())
return logstructured.New(sqllog.New(dialect)), nil
}
func setup(db *sql.DB) error {
for _, stmt := range schema {
_, err := db.Exec(stmt)
if err != nil {
return nil, err
return err
}
}
// check if duplicate indexes
......@@ -107,11 +86,10 @@ func Open(dataSourceName string, tlsInfo *transport.TLSInfo) (*sql.DB, error) {
for _, idx := range indexes {
err := createIndex(db, idx)
if err != nil {
return nil, err
return err
}
}
return db, nil
return nil
}
func createDBIfNotExist(dataSourceName string) error {
......@@ -143,17 +121,7 @@ func createDBIfNotExist(dataSourceName string) error {
return nil
}
func createIndex(db *sql.DB, indexStmt string) error {
_, err := db.Exec(indexStmt)
if err != nil {
if mysqlError, ok := err.(*mysql.MySQLError); !ok || mysqlError.Number != 1061 {
return err
}
}
return nil
}
func prepareDSN(dataSourceName string, tlsConfig *tls.Config) (string, error) {
func prepareDSN(dataSourceName string, tlsConfig *cryptotls.Config) (string, error) {
if len(dataSourceName) == 0 {
dataSourceName = defaultUnixDSN
if tlsConfig != nil {
......@@ -166,8 +134,10 @@ func prepareDSN(dataSourceName string, tlsConfig *tls.Config) (string, error) {
}
// setting up tlsConfig
if tlsConfig != nil {
mysql.RegisterTLSConfig("custom", tlsConfig)
config.TLSConfig = "custom"
if err := mysql.RegisterTLSConfig("kine", tlsConfig); err != nil {
return "", err
}
config.TLSConfig = "kine"
}
dbName := "kubernetes"
if len(config.DBName) > 0 {
......@@ -178,3 +148,13 @@ func prepareDSN(dataSourceName string, tlsConfig *tls.Config) (string, error) {
return parsedDSN, nil
}
func createIndex(db *sql.DB, indexStmt string) error {
_, err := db.Exec(indexStmt)
if err != nil {
if mysqlError, ok := err.(*mysql.MySQLError); !ok || mysqlError.Number != 1061 {
return err
}
}
return nil
}
package pgsql
import (
"context"
"database/sql"
"net/url"
"regexp"
"strconv"
"strings"
"github.com/coreos/etcd/pkg/transport"
"github.com/ibuildthecloud/kvsql/clientv3/driver"
"github.com/lib/pq"
"github.com/rancher/kine/pkg/drivers/generic"
"github.com/rancher/kine/pkg/logstructured"
"github.com/rancher/kine/pkg/logstructured/sqllog"
"github.com/rancher/kine/pkg/server"
"github.com/rancher/kine/pkg/tls"
)
const (
......@@ -17,82 +21,57 @@ const (
)
var (
fieldList = "name, value, old_value, old_revision, create_revision, revision, ttl, version, del"
baseList = `
SELECT kv.id, kv.name, kv.value, kv.old_value, kv.old_revision, kv.create_revision, kv.revision, kv.ttl, kv.version, kv.del
FROM key_value kv
INNER JOIN
(
SELECT MAX(revision) revision, kvi.name
FROM key_value kvi
%REV%
GROUP BY kvi.name
) AS r
ON r.name = kv.name AND r.revision = kv.revision
WHERE kv.name like ? %RES% ORDER BY kv.name ASC limit ?
`
insertSQL = `
INSERT INTO key_value(` + fieldList + `)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
schema = []string{
`create table if not exists key_value
`create table if not exists kine
(
id SERIAL PRIMARY KEY,
name TEXT,
value TEXT,
created INTEGER,
deleted INTEGER,
create_revision INTEGER,
revision INTEGER,
ttl INTEGER,
version INTEGER,
del INTEGER,
old_value TEXT,
id SERIAL PRIMARY KEY,
old_revision INTEGER
prev_revision INTEGER,
lease INTEGER,
value bytea,
old_value bytea
);`,
`create index if not exists name_idx on key_value (name)`,
`create index if not exists revision_idx on key_value (revision)`,
`CREATE INDEX IF NOT EXISTS kine_name_index ON kine (name)`,
`CREATE UNIQUE INDEX IF NOT EXISTS kine_name_prev_revision_uindex ON kine (name, prev_revision)`,
}
createDB = "create database "
)
func NewPGSQL() *driver.Generic {
return &driver.Generic{
CleanupSQL: q("DELETE FROM key_value WHERE ttl > 0 AND ttl < ?"),
GetSQL: q("SELECT id, " + fieldList + " FROM key_value WHERE name=? ORDER BY revision DESC limit ?"),
ListSQL: q(strings.Replace(strings.Replace(baseList, "%REV%", "", -1), "%RES%", "", -1)),
ListRevisionSQL: q(strings.Replace(strings.Replace(baseList, "%REV%", "WHERE kvi.revision>=?", -1), "%RES%", "", -1)),
ListResumeSQL: q(strings.Replace(strings.Replace(baseList, "%REV%", "WHERE kvi.revision<=?", -1),
"%RES%", "and kv.name > ? ", -1)),
InsertSQL: q(insertSQL),
ReplaySQL: q("SELECT id, " + fieldList + " FROM key_value WHERE name like ? and revision>=? ORDER BY revision ASC"),
GetRevisionSQL: q("SELECT MAX(revision) FROM key_value"),
ToDeleteSQL: q("SELECT count(*), name, max(revision) FROM key_value GROUP BY name,del HAVING count(*) > 1 or (count(*)=1 and del=1)"),
DeleteOldSQL: q("DELETE FROM key_value WHERE name=? AND (revision<? OR (revision=? AND del=1))"),
}
}
func Open(dataSourceName string, tlsInfo *transport.TLSInfo) (*sql.DB, error) {
func New(dataSourceName string, tlsInfo tls.Config) (server.Backend, error) {
parsedDSN, err := prepareDSN(dataSourceName, tlsInfo)
if err != nil {
return nil, err
}
// get database name
if err := createDBIfNotExist(parsedDSN); err != nil {
return nil, err
}
db, err := sql.Open("postgres", parsedDSN)
dialect, err := generic.Open("postgres", parsedDSN, "$", true)
if err != nil {
return nil, err
}
if err := setup(dialect.DB); err != nil {
return nil, err
}
dialect.Migrate(context.Background())
return logstructured.New(sqllog.New(dialect)), nil
}
func setup(db *sql.DB) error {
for _, stmt := range schema {
_, err := db.Exec(stmt)
if err != nil {
return nil, err
return err
}
}
return db, nil
return nil
}
func createDBIfNotExist(dataSourceName string) error {
......@@ -100,11 +79,14 @@ func createDBIfNotExist(dataSourceName string) error {
if err != nil {
return err
}
dbName := strings.SplitN(u.Path, "/", 2)[1]
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
return err
}
defer db.Close()
err = db.Ping()
// check if database already exists
if _, ok := err.(*pq.Error); !ok {
......@@ -120,6 +102,7 @@ func createDBIfNotExist(dataSourceName string) error {
if err != nil {
return err
}
defer db.Close()
_, err = db.Exec(createDB + dbName + ";")
if err != nil {
return err
......@@ -138,7 +121,7 @@ func q(sql string) string {
})
}
func prepareDSN(dataSourceName string, tlsInfo *transport.TLSInfo) (string, error) {
func prepareDSN(dataSourceName string, tlsInfo tls.Config) (string, error) {
if len(dataSourceName) == 0 {
dataSourceName = defaultDSN
} else {
......@@ -158,7 +141,7 @@ func prepareDSN(dataSourceName string, tlsInfo *transport.TLSInfo) (string, erro
}
// set up tls dsn
params := url.Values{}
sslmode := "require"
sslmode := ""
if _, ok := queryMap["sslcert"]; tlsInfo.CertFile != "" && !ok {
params.Add("sslcert", tlsInfo.CertFile)
sslmode = "verify-full"
......@@ -171,7 +154,7 @@ func prepareDSN(dataSourceName string, tlsInfo *transport.TLSInfo) (string, erro
params.Add("sslrootcert", tlsInfo.CAFile)
sslmode = "verify-full"
}
if _, ok := queryMap["sslmode"]; !ok {
if _, ok := queryMap["sslmode"]; !ok && sslmode != "" {
params.Add("sslmode", sslmode)
}
for k, v := range queryMap {
......
package sqlite
import (
"context"
"database/sql"
"os"
"github.com/rancher/kine/pkg/drivers/generic"
"github.com/rancher/kine/pkg/logstructured"
"github.com/rancher/kine/pkg/logstructured/sqllog"
"github.com/rancher/kine/pkg/server"
// sqlite db driver
_ "github.com/mattn/go-sqlite3"
)
var (
schema = []string{
`CREATE TABLE IF NOT EXISTS kine
(
id INTEGER primary key autoincrement,
name INTEGER,
created INTEGER,
deleted INTEGER,
create_revision INTEGER,
prev_revision INTEGER,
lease INTEGER,
value BLOB,
old_value BLOB
)`,
`CREATE INDEX IF NOT EXISTS kine_name_index ON kine (name)`,
`CREATE UNIQUE INDEX IF NOT EXISTS kine_name_prev_revision_uindex ON kine (name, prev_revision)`,
}
)
func New(dataSourceName string) (server.Backend, error) {
if dataSourceName == "" {
if err := os.MkdirAll("./db", 0700); err != nil {
return nil, err
}
dataSourceName = "./db/state.db?_journal=WAL&cache=shared"
}
dialect, err := generic.Open("sqlite3", dataSourceName, "?", false)
if err != nil {
return nil, err
}
dialect.LastInsertID = true
if err := setup(dialect.DB); err != nil {
return nil, err
}
dialect.Migrate(context.Background())
return logstructured.New(sqllog.New(dialect)), nil
}
func setup(db *sql.DB) error {
for _, stmt := range schema {
_, err := db.Exec(stmt)
if err != nil {
return err
}
}
return nil
}
package endpoint
import (
"context"
"fmt"
"net"
"os"
"strings"
"github.com/rancher/kine/pkg/drivers/mysql"
"github.com/rancher/kine/pkg/drivers/pgsql"
"github.com/rancher/kine/pkg/drivers/sqlite"
"github.com/rancher/kine/pkg/server"
"github.com/rancher/kine/pkg/tls"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
const (
KineSocket = "unix://kine.sock"
SQLiteBackend = "sqlite"
ETCDBackend = "etcd3"
MySQLBackend = "mysql"
PostgresBackend = "postgres"
)
type Config struct {
GRPCServer *grpc.Server
Listener string
Endpoint string
tls.Config
}
type ETCDConfig struct {
Endpoints []string
TLSConfig tls.Config
LeaderElect bool
}
func Listen(ctx context.Context, config Config) (ETCDConfig, error) {
driver, dsn := ParseStorageEndpoint(config.Endpoint)
if driver == ETCDBackend {
return ETCDConfig{
Endpoints: strings.Split(config.Endpoint, ","),
TLSConfig: config.Config,
LeaderElect: true,
}, nil
}
leaderelect, backend, err := getKineStorageBackend(driver, dsn, config)
if err != nil {
return ETCDConfig{}, err
}
if err := backend.Start(ctx); err != nil {
return ETCDConfig{}, err
}
listen := config.Listener
if listen == "" {
listen = KineSocket
}
b := server.New(backend)
grpcServer := grpcServer(config)
b.Register(grpcServer)
listener, err := createListener(listen)
if err != nil {
return ETCDConfig{}, err
}
go func() {
if err := grpcServer.Serve(listener); err != nil {
logrus.Errorf("Kine server shutdown: %v", err)
}
<-ctx.Done()
grpcServer.Stop()
listener.Close()
}()
return ETCDConfig{
LeaderElect: leaderelect,
Endpoints: []string{listen},
TLSConfig: tls.Config{},
}, nil
}
func createListener(listen string) (ret net.Listener, rerr error) {
network, address := networkAndAddress(listen)
if network == "unix" {
if err := os.Remove(address); err != nil && !os.IsNotExist(err) {
logrus.Warnf("failed to remove socket %s: %v", address, err)
}
defer func() {
if err := os.Chmod(address, 0600); err != nil {
rerr = err
}
}()
}
logrus.Infof("Kine listening on %s://%s", network, address)
return net.Listen(network, address)
}
func grpcServer(config Config) *grpc.Server {
if config.GRPCServer != nil {
return config.GRPCServer
}
return grpc.NewServer()
}
func getKineStorageBackend(driver, dsn string, cfg Config) (bool, server.Backend, error) {
var (
backend server.Backend
leaderElect = true
err error
)
switch driver {
case SQLiteBackend:
leaderElect = false
backend, err = sqlite.New(dsn)
case PostgresBackend:
backend, err = pgsql.New(dsn, cfg.Config)
case MySQLBackend:
backend, err = mysql.New(dsn, cfg.Config)
default:
return false, nil, fmt.Errorf("storage backend is not defined")
}
return leaderElect, backend, err
}
func ParseStorageEndpoint(storageEndpoint string) (string, string) {
network, address := networkAndAddress(storageEndpoint)
switch network {
case "":
return SQLiteBackend, ""
case "http":
fallthrough
case "https":
return ETCDBackend, address
}
return network, address
}
func networkAndAddress(str string) (string, string) {
parts := strings.SplitN(str, "://", 2)
if len(parts) > 1 {
return parts[0], parts[1]
}
return "", parts[0]
}
package logstructured
import (
"context"
"time"
"github.com/rancher/kine/pkg/server"
"github.com/sirupsen/logrus"
)
type Log interface {
Start(ctx context.Context) error
CurrentRevision(ctx context.Context) (int64, error)
List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeletes bool) (int64, []*server.Event, error)
After(ctx context.Context, prefix string, revision int64) (int64, []*server.Event, error)
Watch(ctx context.Context, prefix string) <-chan []*server.Event
Count(ctx context.Context, prefix string) (int64, int64, error)
Append(ctx context.Context, event *server.Event) (int64, error)
}
type LogStructured struct {
log Log
}
func New(log Log) *LogStructured {
return &LogStructured{
log: log,
}
}
func (l *LogStructured) Start(ctx context.Context) error {
if err := l.log.Start(ctx); err != nil {
return err
}
l.Create(ctx, "/registry/health", []byte(`{"health":"true"}`), 0)
go l.ttl(ctx)
return nil
}
func (l *LogStructured) Get(ctx context.Context, key string, revision int64) (revRet int64, kvRet *server.KeyValue, errRet error) {
defer func() {
l.adjustRevision(ctx, &revRet)
logrus.Debugf("GET %s, rev=%d => rev=%d, kv=%v, err=%v", key, revision, revRet, kvRet != nil, errRet)
}()
rev, event, err := l.get(ctx, key, revision, false)
if event == nil {
return rev, nil, err
}
return rev, event.KV, err
}
func (l *LogStructured) get(ctx context.Context, key string, revision int64, includeDeletes bool) (int64, *server.Event, error) {
rev, events, err := l.log.List(ctx, key, "", 1, revision, includeDeletes)
if err == server.ErrCompacted {
// ignore compacted when getting by revision
err = nil
}
if err != nil {
return 0, nil, err
}
if revision != 0 {
rev = revision
}
if len(events) == 0 {
return rev, nil, nil
}
return rev, events[0], nil
}
func (l *LogStructured) adjustRevision(ctx context.Context, rev *int64) {
if *rev != 0 {
return
}
if newRev, err := l.log.CurrentRevision(ctx); err == nil {
*rev = newRev
}
}
func (l *LogStructured) Create(ctx context.Context, key string, value []byte, lease int64) (revRet int64, errRet error) {
defer func() {
l.adjustRevision(ctx, &revRet)
logrus.Debugf("CREATE %s, size=%d, lease=%d => rev=%d, err=%v", key, len(value), lease, revRet, errRet)
}()
rev, prevEvent, err := l.get(ctx, key, 0, true)
if err != nil {
return 0, err
}
createEvent := &server.Event{
Create: true,
KV: &server.KeyValue{
Key: key,
Value: value,
Lease: lease,
},
PrevKV: &server.KeyValue{
ModRevision: rev,
},
}
if prevEvent != nil {
if !prevEvent.Delete {
return 0, server.ErrKeyExists
}
createEvent.PrevKV = prevEvent.KV
}
return l.log.Append(ctx, createEvent)
}
func (l *LogStructured) Delete(ctx context.Context, key string, revision int64) (revRet int64, kvRet *server.KeyValue, deletedRet bool, errRet error) {
defer func() {
l.adjustRevision(ctx, &revRet)
logrus.Debugf("DELETE %s, rev=%d => rev=%d, kv=%v, deleted=%v, err=%v", key, revision, revRet, kvRet != nil, deletedRet, errRet)
}()
rev, event, err := l.get(ctx, key, 0, true)
if err != nil {
return 0, nil, false, err
}
if event == nil {
return rev, nil, true, nil
}
if event.Delete {
return rev, event.KV, true, nil
}
if revision != 0 && event.KV.ModRevision != revision {
return rev, event.KV, false, nil
}
deleteEvent := &server.Event{
Delete: true,
KV: event.KV,
PrevKV: event.KV,
}
rev, err = l.log.Append(ctx, deleteEvent)
return rev, event.KV, true, err
}
func (l *LogStructured) List(ctx context.Context, prefix, startKey string, limit, revision int64) (revRet int64, kvRet []*server.KeyValue, errRet error) {
defer func() {
l.adjustRevision(ctx, &revRet)
logrus.Debugf("LIST %s, start=%s, limit=%d, rev=%d => rev=%d, kvs=%d, err=%v", prefix, startKey, limit, revision, revRet, len(kvRet), errRet)
}()
rev, events, err := l.log.List(ctx, prefix, startKey, limit, revision, false)
if err != nil {
return 0, nil, err
}
if revision != 0 {
rev = revision
}
kvs := make([]*server.KeyValue, 0, len(events))
for _, event := range events {
kvs = append(kvs, event.KV)
}
return rev, kvs, nil
}
func (l *LogStructured) Count(ctx context.Context, prefix string) (revRet int64, count int64, err error) {
defer func() {
l.adjustRevision(ctx, &revRet)
logrus.Debugf("COUNT %s => rev=%d, count=%d, err=%v", prefix, revRet, count, err)
}()
return l.log.Count(ctx, prefix)
}
func (l *LogStructured) Update(ctx context.Context, key string, value []byte, revision, lease int64) (revRet int64, kvRet *server.KeyValue, updateRet bool, errRet error) {
defer func() {
l.adjustRevision(ctx, &revRet)
logrus.Debugf("UPDATE %s, value=%d, rev=%d, lease=%v => rev=%d, kv=%v, updated=%v, err=%v", key, len(value), revision, lease, revRet, kvRet != nil, updateRet, errRet)
}()
rev, event, err := l.get(ctx, key, revision, false)
if err != nil {
return 0, nil, false, err
}
if event == nil {
return 0, nil, false, nil
}
if event.KV.ModRevision != revision {
return rev, event.KV, false, nil
}
updateEvent := &server.Event{
KV: &server.KeyValue{
Key: key,
CreateRevision: event.KV.CreateRevision,
Value: value,
Lease: lease,
},
PrevKV: event.KV,
}
rev, err = l.log.Append(ctx, updateEvent)
if err != nil {
rev, event, err := l.get(ctx, key, 0, false)
if event == nil {
return rev, nil, false, err
}
return rev, event.KV, false, err
}
updateEvent.KV.ModRevision = rev
return rev, updateEvent.KV, true, err
}
func (l *LogStructured) ttl(ctx context.Context) {
// very naive TTL support
for events := range l.log.Watch(ctx, "/") {
for _, event := range events {
if event.KV.Lease <= 0 {
continue
}
go func(event *server.Event) {
select {
case <-ctx.Done():
return
case <-time.After(time.Duration(event.KV.Lease) * time.Second):
}
l.Delete(ctx, event.KV.Key, event.KV.ModRevision)
}(event)
}
}
}
func (l *LogStructured) Watch(ctx context.Context, prefix string, revision int64) <-chan []*server.Event {
logrus.Debugf("WATCH %s, revision=%d", prefix, revision)
// starting watching right away so we don't miss anything
ctx, cancel := context.WithCancel(ctx)
readChan := l.log.Watch(ctx, prefix)
// include the current revision in list
if revision > 0 {
revision -= 1
}
result := make(chan []*server.Event)
rev, kvs, err := l.log.After(ctx, prefix, revision)
if err != nil {
logrus.Errorf("failed to list %s for revision %d", prefix, revision)
cancel()
}
logrus.Debugf("WATCH LIST key=%s rev=%d => rev=%d kvs=%d", prefix, revision, rev, len(kvs))
go func() {
lastRevision := revision
if len(kvs) > 0 {
lastRevision = rev
}
if len(kvs) > 0 {
result <- kvs
}
// always ensure we fully read the channel
for i := range readChan {
result <- filter(i, lastRevision)
}
close(result)
cancel()
}()
return result
}
func filter(events []*server.Event, rev int64) []*server.Event {
for len(events) > 0 && events[0].KV.ModRevision <= rev {
events = events[1:]
}
return events
}
package sqllog
import (
"context"
"database/sql"
"strings"
"time"
"github.com/rancher/kine/pkg/broadcaster"
"github.com/rancher/kine/pkg/server"
"github.com/sirupsen/logrus"
)
type SQLLog struct {
d Dialect
broadcaster broadcaster.Broadcaster
ctx context.Context
notify chan int64
}
func New(d Dialect) *SQLLog {
l := &SQLLog{
d: d,
notify: make(chan int64, 1024),
}
return l
}
type Dialect interface {
ListCurrent(ctx context.Context, prefix string, limit int64, includeDeleted bool) (*sql.Rows, error)
List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeleted bool) (*sql.Rows, error)
Count(ctx context.Context, prefix string) (int64, int64, error)
CurrentRevision(ctx context.Context) (int64, error)
After(ctx context.Context, prefix string, rev int64) (*sql.Rows, error)
Insert(ctx context.Context, key string, create, delete bool, createRevision, previousRevision int64, ttl int64, value, prevValue []byte) (int64, error)
GetRevision(ctx context.Context, revision int64) (*sql.Rows, error)
DeleteRevision(ctx context.Context, revision int64) error
GetCompactRevision(ctx context.Context) (int64, error)
SetCompactRevision(ctx context.Context, revision int64) error
}
func (s *SQLLog) Start(ctx context.Context) error {
s.ctx = ctx
go s.compact()
return nil
}
func (s *SQLLog) compact() {
t := time.NewTicker(2 * time.Second)
outer:
for {
select {
case <-s.ctx.Done():
return
case <-t.C:
}
end, err := s.d.CurrentRevision(s.ctx)
if err != nil {
logrus.Errorf("failed to get current revision: %v", err)
continue
}
cursor, err := s.d.GetCompactRevision(s.ctx)
if err != nil {
logrus.Errorf("failed to get compact revision: %v", err)
continue
}
if end-cursor < 100 {
// Only run if we have at least 100 rows to process
continue
}
savedCursor := cursor
// Purposefully start at the current and redo the current as
// it could have failed before actually compacting
for ; cursor <= end; cursor++ {
rows, err := s.d.GetRevision(s.ctx, cursor)
if err != nil {
logrus.Errorf("failed to get revision %d: %v", cursor, err)
continue outer
}
_, _, events, err := RowsToEvents(rows)
if err != nil {
logrus.Errorf("failed to convert to events: %v", err)
continue outer
}
if len(events) == 0 {
continue
}
event := events[0]
if event.KV.Key == "compact_rev_key" {
// don't compact the compact key
continue
}
setRev := false
if event.PrevKV != nil && event.PrevKV.ModRevision != 0 {
if savedCursor != cursor {
if err := s.d.SetCompactRevision(s.ctx, cursor); err != nil {
logrus.Errorf("failed to record compact revision: %v", err)
continue outer
}
savedCursor = cursor
setRev = true
}
if err := s.d.DeleteRevision(s.ctx, event.PrevKV.ModRevision); err != nil {
logrus.Errorf("failed to delete revision %d: %v", event.PrevKV.ModRevision, err)
continue outer
}
}
if event.Delete {
if !setRev && savedCursor != cursor {
if err := s.d.SetCompactRevision(s.ctx, cursor); err != nil {
logrus.Errorf("failed to record compact revision: %v", err)
continue outer
}
savedCursor = cursor
}
if err := s.d.DeleteRevision(s.ctx, cursor); err != nil {
logrus.Errorf("failed to delete current revision %d: %v", cursor, err)
continue outer
}
}
}
if savedCursor != cursor {
if err := s.d.SetCompactRevision(s.ctx, cursor); err != nil {
logrus.Errorf("failed to record compact revision: %v", err)
continue outer
}
}
}
}
func (s *SQLLog) CurrentRevision(ctx context.Context) (int64, error) {
return s.d.CurrentRevision(ctx)
}
func (s *SQLLog) After(ctx context.Context, prefix string, revision int64) (int64, []*server.Event, error) {
if strings.HasSuffix(prefix, "/") {
prefix += "%"
}
rows, err := s.d.After(ctx, prefix, revision)
if err != nil {
return 0, nil, err
}
rev, _, result, err := RowsToEvents(rows)
return rev, result, err
}
func (s *SQLLog) List(ctx context.Context, prefix, startKey string, limit, revision int64, includeDeleted bool) (int64, []*server.Event, error) {
var (
rows *sql.Rows
err error
)
if strings.HasSuffix(prefix, "/") {
prefix += "%"
}
if revision == 0 {
rows, err = s.d.ListCurrent(ctx, prefix, limit, includeDeleted)
} else {
rows, err = s.d.List(ctx, prefix, startKey, limit, revision, includeDeleted)
}
if err != nil {
return 0, nil, err
}
rev, compact, result, err := RowsToEvents(rows)
if err != nil {
return 0, nil, err
}
if revision > 0 && len(result) == 0 {
// a zero length result won't have the compact revision so get it manually
compact, err = s.d.GetCompactRevision(ctx)
if err != nil {
return 0, nil, err
}
}
if revision > 0 && revision < compact {
return rev, result, server.ErrCompacted
}
select {
case s.notify <- rev:
default:
}
return rev, result, err
}
func RowsToEvents(rows *sql.Rows) (int64, int64, []*server.Event, error) {
var (
result []*server.Event
rev int64
compact int64
)
defer rows.Close()
for rows.Next() {
event := &server.Event{}
if err := scan(rows, &rev, &compact, event); err != nil {
return 0, 0, nil, err
}
result = append(result, event)
}
return rev, compact, result, nil
}
func (s *SQLLog) Watch(ctx context.Context, prefix string) <-chan []*server.Event {
res := make(chan []*server.Event)
values, err := s.broadcaster.Subscribe(ctx, s.startWatch)
if err != nil {
return nil
}
checkPrefix := strings.HasSuffix(prefix, "/")
go func() {
defer close(res)
for i := range values {
events, ok := filter(i, checkPrefix, prefix)
if ok {
res <- events
}
}
}()
return res
}
func filter(events interface{}, checkPrefix bool, prefix string) ([]*server.Event, bool) {
eventList := events.([]*server.Event)
filteredEventList := make([]*server.Event, 0, len(eventList))
for _, event := range eventList {
if (checkPrefix && strings.HasPrefix(event.KV.Key, prefix)) || event.KV.Key == prefix {
filteredEventList = append(filteredEventList, event)
}
}
return filteredEventList, len(filteredEventList) > 0
}
func (s *SQLLog) startWatch() (chan interface{}, error) {
c := make(chan interface{})
go s.poll(c)
return c, nil
}
func (s *SQLLog) poll(result chan interface{}) {
var (
last int64
)
wait := time.NewTicker(120 * time.Second)
defer wait.Stop()
defer close(result)
for {
select {
case <-s.ctx.Done():
return
case check := <-s.notify:
if check <= last {
continue
}
case <-wait.C:
}
rows, err := s.d.After(s.ctx, "%", last)
if err != nil {
logrus.Errorf("fail to list latest changes: %v", err)
continue
}
rev, _, events, err := RowsToEvents(rows)
if err != nil {
logrus.Errorf("fail to convert rows changes: %v", err)
continue
}
if len(events) == 0 {
continue
}
for _, event := range events {
logrus.Debugf("TRIGGERED %s, revision=%d, delete=%v", event.KV.Key, event.KV.ModRevision, event.Delete)
}
result <- events
last = rev
}
}
func (s *SQLLog) Count(ctx context.Context, prefix string) (int64, int64, error) {
if strings.HasSuffix(prefix, "/") {
prefix += "%"
}
return s.d.Count(ctx, prefix)
}
func (s *SQLLog) Append(ctx context.Context, event *server.Event) (int64, error) {
e := *event
if e.KV == nil {
e.KV = &server.KeyValue{}
}
if e.PrevKV == nil {
e.PrevKV = &server.KeyValue{}
}
rev, err := s.d.Insert(ctx, e.KV.Key,
e.Create,
e.Delete,
e.KV.CreateRevision,
e.PrevKV.ModRevision,
e.KV.Lease,
e.KV.Value,
e.PrevKV.Value,
)
if err != nil {
return 0, err
}
select {
case s.notify <- rev:
default:
}
return rev, nil
}
func scan(rows *sql.Rows, rev *int64, compact *int64, event *server.Event) error {
event.KV = &server.KeyValue{}
event.PrevKV = &server.KeyValue{}
c := &sql.NullInt64{}
err := rows.Scan(
rev,
c,
&event.KV.ModRevision,
&event.KV.Key,
&event.Create,
&event.Delete,
&event.KV.CreateRevision,
&event.PrevKV.ModRevision,
&event.KV.Lease,
&event.KV.Value,
&event.PrevKV.Value,
)
if err != nil {
return err
}
if event.Create {
event.KV.CreateRevision = event.KV.ModRevision
event.PrevKV = nil
}
*compact = c.Int64
return nil
}
package server
import (
"context"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func isCompact(txn *etcdserverpb.TxnRequest) bool {
return len(txn.Compare) == 1 &&
txn.Compare[0].Target == etcdserverpb.Compare_VERSION &&
txn.Compare[0].Result == etcdserverpb.Compare_EQUAL &&
len(txn.Success) == 1 &&
txn.Success[0].GetRequestPut() != nil &&
len(txn.Failure) == 1 &&
txn.Failure[0].GetRequestRange() != nil &&
string(txn.Compare[0].Key) == "compact_rev_key"
}
func (l *LimitedServer) compact(ctx context.Context) (*etcdserverpb.TxnResponse, error) {
return &etcdserverpb.TxnResponse{
Header: &etcdserverpb.ResponseHeader{},
Succeeded: true,
Responses: []*etcdserverpb.ResponseOp{
{
Response: &etcdserverpb.ResponseOp_ResponsePut{
ResponsePut: &etcdserverpb.PutResponse{
Header: &etcdserverpb.ResponseHeader{},
},
},
},
},
}, nil
}
package server
import (
"context"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func isCreate(txn *etcdserverpb.TxnRequest) *etcdserverpb.PutRequest {
if len(txn.Compare) == 1 &&
txn.Compare[0].Target == etcdserverpb.Compare_MOD &&
txn.Compare[0].Result == etcdserverpb.Compare_EQUAL &&
txn.Compare[0].GetModRevision() == 0 &&
len(txn.Failure) == 0 &&
len(txn.Success) == 1 &&
txn.Success[0].GetRequestPut() != nil {
return txn.Success[0].GetRequestPut()
}
return nil
}
func (l *LimitedServer) create(ctx context.Context, put *etcdserverpb.PutRequest, txn *etcdserverpb.TxnRequest) (*etcdserverpb.TxnResponse, error) {
if put.IgnoreLease {
return nil, unsupported("ignoreLease")
} else if put.IgnoreValue {
return nil, unsupported("ignoreValue")
} else if put.PrevKv {
return nil, unsupported("prevKv")
}
rev, err := l.backend.Create(ctx, string(put.Key), put.Value, put.Lease)
if err == ErrKeyExists {
return &etcdserverpb.TxnResponse{
Header: txnHeader(rev),
Succeeded: false,
}, nil
} else if err != nil {
return nil, err
}
return &etcdserverpb.TxnResponse{
Header: txnHeader(rev),
Responses: []*etcdserverpb.ResponseOp{
{
Response: &etcdserverpb.ResponseOp_ResponsePut{
ResponsePut: &etcdserverpb.PutResponse{
Header: txnHeader(rev),
},
},
},
},
Succeeded: true,
}, nil
}
package server
import (
"context"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func isDelete(txn *etcdserverpb.TxnRequest) (int64, string, bool) {
if len(txn.Compare) == 0 &&
len(txn.Failure) == 0 &&
len(txn.Success) == 2 &&
txn.Success[0].GetRequestRange() != nil &&
txn.Success[1].GetRequestDeleteRange() != nil {
rng := txn.Success[1].GetRequestDeleteRange()
return 0, string(rng.Key), true
}
if len(txn.Compare) == 1 &&
txn.Compare[0].Target == etcdserverpb.Compare_MOD &&
txn.Compare[0].Result == etcdserverpb.Compare_EQUAL &&
len(txn.Failure) == 1 &&
txn.Failure[0].GetRequestRange() != nil &&
len(txn.Success) == 1 &&
txn.Success[0].GetRequestDeleteRange() != nil {
return txn.Compare[0].GetModRevision(), string(txn.Success[0].GetRequestDeleteRange().Key), true
}
return 0, "", false
}
func (l *LimitedServer) delete(ctx context.Context, key string, revision int64) (*etcdserverpb.TxnResponse, error) {
rev, kv, ok, err := l.backend.Delete(ctx, key, revision)
if err != nil {
return nil, err
}
return &etcdserverpb.TxnResponse{
Header: txnHeader(rev),
Responses: []*etcdserverpb.ResponseOp{
{
Response: &etcdserverpb.ResponseOp_ResponseRange{
ResponseRange: &etcdserverpb.RangeResponse{
Header: txnHeader(rev),
Kvs: toKVs(kv),
},
},
},
},
Succeeded: ok,
}, nil
}
package server
import (
"context"
"fmt"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func (l *LimitedServer) get(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
if r.Limit != 0 {
return nil, fmt.Errorf("invalid combination of rangeEnd and limit, limit should be 0 got %d", r.Limit)
}
rev, kv, err := l.backend.Get(ctx, string(r.Key), r.Revision)
if err != nil {
return nil, err
}
resp := &RangeResponse{
Header: txnHeader(rev),
}
if kv != nil {
resp.Kvs = []*KeyValue{kv}
}
return resp, nil
}
package server
import (
"context"
"fmt"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func (s *KVServerBridge) LeaseGrant(ctx context.Context, req *etcdserverpb.LeaseGrantRequest) (*etcdserverpb.LeaseGrantResponse, error) {
return &etcdserverpb.LeaseGrantResponse{
Header: &etcdserverpb.ResponseHeader{},
ID: req.TTL,
TTL: req.TTL,
}, nil
}
func (s *KVServerBridge) LeaseRevoke(context.Context, *etcdserverpb.LeaseRevokeRequest) (*etcdserverpb.LeaseRevokeResponse, error) {
return nil, fmt.Errorf("lease revoke is not supported")
}
func (s *KVServerBridge) LeaseKeepAlive(etcdserverpb.Lease_LeaseKeepAliveServer) error {
return fmt.Errorf("lease keep alive is not supported")
}
func (s *KVServerBridge) LeaseTimeToLive(context.Context, *etcdserverpb.LeaseTimeToLiveRequest) (*etcdserverpb.LeaseTimeToLiveResponse, error) {
return nil, fmt.Errorf("lease time to live is not supported")
}
func (s *KVServerBridge) LeaseLeases(context.Context, *etcdserverpb.LeaseLeasesRequest) (*etcdserverpb.LeaseLeasesResponse, error) {
return nil, fmt.Errorf("lease leases is not supported")
}
package server
import (
"context"
"fmt"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
type LimitedServer struct {
backend Backend
}
func (l *LimitedServer) Range(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
if len(r.RangeEnd) == 0 {
return l.get(ctx, r)
}
return l.list(ctx, r)
}
func txnHeader(rev int64) *etcdserverpb.ResponseHeader {
return &etcdserverpb.ResponseHeader{
Revision: rev,
}
}
func (l *LimitedServer) Txn(ctx context.Context, txn *etcdserverpb.TxnRequest) (*etcdserverpb.TxnResponse, error) {
if put := isCreate(txn); put != nil {
return l.create(ctx, put, txn)
}
if rev, key, ok := isDelete(txn); ok {
return l.delete(ctx, key, rev)
}
if rev, key, value, lease, ok := isUpdate(txn); ok {
return l.update(ctx, rev, key, value, lease)
}
if isCompact(txn) {
return l.compact(ctx)
}
return nil, fmt.Errorf("unsupported transaction: %v", txn)
}
type ResponseHeader struct {
Revision int64
}
type RangeResponse struct {
Header *etcdserverpb.ResponseHeader
Kvs []*KeyValue
More bool
Count int64
}
package server
import (
"bytes"
"context"
"fmt"
"strings"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func (l *LimitedServer) list(ctx context.Context, r *etcdserverpb.RangeRequest) (*RangeResponse, error) {
if len(r.RangeEnd) == 0 {
return nil, fmt.Errorf("invalid range end length of 0")
}
prefix := string(append(r.RangeEnd[:len(r.RangeEnd)-1], r.RangeEnd[len(r.RangeEnd)-1]-1))
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
}
start := string(bytes.TrimRight(r.Key, "\x00"))
if r.CountOnly {
rev, count, err := l.backend.Count(ctx, prefix)
if err != nil {
return nil, err
}
return &RangeResponse{
Header: txnHeader(rev),
Count: count,
}, nil
}
limit := r.Limit
if limit > 0 {
limit++
}
rev, kvs, err := l.backend.List(ctx, prefix, start, limit, r.Revision)
if err != nil {
return nil, err
}
resp := &RangeResponse{
Header: txnHeader(rev),
Count: int64(len(kvs)),
Kvs: kvs,
}
if limit > 0 && resp.Count > limit {
resp.More = true
resp.Kvs = kvs[0 : limit-1]
}
return resp, nil
}
package server
import (
"context"
"fmt"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/mvcc/mvccpb"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
var (
_ etcdserverpb.KVServer = (*KVServerBridge)(nil)
_ etcdserverpb.WatchServer = (*KVServerBridge)(nil)
)
type KVServerBridge struct {
limited *LimitedServer
}
func New(backend Backend) *KVServerBridge {
return &KVServerBridge{
limited: &LimitedServer{
backend: backend,
},
}
}
func (k *KVServerBridge) Register(server *grpc.Server) {
etcdserverpb.RegisterLeaseServer(server, k)
etcdserverpb.RegisterWatchServer(server, k)
etcdserverpb.RegisterKVServer(server, k)
hsrv := health.NewServer()
hsrv.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthpb.RegisterHealthServer(server, hsrv)
}
func (k *KVServerBridge) Range(ctx context.Context, r *etcdserverpb.RangeRequest) (*etcdserverpb.RangeResponse, error) {
if r.KeysOnly {
return nil, unsupported("keysOnly")
}
if r.MaxCreateRevision != 0 {
return nil, unsupported("maxCreateRevision")
}
if r.SortOrder != 0 {
return nil, unsupported("sortOrder")
}
if r.SortTarget != 0 {
return nil, unsupported("sortTarget")
}
if r.Serializable {
return nil, unsupported("serializable")
}
if r.KeysOnly {
return nil, unsupported("keysOnly")
}
if r.MinModRevision != 0 {
return nil, unsupported("minModRevision")
}
if r.MinCreateRevision != 0 {
return nil, unsupported("minCreateRevision")
}
if r.MaxCreateRevision != 0 {
return nil, unsupported("maxCreateRevision")
}
if r.MaxModRevision != 0 {
return nil, unsupported("maxModRevision")
}
resp, err := k.limited.Range(ctx, r)
if err != nil {
logrus.Errorf("error while range on %s %s: %v", r.Key, r.RangeEnd, err)
return nil, err
}
rangeResponse := &etcdserverpb.RangeResponse{
More: resp.More,
Count: resp.Count,
Header: resp.Header,
Kvs: toKVs(resp.Kvs...),
}
return rangeResponse, nil
}
func toKVs(kvs ...*KeyValue) []*mvccpb.KeyValue {
if len(kvs) == 0 || kvs[0] == nil {
return nil
}
ret := make([]*mvccpb.KeyValue, 0, len(kvs))
for _, kv := range kvs {
newKV := toKV(kv)
if newKV == nil {
fmt.Println("HIHIHIH")
} else {
ret = append(ret, toKV(kv))
}
}
return ret
}
func toKV(kv *KeyValue) *mvccpb.KeyValue {
if kv == nil {
return nil
}
return &mvccpb.KeyValue{
Key: []byte(kv.Key),
Value: kv.Value,
Lease: kv.Lease,
CreateRevision: kv.CreateRevision,
ModRevision: kv.ModRevision,
}
}
func (k *KVServerBridge) Put(ctx context.Context, r *etcdserverpb.PutRequest) (*etcdserverpb.PutResponse, error) {
return nil, fmt.Errorf("put is not supported")
}
func (k *KVServerBridge) DeleteRange(ctx context.Context, r *etcdserverpb.DeleteRangeRequest) (*etcdserverpb.DeleteRangeResponse, error) {
return nil, fmt.Errorf("delete is not supported")
}
func (k *KVServerBridge) Txn(ctx context.Context, r *etcdserverpb.TxnRequest) (*etcdserverpb.TxnResponse, error) {
res, err := k.limited.Txn(ctx, r)
if err != nil {
logrus.Errorf("error in txn: %v", err)
}
return res, err
}
func (k *KVServerBridge) Compact(ctx context.Context, r *etcdserverpb.CompactionRequest) (*etcdserverpb.CompactionResponse, error) {
return &etcdserverpb.CompactionResponse{
Header: &etcdserverpb.ResponseHeader{
Revision: r.Revision,
},
}, nil
}
func unsupported(field string) error {
return fmt.Errorf("%s is unsupported", field)
}
package server
import (
"context"
"github.com/pkg/errors"
)
var (
ErrKeyExists = errors.New("key exists")
ErrCompacted = errors.New("revision has been compact")
)
type Backend interface {
Start(ctx context.Context) error
Get(ctx context.Context, key string, revision int64) (int64, *KeyValue, error)
Create(ctx context.Context, key string, value []byte, lease int64) (int64, error)
Delete(ctx context.Context, key string, revision int64) (int64, *KeyValue, bool, error)
List(ctx context.Context, prefix, startKey string, limit, revision int64) (int64, []*KeyValue, error)
Count(ctx context.Context, prefix string) (int64, int64, error)
Update(ctx context.Context, key string, value []byte, revision, lease int64) (int64, *KeyValue, bool, error)
Watch(ctx context.Context, key string, revision int64) <-chan []*Event
}
type KeyValue struct {
Key string
CreateRevision int64
ModRevision int64
Value []byte
Lease int64
}
type Event struct {
Delete bool
Create bool
KV *KeyValue
PrevKV *KeyValue
}
package server
import (
"context"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
)
func isUpdate(txn *etcdserverpb.TxnRequest) (int64, string, []byte, int64, bool) {
if len(txn.Compare) == 1 &&
txn.Compare[0].Target == etcdserverpb.Compare_MOD &&
txn.Compare[0].Result == etcdserverpb.Compare_EQUAL &&
len(txn.Success) == 1 &&
txn.Success[0].GetRequestPut() != nil &&
len(txn.Failure) == 1 &&
txn.Failure[0].GetRequestRange() != nil {
return txn.Compare[0].GetModRevision(),
string(txn.Compare[0].Key),
txn.Success[0].GetRequestPut().Value,
txn.Success[0].GetRequestPut().Lease,
true
}
return 0, "", nil, 0, false
}
func (l *LimitedServer) update(ctx context.Context, rev int64, key string, value []byte, lease int64) (*etcdserverpb.TxnResponse, error) {
var (
kv *KeyValue
ok bool
err error
)
if rev == 0 {
rev, err = l.backend.Create(ctx, key, value, lease)
ok = true
} else {
rev, kv, ok, err = l.backend.Update(ctx, key, value, rev, lease)
}
if err != nil {
return nil, err
}
resp := &etcdserverpb.TxnResponse{
Header: txnHeader(rev),
Succeeded: ok,
}
if ok {
resp.Responses = []*etcdserverpb.ResponseOp{
{
Response: &etcdserverpb.ResponseOp_ResponsePut{
ResponsePut: &etcdserverpb.PutResponse{
Header: txnHeader(rev),
},
},
},
}
} else {
resp.Responses = []*etcdserverpb.ResponseOp{
{
Response: &etcdserverpb.ResponseOp_ResponseRange{
ResponseRange: &etcdserverpb.RangeResponse{
Header: txnHeader(rev),
Kvs: toKVs(kv),
},
},
},
}
}
return resp, nil
}
package server
import (
"context"
"sync"
"sync/atomic"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
"github.com/coreos/etcd/mvcc/mvccpb"
"github.com/sirupsen/logrus"
)
var (
watchID int64
)
func (s *KVServerBridge) Watch(ws etcdserverpb.Watch_WatchServer) error {
w := watcher{
server: ws,
backend: s.limited.backend,
watches: map[int64]func(){},
}
defer w.Close()
for {
msg, err := ws.Recv()
if err != nil {
return err
}
if msg.GetCreateRequest() != nil {
w.Start(msg.GetCreateRequest())
} else if msg.GetCancelRequest() != nil {
w.Cancel(msg.GetCancelRequest().WatchId)
}
}
}
type watcher struct {
sync.Mutex
wg sync.WaitGroup
backend Backend
server etcdserverpb.Watch_WatchServer
watches map[int64]func()
}
func (w *watcher) Start(r *etcdserverpb.WatchCreateRequest) {
w.Lock()
defer w.Unlock()
ctx, cancel := context.WithCancel(context.Background())
id := atomic.AddInt64(&watchID, 1)
w.watches[id] = cancel
w.wg.Add(1)
key := string(r.Key)
logrus.Debugf("WATCH START id=%d, key=%s, revision=%d", id, key, r.StartRevision)
go func() {
defer w.wg.Done()
if err := w.server.Send(&etcdserverpb.WatchResponse{
Header: &etcdserverpb.ResponseHeader{},
Created: true,
WatchId: id,
}); err != nil {
w.Cancel(id)
return
}
for events := range w.backend.Watch(ctx, key, r.StartRevision) {
if len(events) == 0 {
continue
}
if logrus.IsLevelEnabled(logrus.DebugLevel) {
for _, event := range events {
logrus.Debugf("WATCH READ id=%d, key=%s, revision=%d", id, event.KV.Key, event.KV.ModRevision)
}
}
if err := w.server.Send(&etcdserverpb.WatchResponse{
Header: txnHeader(events[len(events)-1].KV.ModRevision),
WatchId: id,
Events: toEvents(events...),
}); err != nil {
w.Cancel(id)
return
}
}
logrus.Debugf("WATCH CLOSE id=%d, key=%s", id, key)
}()
}
func toEvents(events ...*Event) []*mvccpb.Event {
ret := make([]*mvccpb.Event, 0, len(events))
for _, e := range events {
ret = append(ret, toEvent(e))
}
return ret
}
func toEvent(event *Event) *mvccpb.Event {
e := &mvccpb.Event{
Kv: toKV(event.KV),
PrevKv: toKV(event.PrevKV),
}
if event.Delete {
e.Type = mvccpb.DELETE
} else {
e.Type = mvccpb.PUT
}
return e
}
func (w *watcher) Cancel(watchID int64) {
w.Lock()
defer w.Unlock()
if cancel, ok := w.watches[watchID]; ok {
cancel()
delete(w.watches, watchID)
}
err := w.server.Send(&etcdserverpb.WatchResponse{
Header: &etcdserverpb.ResponseHeader{},
Canceled: true,
WatchId: watchID,
})
if err != nil {
logrus.Errorf("Failed to send cancel response for watchID %d: %v", watchID, err)
}
}
func (w *watcher) Close() {
w.Lock()
defer w.Unlock()
for _, v := range w.watches {
v()
}
w.wg.Wait()
}
package tls
import (
"crypto/tls"
"github.com/coreos/etcd/pkg/transport"
)
type Config struct {
CAFile string
CertFile string
KeyFile string
}
func (c Config) ClientConfig() (*tls.Config, error) {
if c.CertFile == "" && c.KeyFile == "" && c.CAFile == "" {
return nil, nil
}
info := &transport.TLSInfo{
CertFile: c.CertFile,
KeyFile: c.KeyFile,
CAFile: c.CAFile,
}
tlsConfig, err := info.ClientConfig()
if err != nil {
return nil, err
}
return tlsConfig, nil
}
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- tip
go_import_path: github.com/sirupsen/logrus
git:
depth: 1
env:
- GOMAXPROCS=4 GORACE=halt_on_error=1
- GO111MODULE=on
- GO111MODULE=off
go: [ 1.10.x, 1.11.x, 1.12.x ]
os: [ linux, osx, windows ]
matrix:
exclude:
- env: GO111MODULE=on
go: 1.10.x
install:
- go get github.com/stretchr/testify/assert
- go get gopkg.in/gemnasium/logrus-airbrake-hook.v2
- go get golang.org/x/sys/unix
- go get golang.org/x/sys/windows
- if [[ "$GO111MODULE" == "on" ]]; then go mod download; fi
- if [[ "$GO111MODULE" == "off" ]]; then go get github.com/stretchr/testify/assert golang.org/x/sys/unix github.com/konsorten/go-windows-terminal-sequences; fi
script:
- export GOMAXPROCS=4
- export GORACE=halt_on_error=1
- go test -race -v ./...
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then go test -race -v -tags appengine ./... ; fi
# 1.4.1
This new release introduces:
* Enhance TextFormatter to not print caller information when they are empty (#944)
* Remove dependency on golang.org/x/crypto (#932, #943)
Fixes:
* Fix Entry.WithContext method to return a copy of the initial entry (#941)
# 1.4.0
This new release introduces:
* Add `DeferExitHandler`, similar to `RegisterExitHandler` but prepending the handler to the list of handlers (semantically like `defer`) (#848).
* Add `CallerPrettyfier` to `JSONFormatter` and `TextFormatter (#909, #911)
* Add `Entry.WithContext()` and `Entry.Context`, to set a context on entries to be used e.g. in hooks (#919).
Fixes:
* Fix wrong method calls `Logger.Print` and `Logger.Warningln` (#893).
* Update `Entry.Logf` to not do string formatting unless the log level is enabled (#903)
* Fix infinite recursion on unknown `Level.String()` (#907)
* Fix race condition in `getCaller` (#916).
# 1.3.0
This new release introduces:
* Log, Logf, Logln functions for Logger and Entry that take a Level
Fixes:
* Building prometheus node_exporter on AIX (#840)
* Race condition in TextFormatter (#468)
* Travis CI import path (#868)
* Remove coloured output on Windows (#862)
* Pointer to func as field in JSONFormatter (#870)
* Properly marshal Levels (#873)
# 1.2.0
This new release introduces:
* A new method `SetReportCaller` in the `Logger` to enable the file, line and calling function from which the trace has been issued
* A new trace level named `Trace` whose level is below `Debug`
* A configurable exit function to be called upon a Fatal trace
* The `Level` object now implements `encoding.TextUnmarshaler` interface
# 1.1.1
This is a bug fix release.
* fix the build break on Solaris
* don't drop a whole trace in JSONFormatter when a field param is a function pointer which can not be serialized
# 1.1.0
This new release introduces:
* several fixes:
* a fix for a race condition on entry formatting
* proper cleanup of previously used entries before putting them back in the pool
* the extra new line at the end of message in text formatter has been removed
* a new global public API to check if a level is activated: IsLevelEnabled
* the following methods have been added to the Logger object
* IsLevelEnabled
* SetFormatter
* SetOutput
* ReplaceHooks
* introduction of go module
* an indent configuration for the json formatter
* output colour support for windows
* the field sort function is now configurable for text formatter
* the CLICOLOR and CLICOLOR\_FORCE environment variable support in text formater
# 1.0.6
This new release introduces:
* a new api WithTime which allows to easily force the time of the log entry
which is mostly useful for logger wrapper
* a fix reverting the immutability of the entry given as parameter to the hooks
a new configuration field of the json formatter in order to put all the fields
in a nested dictionnary
* a new SetOutput method in the Logger
* a new configuration of the textformatter to configure the name of the default keys
* a new configuration of the text formatter to disable the level truncation
# 1.0.5
* Fix hooks race (#707)
* Fix panic deadlock (#695)
# 1.0.4
* Fix race when adding hooks (#612)
* Fix terminal check in AppEngine (#635)
# 1.0.3
* Replace example files with testable examples
......
......@@ -51,9 +51,9 @@ func Exit(code int) {
os.Exit(code)
}
// RegisterExitHandler adds a Logrus Exit handler, call logrus.Exit to invoke
// all handlers. The handlers will also be invoked when any Fatal log entry is
// made.
// RegisterExitHandler appends a Logrus Exit handler to the list of handlers,
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
// any Fatal log entry is made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
......@@ -62,3 +62,15 @@ func Exit(code int) {
func RegisterExitHandler(handler func()) {
handlers = append(handlers, handler)
}
// DeferExitHandler prepends a Logrus Exit handler to the list of handlers,
// call logrus.Exit to invoke all handlers. The handlers will also be invoked when
// any Fatal log entry is made.
//
// This method is useful when a caller wishes to use logrus to log a fatal
// message but also needs to gracefully shutdown. An example usecase could be
// closing database connections, or sending a alert that the application is
// closing.
func DeferExitHandler(handler func()) {
handlers = append([]func(){handler}, handlers...)
}
package logrus
import (
"context"
"io"
"time"
)
var (
......@@ -15,37 +17,38 @@ func StandardLogger() *Logger {
// SetOutput sets the standard logger output.
func SetOutput(out io.Writer) {
std.mu.Lock()
defer std.mu.Unlock()
std.Out = out
std.SetOutput(out)
}
// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
std.mu.Lock()
defer std.mu.Unlock()
std.Formatter = formatter
std.SetFormatter(formatter)
}
// SetReportCaller sets whether the standard logger will include the calling
// method as a field.
func SetReportCaller(include bool) {
std.SetReportCaller(include)
}
// SetLevel sets the standard logger level.
func SetLevel(level Level) {
std.mu.Lock()
defer std.mu.Unlock()
std.SetLevel(level)
}
// GetLevel returns the standard logger level.
func GetLevel() Level {
std.mu.Lock()
defer std.mu.Unlock()
return std.level()
return std.GetLevel()
}
// IsLevelEnabled checks if the log level of the standard logger is greater than the level param
func IsLevelEnabled(level Level) bool {
return std.IsLevelEnabled(level)
}
// AddHook adds a hook to the standard logger hooks.
func AddHook(hook Hook) {
std.mu.Lock()
defer std.mu.Unlock()
std.Hooks.Add(hook)
std.AddHook(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
......@@ -53,6 +56,11 @@ func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
}
// WithContext creates an entry from the standard logger and adds a context to it.
func WithContext(ctx context.Context) *Entry {
return std.WithContext(ctx)
}
// WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`.
//
......@@ -72,6 +80,20 @@ func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// WithTime creats an entry from the standard logger and overrides the time of
// logs generated with it.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithTime(t time.Time) *Entry {
return std.WithTime(t)
}
// Trace logs a message at level Trace on the standard logger.
func Trace(args ...interface{}) {
std.Trace(args...)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
std.Debug(args...)
......@@ -107,11 +129,16 @@ func Panic(args ...interface{}) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger.
// Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatal(args ...interface{}) {
std.Fatal(args...)
}
// Tracef logs a message at level Trace on the standard logger.
func Tracef(format string, args ...interface{}) {
std.Tracef(format, args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...)
......@@ -147,11 +174,16 @@ func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger.
// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...)
}
// Traceln logs a message at level Trace on the standard logger.
func Traceln(args ...interface{}) {
std.Traceln(args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
std.Debugln(args...)
......@@ -187,7 +219,7 @@ func Panicln(args ...interface{}) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger.
// Fatalln logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
func Fatalln(args ...interface{}) {
std.Fatalln(args...)
}
......@@ -2,7 +2,16 @@ package logrus
import "time"
const defaultTimestampFormat = time.RFC3339
// Default key names for the default fields
const (
defaultTimestampFormat = time.RFC3339
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
FieldKeyLogrusError = "logrus_error"
FieldKeyFunc = "func"
FieldKeyFile = "file"
)
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
......@@ -18,7 +27,7 @@ type Formatter interface {
Format(*Entry) ([]byte, error)
}
// This is to not silently overwrite `time`, `msg` and `level` fields when
// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
......@@ -30,16 +39,40 @@ type Formatter interface {
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) {
if t, ok := data["time"]; ok {
data["fields.time"] = t
func prefixFieldClashes(data Fields, fieldMap FieldMap, reportCaller bool) {
timeKey := fieldMap.resolve(FieldKeyTime)
if t, ok := data[timeKey]; ok {
data["fields."+timeKey] = t
delete(data, timeKey)
}
if m, ok := data["msg"]; ok {
data["fields.msg"] = m
msgKey := fieldMap.resolve(FieldKeyMsg)
if m, ok := data[msgKey]; ok {
data["fields."+msgKey] = m
delete(data, msgKey)
}
if l, ok := data["level"]; ok {
data["fields.level"] = l
levelKey := fieldMap.resolve(FieldKeyLevel)
if l, ok := data[levelKey]; ok {
data["fields."+levelKey] = l
delete(data, levelKey)
}
logrusErrKey := fieldMap.resolve(FieldKeyLogrusError)
if l, ok := data[logrusErrKey]; ok {
data["fields."+logrusErrKey] = l
delete(data, logrusErrKey)
}
// If reportCaller is not set, 'func' will not conflict.
if reportCaller {
funcKey := fieldMap.resolve(FieldKeyFunc)
if l, ok := data[funcKey]; ok {
data["fields."+funcKey] = l
}
fileKey := fieldMap.resolve(FieldKeyFile)
if l, ok := data[fileKey]; ok {
data["fields."+fileKey] = l
}
}
}
module github.com/sirupsen/logrus
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.1
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33
)
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe h1:CHRGQ8V7OlCYtwaKPJi3iA7J+YdNKdo8j7nG5IgDhjs=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
package logrus
import (
"bytes"
"encoding/json"
"fmt"
"runtime"
)
type fieldKey string
......@@ -10,13 +12,6 @@ type fieldKey string
// FieldMap allows customization of the key names for default fields.
type FieldMap map[fieldKey]string
// Default key names for the default fields
const (
FieldKeyMsg = "msg"
FieldKeyLevel = "level"
FieldKeyTime = "time"
)
func (f FieldMap) resolve(key fieldKey) string {
if k, ok := f[key]; ok {
return k
......@@ -33,21 +28,34 @@ type JSONFormatter struct {
// DisableTimestamp allows disabling automatic timestamps in output
DisableTimestamp bool
// DataKey allows users to put all the log entry parameters into a nested dictionary at a given key.
DataKey string
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &JSONFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message",
// FieldKeyMsg: "@message",
// FieldKeyFunc: "@caller",
// },
// }
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the json data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from json fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)
// PrettyPrint will indent all json logs
PrettyPrint bool
}
// Format renders a single log entry
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3)
data := make(Fields, len(entry.Data)+4)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
......@@ -58,22 +66,56 @@ func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data[k] = v
}
}
prefixFieldClashes(data)
if f.DataKey != "" {
newData := make(Fields, 4)
newData[f.DataKey] = data
data = newData
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if entry.err != "" {
data[f.FieldMap.resolve(FieldKeyLogrusError)] = entry.err
}
if !f.DisableTimestamp {
data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat)
}
data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message
data[f.FieldMap.resolve(FieldKeyLevel)] = entry.Level.String()
if entry.HasCaller() {
funcVal := entry.Caller.Function
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if funcVal != "" {
data[f.FieldMap.resolve(FieldKeyFunc)] = funcVal
}
if fileVal != "" {
data[f.FieldMap.resolve(FieldKeyFile)] = fileVal
}
}
serialized, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
return append(serialized, '\n'), nil
encoder := json.NewEncoder(b)
if f.PrettyPrint {
encoder.SetIndent("", " ")
}
if err := encoder.Encode(data); err != nil {
return nil, fmt.Errorf("failed to marshal fields to JSON, %v", err)
}
return b.Bytes(), nil
}
......@@ -14,22 +14,11 @@ type Level uint32
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string {
switch level {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
if b, err := level.MarshalText(); err == nil {
return string(b)
} else {
return "unknown"
}
return "unknown"
}
// ParseLevel takes a string level and returns the Logrus log level constant.
......@@ -47,12 +36,47 @@ func ParseLevel(lvl string) (Level, error) {
return InfoLevel, nil
case "debug":
return DebugLevel, nil
case "trace":
return TraceLevel, nil
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (level *Level) UnmarshalText(text []byte) error {
l, err := ParseLevel(string(text))
if err != nil {
return err
}
*level = Level(l)
return nil
}
func (level Level) MarshalText() ([]byte, error) {
switch level {
case TraceLevel:
return []byte("trace"), nil
case DebugLevel:
return []byte("debug"), nil
case InfoLevel:
return []byte("info"), nil
case WarnLevel:
return []byte("warning"), nil
case ErrorLevel:
return []byte("error"), nil
case FatalLevel:
return []byte("fatal"), nil
case PanicLevel:
return []byte("panic"), nil
}
return nil, fmt.Errorf("not a valid logrus level %d", level)
}
// A constant exposing all logging levels
var AllLevels = []Level{
PanicLevel,
......@@ -61,6 +85,7 @@ var AllLevels = []Level{
WarnLevel,
InfoLevel,
DebugLevel,
TraceLevel,
}
// These are the different logging levels. You can set the logging level to log
......@@ -69,7 +94,7 @@ const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
// FatalLevel level. Logs and then calls `logger.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
......@@ -82,6 +107,8 @@ const (
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
// TraceLevel level. Designates finer-grained informational events than the Debug.
TraceLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
......@@ -140,4 +167,20 @@ type FieldLogger interface {
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
// IsDebugEnabled() bool
// IsInfoEnabled() bool
// IsWarnEnabled() bool
// IsErrorEnabled() bool
// IsFatalEnabled() bool
// IsPanicEnabled() bool
}
// Ext1FieldLogger (the first extension to FieldLogger) is superfluous, it is
// here for consistancy. Do not use. Use Logger or Entry instead.
type Ext1FieldLogger interface {
FieldLogger
Tracef(format string, args ...interface{})
Trace(args ...interface{})
Traceln(args ...interface{})
}
// +build darwin freebsd openbsd netbsd dragonfly
// +build !appengine
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
type Termios unix.Termios
// +build appengine
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return true
}
// +build darwin dragonfly freebsd netbsd openbsd
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TIOCGETA
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}
// +build js
package logrus
import (
"io"
)
func checkIfTerminal(w io.Writer) bool {
return false
}
// +build !appengine,!js,!windows
package logrus
import (
"io"
"os"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return isTerminal(int(v.Fd()))
default:
return false
}
}
// +build linux aix
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil
}
// +build !appengine,!js,windows
package logrus
import (
"io"
"os"
"syscall"
)
func checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
var mode uint32
err := syscall.GetConsoleMode(syscall.Handle(v.Fd()), &mode)
return err == nil
default:
return false
}
}
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine
package logrus
import "golang.org/x/sys/unix"
const ioctlReadTermios = unix.TCGETS
type Termios unix.Termios
// +build !windows
package logrus
import "io"
func initTerminal(w io.Writer) {
}
// +build !appengine,!js,windows
package logrus
import (
"io"
"os"
"syscall"
sequences "github.com/konsorten/go-windows-terminal-sequences"
)
func initTerminal(w io.Writer) {
switch v := w.(type) {
case *os.File:
sequences.EnableVirtualTerminalProcessing(syscall.Handle(v.Fd()), true)
}
}
......@@ -3,28 +3,22 @@ package logrus
import (
"bytes"
"fmt"
"io"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh/terminal"
)
const (
nocolor = 0
red = 31
green = 32
yellow = 33
blue = 36
gray = 37
red = 31
yellow = 33
blue = 36
gray = 37
)
var (
baseTimestamp time.Time
)
var baseTimestamp time.Time
func init() {
baseTimestamp = time.Now()
......@@ -38,6 +32,9 @@ type TextFormatter struct {
// Force disabling colors.
DisableColors bool
// Override coloring based on CLICOLOR and CLICOLOR_FORCE. - https://bixense.com/clicolors/
EnvironmentOverrideColors bool
// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool
......@@ -54,69 +51,155 @@ type TextFormatter struct {
// be desired.
DisableSorting bool
// The keys sorting function, when uninitialized it uses sort.Strings.
SortingFunc func([]string)
// Disables the truncation of the level text to 4 characters.
DisableLevelTruncation bool
// QuoteEmptyFields will wrap empty fields in quotes if true
QuoteEmptyFields bool
// Whether the logger's out is to a terminal
isTerminal bool
sync.Once
// FieldMap allows users to customize the names of keys for default fields.
// As an example:
// formatter := &TextFormatter{
// FieldMap: FieldMap{
// FieldKeyTime: "@timestamp",
// FieldKeyLevel: "@level",
// FieldKeyMsg: "@message"}}
FieldMap FieldMap
// CallerPrettyfier can be set by the user to modify the content
// of the function and file keys in the data when ReportCaller is
// activated. If any of the returned value is the empty string the
// corresponding key will be removed from fields.
CallerPrettyfier func(*runtime.Frame) (function string, file string)
terminalInitOnce sync.Once
}
func (f *TextFormatter) init(entry *Entry) {
if entry.Logger != nil {
f.isTerminal = f.checkIfTerminal(entry.Logger.Out)
f.isTerminal = checkIfTerminal(entry.Logger.Out)
if f.isTerminal {
initTerminal(entry.Logger.Out)
}
}
}
func (f *TextFormatter) checkIfTerminal(w io.Writer) bool {
switch v := w.(type) {
case *os.File:
return terminal.IsTerminal(int(v.Fd()))
default:
return false
func (f *TextFormatter) isColored() bool {
isColored := f.ForceColors || (f.isTerminal && (runtime.GOOS != "windows"))
if f.EnvironmentOverrideColors {
if force, ok := os.LookupEnv("CLICOLOR_FORCE"); ok && force != "0" {
isColored = true
} else if ok && force == "0" {
isColored = false
} else if os.Getenv("CLICOLOR") == "0" {
isColored = false
}
}
return isColored && !f.DisableColors
}
// Format renders a single log entry
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var b *bytes.Buffer
keys := make([]string, 0, len(entry.Data))
for k := range entry.Data {
data := make(Fields)
for k, v := range entry.Data {
data[k] = v
}
prefixFieldClashes(data, f.FieldMap, entry.HasCaller())
keys := make([]string, 0, len(data))
for k := range data {
keys = append(keys, k)
}
var funcVal, fileVal string
fixedKeys := make([]string, 0, 4+len(data))
if !f.DisableTimestamp {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyTime))
}
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLevel))
if entry.Message != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyMsg))
}
if entry.err != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyLogrusError))
}
if entry.HasCaller() {
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
} else {
funcVal = entry.Caller.Function
fileVal = fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
}
if funcVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFunc))
}
if fileVal != "" {
fixedKeys = append(fixedKeys, f.FieldMap.resolve(FieldKeyFile))
}
}
if !f.DisableSorting {
sort.Strings(keys)
if f.SortingFunc == nil {
sort.Strings(keys)
fixedKeys = append(fixedKeys, keys...)
} else {
if !f.isColored() {
fixedKeys = append(fixedKeys, keys...)
f.SortingFunc(fixedKeys)
} else {
f.SortingFunc(keys)
}
}
} else {
fixedKeys = append(fixedKeys, keys...)
}
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
prefixFieldClashes(entry.Data)
f.Do(func() { f.init(entry) })
isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors
f.terminalInitOnce.Do(func() { f.init(entry) })
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = defaultTimestampFormat
}
if isColored {
f.printColored(b, entry, keys, timestampFormat)
if f.isColored() {
f.printColored(b, entry, keys, data, timestampFormat)
} else {
if !f.DisableTimestamp {
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat))
}
f.appendKeyValue(b, "level", entry.Level.String())
if entry.Message != "" {
f.appendKeyValue(b, "msg", entry.Message)
}
for _, key := range keys {
f.appendKeyValue(b, key, entry.Data[key])
for _, key := range fixedKeys {
var value interface{}
switch {
case key == f.FieldMap.resolve(FieldKeyTime):
value = entry.Time.Format(timestampFormat)
case key == f.FieldMap.resolve(FieldKeyLevel):
value = entry.Level.String()
case key == f.FieldMap.resolve(FieldKeyMsg):
value = entry.Message
case key == f.FieldMap.resolve(FieldKeyLogrusError):
value = entry.err
case key == f.FieldMap.resolve(FieldKeyFunc) && entry.HasCaller():
value = funcVal
case key == f.FieldMap.resolve(FieldKeyFile) && entry.HasCaller():
value = fileVal
default:
value = data[key]
}
f.appendKeyValue(b, key, value)
}
}
......@@ -124,10 +207,10 @@ func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, data Fields, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel:
case DebugLevel, TraceLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
......@@ -137,17 +220,42 @@ func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []strin
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())[0:4]
levelText := strings.ToUpper(entry.Level.String())
if !f.DisableLevelTruncation {
levelText = levelText[0:4]
}
// Remove a single newline if it already exists in the message to keep
// the behavior of logrus text_formatter the same as the stdlib log package
entry.Message = strings.TrimSuffix(entry.Message, "\n")
caller := ""
if entry.HasCaller() {
funcVal := fmt.Sprintf("%s()", entry.Caller.Function)
fileVal := fmt.Sprintf("%s:%d", entry.Caller.File, entry.Caller.Line)
if f.CallerPrettyfier != nil {
funcVal, fileVal = f.CallerPrettyfier(entry.Caller)
}
if fileVal == "" {
caller = funcVal
} else if funcVal == "" {
caller = fileVal
} else {
caller = fileVal + " " + funcVal
}
}
if f.DisableTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m %-44s ", levelColor, levelText, entry.Message)
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m%s %-44s ", levelColor, levelText, caller, entry.Message)
} else if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), entry.Message)
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d]%s %-44s ", levelColor, levelText, int(entry.Time.Sub(baseTimestamp)/time.Second), caller, entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s]%s %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), caller, entry.Message)
}
for _, k := range keys {
v := entry.Data[k]
v := data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=", levelColor, k)
f.appendValue(b, v)
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment