logstash-filter-fix_protocol 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c738b5a16d38b8c62fb2c1f03a6656af33e9ca8
|
4
|
+
data.tar.gz: 54367647f6258f5c17ad2739868ca855f5432439
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86c6277a75cce71efa8f413f887e30669bc4954aeba5acd9ce52d831272ab68215d103ea69e0ec8b950c8bf2198a099f8edbe30efeea18c283769b68c4b66015
|
7
|
+
data.tar.gz: c24afff68ef9a9bcf109a796aba773b6c17a5c0dfb4fcf3fd91d5928c0428e5d8e2d332795815d523db697d415e61059888bf6f7a75ce7959242708213d386cb
|
@@ -1,17 +1,16 @@
|
|
1
1
|
require 'quickfix'
|
2
|
-
require 'active_support/core_ext'
|
3
2
|
|
4
3
|
module LogStash
|
5
4
|
module Filters
|
6
5
|
class FixMessage < quickfix.Message
|
7
|
-
attr_reader :type, :msg_string, :session_dictionary, :data_dictionary, :all_dictionaries
|
6
|
+
attr_reader :type, :msg_string, :session_dictionary, :data_dictionary, :all_dictionaries, :unknown_fields
|
8
7
|
|
9
8
|
def initialize(msg_string, data_dictionary, session_dictionary)
|
10
9
|
@session_dictionary = session_dictionary
|
11
10
|
@data_dictionary = data_dictionary
|
12
11
|
@msg_string = msg_string
|
13
12
|
@type = quickfix.MessageUtils.getMessageType(msg_string)
|
14
|
-
|
13
|
+
@unknown_fields = []
|
15
14
|
@all_dictionaries = [@data_dictionary, @session_dictionary]
|
16
15
|
|
17
16
|
super(msg_string, data_dictionary, false)
|
@@ -44,6 +43,14 @@ module LogStash
|
|
44
43
|
value = dd.get_field_name(tag)
|
45
44
|
return value if value.present?
|
46
45
|
end
|
46
|
+
|
47
|
+
tag = to_string(tag)
|
48
|
+
@unknown_fields << tag
|
49
|
+
tag
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_string(tag)
|
53
|
+
tag.to_s.force_encoding("UTF-8")
|
47
54
|
end
|
48
55
|
|
49
56
|
def field_map_to_hash(field_map, msg_type = nil)
|
@@ -56,17 +63,21 @@ module LogStash
|
|
56
63
|
tag = field.get_tag
|
57
64
|
value = field.get_value
|
58
65
|
|
59
|
-
# IF GROUP
|
60
66
|
if msg_type.present? and @data_dictionary.is_group(msg_type, tag)
|
61
67
|
groups = []
|
62
68
|
|
63
69
|
for i in 1..value.to_i
|
64
|
-
|
65
|
-
|
70
|
+
begin
|
71
|
+
group_map = field_map.get_group(i, tag)
|
72
|
+
group_hash = field_map_to_hash(group_map, msg_type)
|
73
|
+
rescue Java::Quickfix::FieldNotFound
|
74
|
+
group_hash = {to_string(tag) => i}
|
75
|
+
self.unknown_fields << to_string(tag)
|
76
|
+
end
|
77
|
+
groups << group_hash
|
66
78
|
end
|
67
79
|
|
68
80
|
value = groups
|
69
|
-
# IF FIELD
|
70
81
|
elsif @data_dictionary.is_field(tag)
|
71
82
|
value =
|
72
83
|
case field_type(tag)
|
@@ -3,6 +3,7 @@ require "logstash/filters/base"
|
|
3
3
|
require "logstash/namespace"
|
4
4
|
require "logstash/filters/data_dictionary"
|
5
5
|
require "logstash/filters/fix_message"
|
6
|
+
require 'active_support/core_ext'
|
6
7
|
|
7
8
|
module LogStash
|
8
9
|
module Filters
|
@@ -48,18 +49,15 @@ module LogStash
|
|
48
49
|
if fix_message
|
49
50
|
fix_hash = fix_message.to_hash
|
50
51
|
|
52
|
+
# TODO: Unknown tags are only set after calling #to_hash
|
53
|
+
# this creates an implicit timing issue
|
54
|
+
if fix_message.unknown_fields.any?
|
55
|
+
event["unknown_fields"] = fix_message.unknown_fields
|
56
|
+
event["tags"] = ["_fix_field_not_found"]
|
57
|
+
end
|
58
|
+
|
51
59
|
fix_hash.each do |key, value|
|
52
|
-
|
53
|
-
event[key] = value
|
54
|
-
rescue NoMethodError => e
|
55
|
-
puts "********"
|
56
|
-
puts "WARNING: Could not correctly parse #{event["fix_message"]}"
|
57
|
-
puts JSON.pretty_generate(fix_hash)
|
58
|
-
puts "Message: #{e.message}"
|
59
|
-
puts "********"
|
60
|
-
ensure
|
61
|
-
next
|
62
|
-
end
|
60
|
+
event[key] = value
|
63
61
|
end
|
64
62
|
end
|
65
63
|
end
|
@@ -9,6 +9,9 @@ describe LF::FixMessage do
|
|
9
9
|
let(:message) { LF::FixMessage.new(message_str, data_dictionary, session_dictionary) }
|
10
10
|
let(:message2) { LF::FixMessage.new(another_str, data_dictionary, session_dictionary) }
|
11
11
|
|
12
|
+
let(:fix_4) { {data_dictionary: "FIX42.xml", session_dictionary: nil} }
|
13
|
+
let(:fix_5) { {data_dictionary: "FIX50SP1.xml", session_dictionary: "FIXT11.xml"} }
|
14
|
+
|
12
15
|
describe '#to_hash' do
|
13
16
|
it 'converts the FIX message string to a hash in human readable format' do
|
14
17
|
expect(message.to_hash).to eq({
|
@@ -59,10 +62,41 @@ describe LF::FixMessage do
|
|
59
62
|
end
|
60
63
|
end
|
61
64
|
|
65
|
+
describe '#field_map_to_hash' do
|
66
|
+
context 'invalid message - field name not found' do
|
67
|
+
let(:fix_string) { "8=FIX.4.29=24035=834=649=DUMMY_INC52=20150826-23:10:17.74456=ANOTHER_INC57=Firm_B1=Inst_B6=011=best_buy14=517=ITRZ1201508261_2420=022=831=101032=537=ITRZ1201508261_1238=539=27012=22740=241=best_buy44=101154=155=ITRZ160=20150826-23:10:15.547150=2151=010=227" }
|
68
|
+
|
69
|
+
it 'adds an error object and uses the data dictionary index number as the key' do
|
70
|
+
data_dictionary = LF::DataDictionary.new(load_fixture(fix_5[:data_dictionary]))
|
71
|
+
sess_dictionary = LF::DataDictionary.new(load_fixture(fix_5[:session_dictionary]))
|
72
|
+
|
73
|
+
fix_message = LF::FixMessage.new(fix_string, data_dictionary, sess_dictionary)
|
74
|
+
hash = fix_message.to_hash
|
75
|
+
# Side Note: field 20, LastShares, was changed between FIX 4 / 5
|
76
|
+
expect(hash["20"]).to eq("0")
|
77
|
+
expect(hash["7012"]).to eq("227")
|
78
|
+
expect(fix_message.unknown_fields.include?("20")).to be true
|
79
|
+
expect(fix_message.unknown_fields.include?("7012")).to be true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'invalid message - group field name not found (Java::Quickfix::FieldNotFound)' do
|
84
|
+
let(:fix_string) { "8=FIX.4.235=D34=249=TW52=<TIME>56=ISLD11=ID21=140=154=138=200.0055=INTC386=3336=PRE-OPEN336=AFTER-HOURS60=<TIME>" }
|
85
|
+
|
86
|
+
it 'adds an error object and uses the data dictionary index number as the key' do
|
87
|
+
data_dictionary = LF::DataDictionary.new(load_fixture(fix_5[:data_dictionary]))
|
88
|
+
sess_dictionary = LF::DataDictionary.new(load_fixture(fix_5[:session_dictionary]))
|
89
|
+
|
90
|
+
fix_message = LF::FixMessage.new(fix_string, data_dictionary, sess_dictionary)
|
91
|
+
hash = fix_message.to_hash
|
92
|
+
expect(fix_message.unknown_fields.include?("386")).to be true
|
93
|
+
expect(hash["NoTradingSessions"]).to eq([{"TradingSessionID"=>"PRE-OPEN"}, {"TradingSessionID"=>"AFTER-HOURS"}, {"386"=>3}])
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
62
98
|
context 'message types' do
|
63
|
-
|
64
|
-
let(:fix_5) { {data_dictionary: "FIX50SP1.xml", session_dictionary: "FIXT11.xml"} }
|
65
|
-
# data is from: http://fixparser.targetcompid.com/
|
99
|
+
# most data is from: http://fixparser.targetcompid.com/
|
66
100
|
context 'heartbeats' do
|
67
101
|
it 'can parse these' do
|
68
102
|
[fix_4, fix_5].each do |version|
|
@@ -88,8 +122,7 @@ describe LF::FixMessage do
|
|
88
122
|
expect(["Logon", "LOGON"].include?(hash["MsgType"])).to be true
|
89
123
|
|
90
124
|
expect(hash["BeginString"]).to be_a String
|
91
|
-
|
92
|
-
# expect([String, Fixnum].include?(hash["HeartBtInt"].class)).to be true
|
125
|
+
expect([String, Fixnum].include?(hash["HeartBtInt"].class)).to be true
|
93
126
|
|
94
127
|
expect(["BANZAI", "EXEC"].include?(hash["TargetCompID"])).to be true
|
95
128
|
expect(["BANZAI", "EXEC"].include?(hash["SenderCompID"])).to be true
|
@@ -36,14 +36,31 @@ describe LF::FixProtocol do
|
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
|
+
context 'an incoming execution report' do
|
40
|
+
config fix_4_configuration
|
41
|
+
|
42
|
+
execution = "8=FIXT.1.1\x0135=8\x0149=ITG\x0156=SILO\x01315=8\x016=100.25\x01410=50.25\x01424=23.45\x01411=Y\x0143=N\x0140=1\x015=N\x01"
|
43
|
+
|
44
|
+
sample(execution) do
|
45
|
+
filtered_event = subject
|
46
|
+
insist { filtered_event["BeginString"] } == "FIXT.1.1"
|
47
|
+
insist { filtered_event["MsgType"] } == "ExecutionReport"
|
48
|
+
insist { filtered_event["SenderCompID"] } == "ITG"
|
49
|
+
insist { filtered_event["AvgPx"] } == 100.25
|
50
|
+
insist { filtered_event["OrdType"] } == "MARKET"
|
51
|
+
insist { filtered_event["UnderlyingPutOrCall"] } == 8
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
39
55
|
context 'invalid message - Java::Quickfix::InvalidMessage' do
|
40
56
|
config fix_4_configuration
|
41
57
|
|
42
58
|
invalid_msg = "8=invalid_stuff"
|
43
59
|
|
44
60
|
sample(invalid_msg) do
|
45
|
-
|
46
|
-
insist {
|
61
|
+
filtered_event = subject
|
62
|
+
insist { filtered_event["tags"] } == ["_fix_parse_failure"]
|
63
|
+
insist { filtered_event["message"] } == invalid_msg
|
47
64
|
end
|
48
65
|
end
|
49
66
|
|
@@ -53,41 +70,48 @@ describe LF::FixProtocol do
|
|
53
70
|
invalid_msg = "8=FIX.4.09=8135=D34=349garbled=TW52=<TIME>56=ISLD11=ID21=340=154=155=INTC10=0"
|
54
71
|
|
55
72
|
sample(invalid_msg) do
|
56
|
-
|
57
|
-
insist {
|
73
|
+
filtered_event = subject
|
74
|
+
insist { filtered_event["tags"] } == ["_fix_parse_failure"]
|
75
|
+
insist { filtered_event["message"] } == invalid_msg
|
58
76
|
end
|
59
77
|
end
|
60
78
|
|
61
|
-
context '
|
79
|
+
context 'invalid message - group field not found - Java::Quickfix::FieldNotFound' do
|
62
80
|
config fix_4_configuration
|
63
81
|
|
64
|
-
|
82
|
+
invalid_msg = "8=FIX.4.235=D34=249=TW52=<TIME>56=ISLD11=ID21=140=154=138=200.0055=INTC386=3336=PRE-OPEN336=AFTER-HOURS60=<TIME>"
|
65
83
|
|
66
|
-
sample(
|
84
|
+
sample(invalid_msg) do
|
67
85
|
filtered_event = subject
|
68
|
-
|
69
|
-
insist { filtered_event["
|
70
|
-
insist { filtered_event["
|
71
|
-
|
72
|
-
insist { filtered_event["
|
73
|
-
insist { filtered_event["
|
86
|
+
# adds a tag to warn that fields weren't found in the DD
|
87
|
+
insist { filtered_event["tags"] } == ["_fix_field_not_found"]
|
88
|
+
insist { filtered_event["unknown_fields"] } == ["386"]
|
89
|
+
# correctly parses as much as it can
|
90
|
+
insist { filtered_event["OrderQty"]} == 200.0
|
91
|
+
insist { filtered_event["OrdType"]} == "MARKET"
|
92
|
+
insist { filtered_event["Side"]} == "BUY"
|
93
|
+
insist { filtered_event["Symbol"]} == "INTC"
|
94
|
+
insist { filtered_event["NoTradingSessions"]} == [{"TradingSessionID"=>"PRE-OPEN"}, {"TradingSessionID"=>"AFTER-HOURS"}, {"386"=>3}] # here's the rescued group field
|
74
95
|
end
|
75
96
|
end
|
76
97
|
|
77
|
-
context '
|
98
|
+
context 'invalid message - field name not found' do
|
78
99
|
config fix_5_configuration
|
79
100
|
|
80
101
|
execution = "8=FIX.4.2\x019=240\x0135=8\x0134=6\x0149=DUMMY_INC\x0152=20150826-23:10:17.744\x0156=ANOTHER_INC\x0157=Firm_B\x011=Inst_B\x016=0\x0111=151012569\x0117=ITRZ1201508261_24\x0120=0\x0122=8\x0131=1010\x0132=5\x0137=ITRZ1201508261_12\x0138=5\x0139=2\x0140=2\x0141=best_buy\x0144=1011\x0154=1\x0155=ITRZ1\x0160=20150826-23:10:15.547\x01150=2\x01151=0\x0110=227\x01"
|
81
102
|
|
82
103
|
sample(execution) do
|
83
|
-
expect { subject }.to output.to_stdout
|
84
104
|
filtered_event = subject
|
105
|
+
# adds a tag to warn that fields weren't found in the DD
|
106
|
+
insist { filtered_event["tags"] } == ["_fix_field_not_found"]
|
107
|
+
insist { filtered_event["unknown_fields"] } == ["20"]
|
108
|
+
# correctly parses as much as it can
|
85
109
|
insist { filtered_event["BeginString"] } == "FIX.4.2"
|
86
110
|
insist { filtered_event["MsgType"] } == "ExecutionReport"
|
87
111
|
insist { filtered_event["SenderCompID"] } == "DUMMY_INC"
|
88
112
|
insist { filtered_event["AvgPx"] } == 0.0
|
89
113
|
insist { filtered_event["OrdType"] } == "LIMIT"
|
90
|
-
insist { filtered_event["LeavesQty"] } == 0.0
|
114
|
+
insist { filtered_event["LeavesQty"] } == 0.0
|
91
115
|
end
|
92
116
|
end
|
93
117
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-filter-fix_protocol
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Connamara Systems
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-03-
|
11
|
+
date: 2016-03-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|