logstash-filter-fix_protocol 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
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
|