adding quickfix
This commit is contained in:
81
quickfix/log/composite/composite_log.go
Normal file
81
quickfix/log/composite/composite_log.go
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package composite
|
||||
|
||||
import (
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
)
|
||||
|
||||
type compositeLog struct {
|
||||
logs []quickfix.Log
|
||||
}
|
||||
|
||||
func (l compositeLog) OnIncoming(s []byte) {
|
||||
for _, log := range l.logs {
|
||||
log.OnIncoming(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (l compositeLog) OnOutgoing(s []byte) {
|
||||
for _, log := range l.logs {
|
||||
log.OnOutgoing(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (l compositeLog) OnEvent(s string) {
|
||||
for _, log := range l.logs {
|
||||
log.OnEvent(s)
|
||||
}
|
||||
}
|
||||
|
||||
func (l compositeLog) OnEventf(format string, a ...interface{}) {
|
||||
for _, log := range l.logs {
|
||||
log.OnEventf(format, a)
|
||||
}
|
||||
}
|
||||
|
||||
type compositeLogFactory struct {
|
||||
logFactories []quickfix.LogFactory
|
||||
}
|
||||
|
||||
func (clf compositeLogFactory) Create() (quickfix.Log, error) {
|
||||
logs := []quickfix.Log{}
|
||||
for _, lf := range clf.logFactories {
|
||||
log, err := lf.Create()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
return compositeLog{logs}, nil
|
||||
}
|
||||
|
||||
func (clf compositeLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (quickfix.Log, error) {
|
||||
logs := []quickfix.Log{}
|
||||
for _, lf := range clf.logFactories {
|
||||
log, err := lf.CreateSessionLog(sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
return compositeLog{logs}, nil
|
||||
}
|
||||
|
||||
// NewLogFactory creates an instance of LogFactory that writes messages and events to stdout.
|
||||
func NewLogFactory(logfactories []quickfix.LogFactory) quickfix.LogFactory {
|
||||
return compositeLogFactory{logfactories}
|
||||
}
|
||||
109
quickfix/log/composite/composite_log_test.go
Normal file
109
quickfix/log/composite/composite_log_test.go
Normal file
@ -0,0 +1,109 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package composite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/log/file"
|
||||
"quantex.com/qfixdpl/quickfix/log/mongo"
|
||||
"quantex.com/qfixdpl/quickfix/log/screen"
|
||||
"quantex.com/qfixdpl/quickfix/log/sql"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// CompositeLogTestSuite runs tests for the MongoLog impl of Log.
|
||||
type CompositeLogTestSuite struct {
|
||||
suite.Suite
|
||||
sqlLogRootPath string
|
||||
settings *quickfix.Settings
|
||||
sessionID quickfix.SessionID
|
||||
}
|
||||
|
||||
func (suite *CompositeLogTestSuite) SetupTest() {
|
||||
mongoDbCxn := os.Getenv("MONGODB_TEST_CXN")
|
||||
if len(mongoDbCxn) <= 0 {
|
||||
log.Println("MONGODB_TEST_CXN environment arg is not provided, skipping...")
|
||||
suite.T().SkipNow()
|
||||
}
|
||||
mongoDatabase := "automated_testing_database"
|
||||
mongoReplicaSet := "replicaset"
|
||||
|
||||
// create settings
|
||||
sessionID := quickfix.SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"}
|
||||
logPath := path.Join(os.TempDir(), fmt.Sprintf("TestLogStore-%d", os.Getpid()))
|
||||
suite.sqlLogRootPath = path.Join(os.TempDir(), fmt.Sprintf("SQLLogTestSuite-%d", os.Getpid()))
|
||||
err := os.MkdirAll(suite.sqlLogRootPath, os.ModePerm)
|
||||
require.Nil(suite.T(), err)
|
||||
sqlDriver := "sqlite3"
|
||||
sqlDsn := path.Join(suite.sqlLogRootPath, fmt.Sprintf("%d.db", time.Now().UnixNano()))
|
||||
|
||||
settings, err := quickfix.ParseSettings(strings.NewReader(fmt.Sprintf(`
|
||||
[DEFAULT]
|
||||
MongoLogConnection=%s
|
||||
MongoLogDatabase=%s
|
||||
MongoLogReplicaSet=%s
|
||||
FileLogPath=%s
|
||||
SQLLogDriver=%s
|
||||
SQLLogDataSourceName=%s
|
||||
SQLLogConnMaxLifetime=14400s
|
||||
|
||||
[SESSION]
|
||||
BeginString=%s
|
||||
SenderCompID=%s
|
||||
TargetCompID=%s`, mongoDbCxn, mongoDatabase, mongoReplicaSet, logPath, sqlDriver, sqlDsn, sessionID.BeginString, sessionID.SenderCompID, sessionID.TargetCompID)))
|
||||
require.Nil(suite.T(), err)
|
||||
|
||||
suite.sessionID = sessionID
|
||||
suite.settings = settings
|
||||
}
|
||||
|
||||
func (suite *CompositeLogTestSuite) TestCreateLogNoSession() {
|
||||
|
||||
mngoLogFactory := mongo.NewLogFactory(suite.settings)
|
||||
sqlLogFactory := sql.NewLogFactory(suite.settings)
|
||||
// create log
|
||||
_, err := NewLogFactory([]quickfix.LogFactory{mngoLogFactory, sqlLogFactory}).Create()
|
||||
require.Nil(suite.T(), err)
|
||||
}
|
||||
|
||||
func (suite *CompositeLogTestSuite) TestCreateLogSession() {
|
||||
|
||||
screenLogFactory := screen.NewLogFactory()
|
||||
fileLogFactory, err := file.NewLogFactory(suite.settings)
|
||||
require.Nil(suite.T(), err)
|
||||
|
||||
// create log
|
||||
_, err = NewLogFactory([]quickfix.LogFactory{screenLogFactory, fileLogFactory}).CreateSessionLog(suite.sessionID)
|
||||
require.Nil(suite.T(), err)
|
||||
}
|
||||
|
||||
func (suite *CompositeLogTestSuite) TearDownTest() {
|
||||
os.RemoveAll(suite.sqlLogRootPath)
|
||||
}
|
||||
|
||||
func TestCompositeLogTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(CompositeLogTestSuite))
|
||||
}
|
||||
118
quickfix/log/file/file_log.go
Normal file
118
quickfix/log/file/file_log.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/config"
|
||||
)
|
||||
|
||||
type fileLog struct {
|
||||
eventLogger *log.Logger
|
||||
messageLogger *log.Logger
|
||||
}
|
||||
|
||||
func (l fileLog) OnIncoming(msg []byte) {
|
||||
l.messageLogger.Print(string(msg))
|
||||
}
|
||||
|
||||
func (l fileLog) OnOutgoing(msg []byte) {
|
||||
l.messageLogger.Print(string(msg))
|
||||
}
|
||||
|
||||
func (l fileLog) OnEvent(msg string) {
|
||||
l.eventLogger.Print(msg)
|
||||
}
|
||||
|
||||
func (l fileLog) OnEventf(format string, v ...interface{}) {
|
||||
l.eventLogger.Printf(format, v...)
|
||||
}
|
||||
|
||||
type fileLogFactory struct {
|
||||
globalLogPath string
|
||||
sessionLogPaths map[quickfix.SessionID]string
|
||||
}
|
||||
|
||||
// NewLogFactory creates an instance of LogFactory that writes messages and events to file.
|
||||
// The location of global and session log files is configured via FileLogPath.
|
||||
func NewLogFactory(settings *quickfix.Settings) (quickfix.LogFactory, error) {
|
||||
logFactory := fileLogFactory{}
|
||||
|
||||
var err error
|
||||
if logFactory.globalLogPath, err = settings.GlobalSettings().Setting(config.FileLogPath); err != nil {
|
||||
return logFactory, err
|
||||
}
|
||||
|
||||
logFactory.sessionLogPaths = make(map[quickfix.SessionID]string)
|
||||
|
||||
for sid, sessionSettings := range settings.SessionSettings() {
|
||||
logPath, err := sessionSettings.Setting(config.FileLogPath)
|
||||
if err != nil {
|
||||
return logFactory, err
|
||||
}
|
||||
logFactory.sessionLogPaths[sid] = logPath
|
||||
}
|
||||
|
||||
return logFactory, nil
|
||||
}
|
||||
|
||||
func newFileLog(prefix string, logPath string) (fileLog, error) {
|
||||
l := fileLog{}
|
||||
|
||||
eventLogName := path.Join(logPath, prefix+".event.current.log")
|
||||
messageLogName := path.Join(logPath, prefix+".messages.current.log")
|
||||
|
||||
if err := os.MkdirAll(logPath, os.ModePerm); err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
||||
fileFlags := os.O_RDWR | os.O_CREATE | os.O_APPEND
|
||||
eventFile, err := os.OpenFile(eventLogName, fileFlags, os.ModePerm)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
||||
messageFile, err := os.OpenFile(messageLogName, fileFlags, os.ModePerm)
|
||||
if err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
||||
logFlag := log.Ldate | log.Ltime | log.Lmicroseconds | log.LUTC
|
||||
l.eventLogger = log.New(eventFile, "", logFlag)
|
||||
l.messageLogger = log.New(messageFile, "", logFlag)
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (f fileLogFactory) Create() (quickfix.Log, error) {
|
||||
return newFileLog("GLOBAL", f.globalLogPath)
|
||||
}
|
||||
|
||||
func (f fileLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (quickfix.Log, error) {
|
||||
logPath, ok := f.sessionLogPaths[sessionID]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("logger not defined for %v", sessionID)
|
||||
}
|
||||
|
||||
prefix := sessionIDFilenamePrefix(sessionID)
|
||||
return newFileLog(prefix, logPath)
|
||||
}
|
||||
147
quickfix/log/file/file_log_test.go
Normal file
147
quickfix/log/file/file_log_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
)
|
||||
|
||||
func TestFileLog_NewFileLogFactory(t *testing.T) {
|
||||
|
||||
_, err := NewLogFactory(quickfix.NewSettings())
|
||||
|
||||
if err == nil {
|
||||
t.Error("Should expect error when settings have no file log path")
|
||||
}
|
||||
|
||||
cfg := `
|
||||
# default settings for sessions
|
||||
[DEFAULT]
|
||||
ConnectionType=initiator
|
||||
ReconnectInterval=60
|
||||
SenderCompID=TW
|
||||
FileLogPath=.
|
||||
|
||||
# session definition
|
||||
[SESSION]
|
||||
BeginString=FIX.4.1
|
||||
TargetCompID=ARCA
|
||||
FileLogPath=mydir
|
||||
|
||||
[SESSION]
|
||||
BeginString=FIX.4.1
|
||||
TargetCompID=ARCA
|
||||
SessionQualifier=BS
|
||||
`
|
||||
stringReader := strings.NewReader(cfg)
|
||||
settings, _ := quickfix.ParseSettings(stringReader)
|
||||
|
||||
factory, err := NewLogFactory(settings)
|
||||
|
||||
if err != nil {
|
||||
t.Error("Did not expect error", err)
|
||||
}
|
||||
|
||||
if factory == nil {
|
||||
t.Error("Should have returned factory")
|
||||
}
|
||||
}
|
||||
|
||||
type fileLogHelper struct {
|
||||
LogPath string
|
||||
Prefix string
|
||||
Log quickfix.Log
|
||||
}
|
||||
|
||||
func newFileLogHelper(t *testing.T) *fileLogHelper {
|
||||
prefix := "myprefix"
|
||||
logPath := path.Join(os.TempDir(), fmt.Sprintf("TestLogStore-%d", os.Getpid()))
|
||||
|
||||
log, err := newFileLog(prefix, logPath)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
|
||||
return &fileLogHelper{
|
||||
LogPath: logPath,
|
||||
Prefix: prefix,
|
||||
Log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFileLog(t *testing.T) {
|
||||
helper := newFileLogHelper(t)
|
||||
|
||||
tests := []struct {
|
||||
expectedPath string
|
||||
}{
|
||||
{path.Join(helper.LogPath, fmt.Sprintf("%v.messages.current.log", helper.Prefix))},
|
||||
{path.Join(helper.LogPath, fmt.Sprintf("%v.event.current.log", helper.Prefix))},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if _, err := os.Stat(test.expectedPath); os.IsNotExist(err) {
|
||||
t.Errorf("%v does not exist", test.expectedPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLog_Append(t *testing.T) {
|
||||
helper := newFileLogHelper(t)
|
||||
|
||||
messageLogFile, err := os.Open(path.Join(helper.LogPath, fmt.Sprintf("%v.messages.current.log", helper.Prefix)))
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
defer messageLogFile.Close()
|
||||
|
||||
eventLogFile, err := os.Open(path.Join(helper.LogPath, fmt.Sprintf("%v.event.current.log", helper.Prefix)))
|
||||
if err != nil {
|
||||
t.Error("Unexpected error", err)
|
||||
}
|
||||
defer eventLogFile.Close()
|
||||
|
||||
messageScanner := bufio.NewScanner(messageLogFile)
|
||||
eventScanner := bufio.NewScanner(eventLogFile)
|
||||
|
||||
helper.Log.OnIncoming([]byte("incoming"))
|
||||
if !messageScanner.Scan() {
|
||||
t.Error("Unexpected EOF")
|
||||
}
|
||||
|
||||
helper.Log.OnEvent("Event")
|
||||
if !eventScanner.Scan() {
|
||||
t.Error("Unexpected EOF")
|
||||
}
|
||||
|
||||
newHelper := newFileLogHelper(t)
|
||||
newHelper.Log.OnIncoming([]byte("incoming"))
|
||||
if !messageScanner.Scan() {
|
||||
t.Error("Unexpected EOF")
|
||||
}
|
||||
|
||||
newHelper.Log.OnEvent("Event")
|
||||
if !eventScanner.Scan() {
|
||||
t.Error("Unexpected EOF")
|
||||
}
|
||||
}
|
||||
58
quickfix/log/file/file_util.go
Normal file
58
quickfix/log/file/file_util.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
)
|
||||
|
||||
func sessionIDFilenamePrefix(s quickfix.SessionID) string {
|
||||
sender := []string{s.SenderCompID}
|
||||
if s.SenderSubID != "" {
|
||||
sender = append(sender, s.SenderSubID)
|
||||
}
|
||||
if s.SenderLocationID != "" {
|
||||
sender = append(sender, s.SenderLocationID)
|
||||
}
|
||||
|
||||
target := []string{s.TargetCompID}
|
||||
if s.TargetSubID != "" {
|
||||
target = append(target, s.TargetSubID)
|
||||
}
|
||||
if s.TargetLocationID != "" {
|
||||
target = append(target, s.TargetLocationID)
|
||||
}
|
||||
|
||||
fname := []string{s.BeginString, strings.Join(sender, "_"), strings.Join(target, "_")}
|
||||
if s.Qualifier != "" {
|
||||
fname = append(fname, s.Qualifier)
|
||||
}
|
||||
return strings.Join(fname, "-")
|
||||
}
|
||||
|
||||
// openOrCreateFile opens a file for reading and writing, creating it if necessary.
|
||||
func openOrCreateFile(fname string, perm os.FileMode) (f *os.File, err error) {
|
||||
if f, err = os.OpenFile(fname, os.O_RDWR, perm); err != nil {
|
||||
if f, err = os.OpenFile(fname, os.O_RDWR|os.O_CREATE, perm); err != nil {
|
||||
return nil, fmt.Errorf("error opening or creating file: %s: %s", fname, err.Error())
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
82
quickfix/log/file/file_util_test.go
Normal file
82
quickfix/log/file/file_util_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func requireNotFileExists(t *testing.T, fname string) {
|
||||
_, err := os.Stat(fname)
|
||||
require.NotNil(t, err)
|
||||
require.True(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func requireFileExists(t *testing.T, fname string) {
|
||||
_, err := os.Stat(fname)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestSessionIDFilename_MinimallyQualifiedSessionID(t *testing.T) {
|
||||
// When the session ID is
|
||||
sessionID := quickfix.SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"}
|
||||
|
||||
// Then the filename should be
|
||||
require.Equal(t, "FIX.4.4-SENDER-TARGET", sessionIDFilenamePrefix(sessionID))
|
||||
}
|
||||
|
||||
func TestSessionIDFilename_FullyQualifiedSessionID(t *testing.T) {
|
||||
// When the session ID is
|
||||
sessionID := quickfix.SessionID{
|
||||
BeginString: "FIX.4.4",
|
||||
SenderCompID: "A",
|
||||
SenderSubID: "B",
|
||||
SenderLocationID: "C",
|
||||
TargetCompID: "D",
|
||||
TargetSubID: "E",
|
||||
TargetLocationID: "F",
|
||||
Qualifier: "G",
|
||||
}
|
||||
|
||||
// Then the filename should be
|
||||
require.Equal(t, "FIX.4.4-A_B_C-D_E_F-G", sessionIDFilenamePrefix(sessionID))
|
||||
}
|
||||
|
||||
func TestOpenOrCreateFile(t *testing.T) {
|
||||
// When the file doesn't exist yet
|
||||
fname := path.Join(os.TempDir(), fmt.Sprintf("TestOpenOrCreateFile-%d", os.Getpid()))
|
||||
requireNotFileExists(t, fname)
|
||||
defer os.Remove(fname)
|
||||
|
||||
// Then it should be created
|
||||
f, err := openOrCreateFile(fname, 0664)
|
||||
require.Nil(t, err)
|
||||
requireFileExists(t, fname)
|
||||
|
||||
// When the file already exists
|
||||
f.Close()
|
||||
|
||||
// Then it should be opened
|
||||
f, err = openOrCreateFile(fname, 0664)
|
||||
require.Nil(t, err)
|
||||
require.Nil(t, f.Close())
|
||||
}
|
||||
222
quickfix/log/mongo/mongo_log.go
Normal file
222
quickfix/log/mongo/mongo_log.go
Normal file
@ -0,0 +1,222 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/config"
|
||||
)
|
||||
|
||||
type mongoLogFactory struct {
|
||||
settings *quickfix.Settings
|
||||
messagesLogCollection string
|
||||
eventLogCollection string
|
||||
}
|
||||
|
||||
type mongoLog struct {
|
||||
sessionID quickfix.SessionID
|
||||
mongoURL string
|
||||
mongoDatabase string
|
||||
db *mongo.Client
|
||||
messagesLogCollection string
|
||||
eventLogCollection string
|
||||
allowTransactions bool
|
||||
}
|
||||
|
||||
// NewLogFactory returns a mongo-based implementation of LogFactory.
|
||||
func NewLogFactory(settings *quickfix.Settings) quickfix.LogFactory {
|
||||
return NewLogFactoryPrefixed(settings, "")
|
||||
}
|
||||
|
||||
// NewLogFactoryPrefixed returns a mongo-based implementation of LogFactory, with prefix on collections.
|
||||
func NewLogFactoryPrefixed(settings *quickfix.Settings, collectionsPrefix string) quickfix.LogFactory {
|
||||
return mongoLogFactory{
|
||||
settings: settings,
|
||||
messagesLogCollection: collectionsPrefix + "messages_log",
|
||||
eventLogCollection: collectionsPrefix + "event_log",
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates a new mongo implementation of the Log interface.
|
||||
func (f mongoLogFactory) Create() (l quickfix.Log, err error) {
|
||||
globalSettings := f.settings.GlobalSettings()
|
||||
|
||||
mongoConnectionURL, err := globalSettings.Setting(config.MongoLogConnection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mongoDatabase, err := globalSettings.Setting(config.MongoLogDatabase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optional.
|
||||
mongoReplicaSet, _ := globalSettings.Setting(config.MongoLogReplicaSet)
|
||||
|
||||
return newmongoLog(quickfix.SessionID{}, mongoConnectionURL, mongoDatabase, mongoReplicaSet, f.messagesLogCollection, f.eventLogCollection)
|
||||
}
|
||||
|
||||
// CreateSessionLog creates a new mongo implementation of the Log interface.
|
||||
func (f mongoLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (l quickfix.Log, err error) {
|
||||
globalSettings := f.settings.GlobalSettings()
|
||||
dynamicSessions, _ := globalSettings.BoolSetting(config.DynamicSessions)
|
||||
|
||||
sessionSettings, ok := f.settings.SessionSettings()[sessionID]
|
||||
if !ok {
|
||||
if dynamicSessions {
|
||||
sessionSettings = globalSettings
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown session: %v", sessionID)
|
||||
}
|
||||
}
|
||||
mongoConnectionURL, err := sessionSettings.Setting(config.MongoLogConnection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mongoDatabase, err := sessionSettings.Setting(config.MongoLogDatabase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Optional.
|
||||
mongoReplicaSet, _ := sessionSettings.Setting(config.MongoLogReplicaSet)
|
||||
|
||||
return newmongoLog(sessionID, mongoConnectionURL, mongoDatabase, mongoReplicaSet, f.messagesLogCollection, f.eventLogCollection)
|
||||
}
|
||||
|
||||
func newmongoLog(sessionID quickfix.SessionID, mongoURL, mongoDatabase, mongoReplicaSet, messagesLogCollection, eventLogCollection string) (l *mongoLog, err error) {
|
||||
|
||||
allowTransactions := len(mongoReplicaSet) > 0
|
||||
l = &mongoLog{
|
||||
sessionID: sessionID,
|
||||
mongoURL: mongoURL,
|
||||
mongoDatabase: mongoDatabase,
|
||||
messagesLogCollection: messagesLogCollection,
|
||||
eventLogCollection: eventLogCollection,
|
||||
allowTransactions: allowTransactions,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
l.db, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURL).SetDirect(len(mongoReplicaSet) == 0).SetReplicaSet(mongoReplicaSet))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l mongoLog) OnIncoming(msg []byte) {
|
||||
l.insert(l.messagesLogCollection, msg)
|
||||
}
|
||||
|
||||
func (l mongoLog) OnOutgoing(msg []byte) {
|
||||
l.insert(l.messagesLogCollection, msg)
|
||||
}
|
||||
|
||||
func (l mongoLog) OnEvent(msg string) {
|
||||
l.insert(l.eventLogCollection, []byte(msg))
|
||||
}
|
||||
|
||||
func (l mongoLog) OnEventf(format string, v ...interface{}) {
|
||||
l.insert(l.eventLogCollection, []byte(fmt.Sprintf(format, v...)))
|
||||
}
|
||||
|
||||
func generateEntry(s *quickfix.SessionID) (entry *entryData) {
|
||||
entry = &entryData{
|
||||
BeginString: s.BeginString,
|
||||
SessionQualifier: s.Qualifier,
|
||||
SenderCompID: s.SenderCompID,
|
||||
SenderSubID: s.SenderSubID,
|
||||
SenderLocID: s.SenderLocationID,
|
||||
TargetCompID: s.TargetCompID,
|
||||
TargetSubID: s.TargetSubID,
|
||||
TargetLocID: s.TargetLocationID,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type entryData struct {
|
||||
Time time.Time `bson:"time,omitempty"`
|
||||
BeginString string `bson:"begin_string"`
|
||||
SenderCompID string `bson:"sender_comp_id"`
|
||||
SenderSubID string `bson:"sender_sub_id"`
|
||||
SenderLocID string `bson:"sender_loc_id"`
|
||||
TargetCompID string `bson:"target_comp_id"`
|
||||
TargetSubID string `bson:"target_sub_id"`
|
||||
TargetLocID string `bson:"target_loc_id"`
|
||||
SessionQualifier string `bson:"session_qualifier"`
|
||||
Text []byte `bson:"text,omitempty"`
|
||||
}
|
||||
|
||||
func (l *mongoLog) insert(collection string, text []byte) {
|
||||
entry := generateEntry(&l.sessionID)
|
||||
entry.Text = text
|
||||
entry.Time = time.Now()
|
||||
_, err := l.db.Database(l.mongoDatabase).Collection(collection).InsertOne(context.Background(), entry)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *mongoLog) iterate(coll string, cb func(string) error) error {
|
||||
entry := generateEntry(&l.sessionID)
|
||||
|
||||
cursor, err := l.db.Database(l.mongoDatabase).Collection(coll).Find(context.Background(), bson.D{}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = cursor.Close(context.Background()) }()
|
||||
for cursor.Next(context.Background()) {
|
||||
if err = cursor.Decode(&entry); err != nil {
|
||||
return err
|
||||
} else if err = cb(string(entry.Text)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *mongoLog) getEntries(coll string) ([]string, error) {
|
||||
var txts []string
|
||||
err := l.iterate(coll, func(text string) error {
|
||||
txts = append(txts, text)
|
||||
return nil
|
||||
})
|
||||
return txts, err
|
||||
}
|
||||
|
||||
// close closes the l's database connection.
|
||||
func (l *mongoLog) close() error {
|
||||
if l.db != nil {
|
||||
err := l.db.Disconnect(context.Background())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error disconnecting from database")
|
||||
}
|
||||
l.db = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
127
quickfix/log/mongo/mongo_log_test.go
Normal file
127
quickfix/log/mongo/mongo_log_test.go
Normal file
@ -0,0 +1,127 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// MongoLogTestSuite runs tests for the MongoLog impl of Log.
|
||||
type MongoLogTestSuite struct {
|
||||
suite.Suite
|
||||
log *mongoLog
|
||||
settings *quickfix.Settings
|
||||
sessionID quickfix.SessionID
|
||||
}
|
||||
|
||||
func (suite *MongoLogTestSuite) SetupTest() {
|
||||
mongoDbCxn := os.Getenv("MONGODB_TEST_CXN")
|
||||
if len(mongoDbCxn) <= 0 {
|
||||
log.Println("MONGODB_TEST_CXN environment arg is not provided, skipping...")
|
||||
suite.T().SkipNow()
|
||||
}
|
||||
mongoDatabase := "automated_testing_database"
|
||||
mongoReplicaSet := "replicaset"
|
||||
|
||||
// create settings
|
||||
sessionID := quickfix.SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"}
|
||||
settings, err := quickfix.ParseSettings(strings.NewReader(fmt.Sprintf(`
|
||||
[DEFAULT]
|
||||
MongoLogConnection=%s
|
||||
MongoLogDatabase=%s
|
||||
MongoLogReplicaSet=%s
|
||||
|
||||
[SESSION]
|
||||
BeginString=%s
|
||||
SenderCompID=%s
|
||||
TargetCompID=%s`, mongoDbCxn, mongoDatabase, mongoReplicaSet, sessionID.BeginString, sessionID.SenderCompID, sessionID.TargetCompID)))
|
||||
require.Nil(suite.T(), err)
|
||||
|
||||
suite.sessionID = sessionID
|
||||
suite.settings = settings
|
||||
}
|
||||
|
||||
func (suite *MongoLogTestSuite) TestMongoLogNoSession() {
|
||||
// create log
|
||||
log, err := NewLogFactory(suite.settings).Create()
|
||||
require.Nil(suite.T(), err)
|
||||
suite.log = log.(*mongoLog)
|
||||
|
||||
suite.log.OnIncoming([]byte("Cool1"))
|
||||
suite.log.OnOutgoing([]byte("Cool2"))
|
||||
entries, err := suite.log.getEntries("messages_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool1", entries[0])
|
||||
require.Equal(suite.T(), "Cool2", entries[1])
|
||||
|
||||
suite.log.OnEvent("Cool3")
|
||||
suite.log.OnEvent("Cool4")
|
||||
entries, err = suite.log.getEntries("event_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool3", entries[0])
|
||||
require.Equal(suite.T(), "Cool4", entries[1])
|
||||
}
|
||||
|
||||
func (suite *MongoLogTestSuite) TestMongoLog() {
|
||||
// create log
|
||||
log, err := NewLogFactory(suite.settings).CreateSessionLog(suite.sessionID)
|
||||
require.Nil(suite.T(), err)
|
||||
suite.log = log.(*mongoLog)
|
||||
|
||||
suite.log.OnIncoming([]byte("Cool1"))
|
||||
suite.log.OnOutgoing([]byte("Cool2"))
|
||||
entries, err := suite.log.getEntries("messages_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool1", entries[0])
|
||||
require.Equal(suite.T(), "Cool2", entries[1])
|
||||
|
||||
suite.log.OnEvent("Cool3")
|
||||
suite.log.OnEvent("Cool4")
|
||||
entries, err = suite.log.getEntries("event_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool3", entries[0])
|
||||
require.Equal(suite.T(), "Cool4", entries[1])
|
||||
}
|
||||
|
||||
func (suite *MongoLogTestSuite) TearDownTest() {
|
||||
entry := generateEntry(&suite.log.sessionID)
|
||||
_, err := suite.log.db.Database(suite.log.mongoDatabase).Collection(suite.log.messagesLogCollection).DeleteMany(context.Background(), entry)
|
||||
require.Nil(suite.T(), err)
|
||||
|
||||
entry2 := generateEntry(&suite.log.sessionID)
|
||||
_, err = suite.log.db.Database(suite.log.mongoDatabase).Collection(suite.log.eventLogCollection).DeleteMany(context.Background(), entry2)
|
||||
require.Nil(suite.T(), err)
|
||||
|
||||
err = suite.log.close()
|
||||
require.Nil(suite.T(), err)
|
||||
}
|
||||
|
||||
func TestMongoLogTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(MongoLogTestSuite))
|
||||
}
|
||||
63
quickfix/log/screen/screen_log.go
Normal file
63
quickfix/log/screen/screen_log.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package screen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
)
|
||||
|
||||
type screenLog struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (l screenLog) OnIncoming(s []byte) {
|
||||
logTime := time.Now().UTC()
|
||||
fmt.Printf("<%v, %s, incoming>\n (%s)\n", logTime, l.prefix, s)
|
||||
}
|
||||
|
||||
func (l screenLog) OnOutgoing(s []byte) {
|
||||
logTime := time.Now().UTC()
|
||||
fmt.Printf("<%v, %s, outgoing>\n (%s)\n", logTime, l.prefix, s)
|
||||
}
|
||||
|
||||
func (l screenLog) OnEvent(s string) {
|
||||
logTime := time.Now().UTC()
|
||||
fmt.Printf("<%v, %s, event>\n (%s)\n", logTime, l.prefix, s)
|
||||
}
|
||||
|
||||
func (l screenLog) OnEventf(format string, a ...interface{}) {
|
||||
l.OnEvent(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
type screenLogFactory struct{}
|
||||
|
||||
func (screenLogFactory) Create() (quickfix.Log, error) {
|
||||
log := screenLog{"GLOBAL"}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (screenLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (quickfix.Log, error) {
|
||||
log := screenLog{sessionID.String()}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// NewLogFactory creates an instance of LogFactory that writes messages and events to stdout.
|
||||
func NewLogFactory() quickfix.LogFactory {
|
||||
return screenLogFactory{}
|
||||
}
|
||||
226
quickfix/log/sql/sql_log.go
Normal file
226
quickfix/log/sql/sql_log.go
Normal file
@ -0,0 +1,226 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"quantex.com/qfixdpl/quickfix/config"
|
||||
)
|
||||
|
||||
type sqlLogFactory struct {
|
||||
settings *quickfix.Settings
|
||||
}
|
||||
|
||||
type sqlLog struct {
|
||||
sessionID quickfix.SessionID
|
||||
sqlDriver string
|
||||
sqlDataSourceName string
|
||||
sqlConnMaxLifetime time.Duration
|
||||
db *sql.DB
|
||||
placeholder placeholderFunc
|
||||
}
|
||||
|
||||
type placeholderFunc func(int) string
|
||||
|
||||
var rePlaceholder = regexp.MustCompile(`\?`)
|
||||
|
||||
func sqlString(raw string, placeholder placeholderFunc) string {
|
||||
if placeholder == nil {
|
||||
return raw
|
||||
}
|
||||
idx := 0
|
||||
return rePlaceholder.ReplaceAllStringFunc(raw, func(_ string) string {
|
||||
p := placeholder(idx)
|
||||
idx++
|
||||
return p
|
||||
})
|
||||
}
|
||||
|
||||
func postgresPlaceholder(i int) string {
|
||||
return fmt.Sprintf("$%d", i+1)
|
||||
}
|
||||
|
||||
// NewLogFactory returns a sql-based implementation of LogFactory.
|
||||
func NewLogFactory(settings *quickfix.Settings) quickfix.LogFactory {
|
||||
return sqlLogFactory{settings: settings}
|
||||
}
|
||||
|
||||
// Create creates a new SQLLog implementation of the Log interface.
|
||||
func (f sqlLogFactory) Create() (log quickfix.Log, err error) {
|
||||
globalSettings := f.settings.GlobalSettings()
|
||||
|
||||
sqlDriver, err := globalSettings.Setting(config.SQLLogDriver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDataSourceName, err := globalSettings.Setting(config.SQLLogDataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlConnMaxLifetime := 0 * time.Second
|
||||
if globalSettings.HasSetting(config.SQLLogConnMaxLifetime) {
|
||||
sqlConnMaxLifetime, err = globalSettings.DurationSetting(config.SQLLogConnMaxLifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newSQLLog(quickfix.SessionID{}, sqlDriver, sqlDataSourceName, sqlConnMaxLifetime)
|
||||
}
|
||||
|
||||
// CreateSessionLog creates a new SQLLog implementation of the Log interface.
|
||||
func (f sqlLogFactory) CreateSessionLog(sessionID quickfix.SessionID) (log quickfix.Log, err error) {
|
||||
globalSettings := f.settings.GlobalSettings()
|
||||
dynamicSessions, _ := globalSettings.BoolSetting(config.DynamicSessions)
|
||||
|
||||
sessionSettings, ok := f.settings.SessionSettings()[sessionID]
|
||||
if !ok {
|
||||
if dynamicSessions {
|
||||
sessionSettings = globalSettings
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown session: %v", sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
sqlDriver, err := sessionSettings.Setting(config.SQLLogDriver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDataSourceName, err := sessionSettings.Setting(config.SQLLogDataSourceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlConnMaxLifetime := 0 * time.Second
|
||||
if sessionSettings.HasSetting(config.SQLLogConnMaxLifetime) {
|
||||
sqlConnMaxLifetime, err = sessionSettings.DurationSetting(config.SQLLogConnMaxLifetime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return newSQLLog(sessionID, sqlDriver, sqlDataSourceName, sqlConnMaxLifetime)
|
||||
}
|
||||
|
||||
func newSQLLog(sessionID quickfix.SessionID, driver string, dataSourceName string, connMaxLifetime time.Duration) (l *sqlLog, err error) {
|
||||
l = &sqlLog{
|
||||
sessionID: sessionID,
|
||||
sqlDriver: driver,
|
||||
sqlDataSourceName: dataSourceName,
|
||||
sqlConnMaxLifetime: connMaxLifetime,
|
||||
}
|
||||
|
||||
if l.sqlDriver == "postgres" || l.sqlDriver == "pgx" {
|
||||
l.placeholder = postgresPlaceholder
|
||||
}
|
||||
|
||||
if l.db, err = sql.Open(l.sqlDriver, l.sqlDataSourceName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.db.SetConnMaxLifetime(l.sqlConnMaxLifetime)
|
||||
|
||||
if err = l.db.Ping(); err != nil { // ensure immediate connection
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (l sqlLog) OnIncoming(msg []byte) {
|
||||
l.insert("messages_log", string(msg))
|
||||
}
|
||||
|
||||
func (l sqlLog) OnOutgoing(msg []byte) {
|
||||
l.insert("messages_log", string(msg))
|
||||
}
|
||||
|
||||
func (l sqlLog) OnEvent(msg string) {
|
||||
l.insert("event_log", msg)
|
||||
}
|
||||
|
||||
func (l sqlLog) OnEventf(format string, v ...interface{}) {
|
||||
l.insert("event_log", fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (l sqlLog) insert(table string, value string) {
|
||||
s := l.sessionID
|
||||
|
||||
_, err := l.db.Exec(sqlString(`INSERT INTO `+table+` (
|
||||
time,
|
||||
beginstring, session_qualifier,
|
||||
sendercompid, sendersubid, senderlocid,
|
||||
targetcompid, targetsubid, targetlocid,
|
||||
text)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, l.placeholder),
|
||||
time.Now(),
|
||||
s.BeginString, s.Qualifier,
|
||||
s.SenderCompID, s.SenderSubID, s.SenderLocationID,
|
||||
s.TargetCompID, s.TargetSubID, s.TargetLocationID,
|
||||
value,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *sqlLog) iterate(table string, cb func(string) error) error {
|
||||
s := l.sessionID
|
||||
rows, err := l.db.Query(sqlString(`SELECT text FROM `+table+`
|
||||
WHERE beginstring=? AND session_qualifier=?
|
||||
AND sendercompid=? AND sendersubid=? AND senderlocid=?
|
||||
AND targetcompid=? AND targetsubid=? AND targetlocid=?`,
|
||||
l.placeholder),
|
||||
s.BeginString, s.Qualifier,
|
||||
s.SenderCompID, s.SenderSubID, s.SenderLocationID,
|
||||
s.TargetCompID, s.TargetSubID, s.TargetLocationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
for rows.Next() {
|
||||
var txt string
|
||||
if err = rows.Scan(&txt); err != nil {
|
||||
return err
|
||||
} else if err = cb(txt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func (l *sqlLog) getEntries(table string) ([]string, error) {
|
||||
var msgs []string
|
||||
err := l.iterate(table, func(msg string) error {
|
||||
msgs = append(msgs, msg)
|
||||
return nil
|
||||
})
|
||||
return msgs, err
|
||||
}
|
||||
|
||||
// Close closes the log's database connection.
|
||||
func (l *sqlLog) close() error {
|
||||
if l.db != nil {
|
||||
l.db.Close()
|
||||
l.db = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
139
quickfix/log/sql/sql_log_test.go
Normal file
139
quickfix/log/sql/sql_log_test.go
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright (c) quickfixengine.org All rights reserved.
|
||||
//
|
||||
// This file may be distributed under the terms of the quickfixengine.org
|
||||
// license as defined by quickfixengine.org and appearing in the file
|
||||
// LICENSE included in the packaging of this file.
|
||||
//
|
||||
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING
|
||||
// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A
|
||||
// PARTICULAR PURPOSE.
|
||||
//
|
||||
// See http://www.quickfixengine.org/LICENSE for licensing information.
|
||||
//
|
||||
// Contact ask@quickfixengine.org if any conditions of this licensing
|
||||
// are not clear to you.
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"quantex.com/qfixdpl/quickfix"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// SQLLogTestSuite runs tests for the SQLLog impl of Log.
|
||||
type SQLLogTestSuite struct {
|
||||
suite.Suite
|
||||
sqlLogRootPath string
|
||||
log *sqlLog
|
||||
|
||||
settings *quickfix.Settings
|
||||
sessionID quickfix.SessionID
|
||||
}
|
||||
|
||||
func (suite *SQLLogTestSuite) SetupTest() {
|
||||
suite.sqlLogRootPath = path.Join(os.TempDir(), fmt.Sprintf("SQLLogTestSuite-%d", os.Getpid()))
|
||||
err := os.MkdirAll(suite.sqlLogRootPath, os.ModePerm)
|
||||
require.Nil(suite.T(), err)
|
||||
sqlDriver := "sqlite3"
|
||||
sqlDsn := path.Join(suite.sqlLogRootPath, fmt.Sprintf("%d.db", time.Now().UnixNano()))
|
||||
|
||||
// create tables
|
||||
db, err := sql.Open(sqlDriver, sqlDsn)
|
||||
require.Nil(suite.T(), err)
|
||||
ddlFnames, err := filepath.Glob(fmt.Sprintf("../../_sql/%s/*.sql", sqlDriver))
|
||||
require.Nil(suite.T(), err)
|
||||
for _, fname := range ddlFnames {
|
||||
sqlBytes, err := os.ReadFile(fname)
|
||||
require.Nil(suite.T(), err)
|
||||
_, err = db.Exec(string(sqlBytes))
|
||||
require.Nil(suite.T(), err)
|
||||
}
|
||||
|
||||
// create settings
|
||||
sessionID := quickfix.SessionID{BeginString: "FIX.4.4", SenderCompID: "SENDER", TargetCompID: "TARGET"}
|
||||
settings, err := quickfix.ParseSettings(strings.NewReader(fmt.Sprintf(`
|
||||
[DEFAULT]
|
||||
SQLLogDriver=%s
|
||||
SQLLogDataSourceName=%s
|
||||
SQLLogConnMaxLifetime=14400s
|
||||
|
||||
[SESSION]
|
||||
BeginString=%s
|
||||
SenderCompID=%s
|
||||
TargetCompID=%s`, sqlDriver, sqlDsn, sessionID.BeginString, sessionID.SenderCompID, sessionID.TargetCompID)))
|
||||
require.Nil(suite.T(), err)
|
||||
|
||||
suite.sessionID = sessionID
|
||||
suite.settings = settings
|
||||
}
|
||||
|
||||
func (suite *SQLLogTestSuite) TestSQLLogNoSession() {
|
||||
// create log
|
||||
log, err := NewLogFactory(suite.settings).Create()
|
||||
require.Nil(suite.T(), err)
|
||||
suite.log = log.(*sqlLog)
|
||||
|
||||
suite.log.OnIncoming([]byte("Cool1"))
|
||||
suite.log.OnOutgoing([]byte("Cool2"))
|
||||
entries, err := suite.log.getEntries("messages_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool1", entries[0])
|
||||
require.Equal(suite.T(), "Cool2", entries[1])
|
||||
|
||||
suite.log.OnEvent("Cool3")
|
||||
suite.log.OnEvent("Cool4")
|
||||
entries, err = suite.log.getEntries("event_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool3", entries[0])
|
||||
require.Equal(suite.T(), "Cool4", entries[1])
|
||||
}
|
||||
|
||||
func (suite *SQLLogTestSuite) TestSQLLog() {
|
||||
// create log
|
||||
log, err := NewLogFactory(suite.settings).CreateSessionLog(suite.sessionID)
|
||||
require.Nil(suite.T(), err)
|
||||
suite.log = log.(*sqlLog)
|
||||
|
||||
suite.log.OnIncoming([]byte("Cool1"))
|
||||
suite.log.OnOutgoing([]byte("Cool2"))
|
||||
entries, err := suite.log.getEntries("messages_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool1", entries[0])
|
||||
require.Equal(suite.T(), "Cool2", entries[1])
|
||||
|
||||
suite.log.OnEvent("Cool3")
|
||||
suite.log.OnEvent("Cool4")
|
||||
entries, err = suite.log.getEntries("event_log")
|
||||
require.Nil(suite.T(), err)
|
||||
require.Len(suite.T(), entries, 2)
|
||||
require.Equal(suite.T(), "Cool3", entries[0])
|
||||
require.Equal(suite.T(), "Cool4", entries[1])
|
||||
}
|
||||
|
||||
func (suite *SQLLogTestSuite) TestSqlPlaceholderReplacement() {
|
||||
got := sqlString("A ? B ? C ?", postgresPlaceholder)
|
||||
suite.Equal("A $1 B $2 C $3", got)
|
||||
}
|
||||
|
||||
func (suite *SQLLogTestSuite) TearDownTest() {
|
||||
suite.log.close()
|
||||
os.RemoveAll(suite.sqlLogRootPath)
|
||||
}
|
||||
|
||||
func TestSQLLogTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(SQLLogTestSuite))
|
||||
}
|
||||
Reference in New Issue
Block a user