diff --git a/flow/record/adapter/splunk.py b/flow/record/adapter/splunk.py index daf767e..1300d66 100644 --- a/flow/record/adapter/splunk.py +++ b/flow/record/adapter/splunk.py @@ -28,7 +28,7 @@ [TAG]: optional value to add as "rdtag" output field when writing [TOKEN]: Authentication token for sending data over HTTP(S) [SOURCETYPE]: Set sourcetype of data. Defaults to records, but can also be set to JSON. -[SSL_VERIFY]: Whether to verify the server certificate when sending data over HTTP(S). Defaults to True. +[SSL_VERIFY]: Whether to verify the server certificate when sending data over HTTPS. Defaults to True. """ log = logging.getLogger(__package__) @@ -36,21 +36,38 @@ # Amount of records to bundle into a single request when sending data over HTTP(S). RECORD_BUFFER_LIMIT = 20 -# https://docs.splunk.com/Documentation/Splunk/7.3.1/Data/Configureindex-timefieldextraction -RESERVED_SPLUNK_FIELDS = [ - "_indextime", - "_time", - "index", - "punct", - "source", - "sourcetype", - "tag", - "type", -] +# List of reserved splunk fields that do not start with an `_`, as those will be escaped anyway. +# See: https://docs.splunk.com/Documentation/Splunk/9.2.1/Data/Aboutdefaultfields +RESERVED_SPLUNK_FIELDS = set( + [ + "host", + "index", + "linecount", + "punct", + "source", + "sourcetype", + "splunk_server", + "timestamp", + ], +) + +RESERVED_SPLUNK_APP_FIELDS = set( + [ + "tag", + "type", + ] +) + +RESERVED_RDUMP_FIELDS = set( + [ + "rdtag", + "rdtype", + ], +) -RESERVED_RECORD_FIELDS = ["_classification", "_generated", "_source"] +RESERVED_FIELDS = RESERVED_SPLUNK_FIELDS.union(RESERVED_SPLUNK_APP_FIELDS.union(RESERVED_RDUMP_FIELDS)) -PREFIX_WITH_RD = set(RESERVED_SPLUNK_FIELDS + RESERVED_RECORD_FIELDS) +ESCAPE = "rd_" class Protocol(Enum): @@ -64,7 +81,13 @@ class SourceType(Enum): RECORDS = "records" -def splunkify_key_value(record: Record, tag: Optional[str] = None) -> str: +def escape_field_name(field: str) -> str: + if field.startswith(("_", ESCAPE)) or field in RESERVED_FIELDS: + field = f"{ESCAPE}{field}" + return field + + +def record_to_splunk_kv_line(record: Record, tag: Optional[str] = None) -> str: ret = [] ret.append(f'rdtype="{record._desc.name}"') @@ -81,8 +104,7 @@ def splunkify_key_value(record: Record, tag: Optional[str] = None) -> str: val = getattr(record, field) - if field in PREFIX_WITH_RD: - field = f"rd_{field}" + field = escape_field_name(field) if val is None: ret.append(f"{field}=None") @@ -94,7 +116,25 @@ def splunkify_key_value(record: Record, tag: Optional[str] = None) -> str: return " ".join(ret) -def splunkify_json(packer: JsonRecordPacker, record: Record, tag: Optional[str] = None) -> str: +def record_to_splunk_json(packer: JsonRecordPacker, record: Record, tag: Optional[str] = None) -> dict: + record_as_dict = packer.pack_obj(record) + json_dict = {} + + for field, value in record_as_dict.items(): + # Omit the _version field as the Splunk adapter has no reader support for deserialising records back. + if field == "_version": + continue + escaped_field = escape_field_name(field) + json_dict[escaped_field] = value + + # Add rdump specific fields + json_dict["rdtag"] = tag + json_dict["rdtype"] = record._desc.name + + return json_dict + + +def record_to_splunk_http_api_json(packer: JsonRecordPacker, record: Record, tag: Optional[str] = None) -> str: ret = {} indexer_fields = [ @@ -115,29 +155,13 @@ def splunkify_json(packer: JsonRecordPacker, record: Record, tag: Optional[str] continue ret[splunk_name] = to_str(val) - record_as_dict = packer.pack_obj(record) - - # Omit the _version field as the Splunk adapter has no reader support for deserialising records back. - del record_as_dict["_version"] - - # These fields end up in the 'event', but we have a few reserved field names. If those field names are in the - # record, we prefix them with 'rd_' (short for record descriptor) - for field in PREFIX_WITH_RD: - if field not in record_as_dict: - continue - new_field = f"rd_{field}" - - record_as_dict[new_field] = record_as_dict[field] - del record_as_dict[field] - - # Almost done, just have to add the tag and the type (i.e the record descriptor's name) to the event. - record_as_dict["rdtag"] = tag + ret["event"] = record_to_splunk_json(packer, record, tag) + return json.dumps(ret, default=packer.pack_obj) - # Yes. - record_as_dict["rdtype"] = record._desc.name - ret["event"] = record_as_dict - return json.dumps(ret, default=packer.pack_obj) +def record_to_splunk_tcp_api_json(packer: JsonRecordPacker, record: Record, tag: Optional[str] = None) -> str: + record_dict = record_to_splunk_json(packer, record, tag) + return json.dumps(record_dict, default=packer.pack_obj) class SplunkWriter(AbstractWriter): @@ -159,31 +183,31 @@ def __init__( if sourcetype is None: log.warning("No sourcetype provided, assuming 'records' sourcetype") - sourcetype = SourceType.RECORDS + self.sourcetype = SourceType.RECORDS + else: + self.sourcetype = SourceType(sourcetype) parsed_url = urlparse(uri) url_scheme = parsed_url.scheme.lower() - - self.sourcetype = SourceType(sourcetype) self.protocol = Protocol(url_scheme) - - if self.protocol == Protocol.TCP and self.sourcetype != SourceType.RECORDS: - raise ValueError("For sending data to Splunk over TCP, only the 'records' sourcetype is allowed") - self.host = parsed_url.hostname self.port = parsed_url.port + self.tag = tag self.record_buffer = [] self._warned = False self.packer = None - - if self.sourcetype == SourceType.JSON: - self.packer = JsonRecordPacker(indent=4, pack_descriptors=False) + self.json_converter = None if self.protocol == Protocol.TCP: self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.SOL_TCP) self.sock.connect((self.host, self.port)) self._send = self._send_tcp + + if self.sourcetype == SourceType.JSON: + self.packer = JsonRecordPacker(indent=None, pack_descriptors=False) + self.json_converter = record_to_splunk_tcp_api_json + elif self.protocol in (Protocol.HTTP, Protocol.HTTPS): if not HAS_HTTPX: raise ImportError("The httpx library is required for sending data over HTTP(S)") @@ -214,6 +238,10 @@ def __init__( self._send = self._send_http + if self.sourcetype == SourceType.JSON: + self.packer = JsonRecordPacker(indent=4, pack_descriptors=False) + self.json_converter = record_to_splunk_http_api_json + def _cache_records_for_http(self, data: Optional[bytes] = None, flush: bool = False) -> Optional[bytes]: # It's possible to call this function without any data, purely to flush. Hence this check. if data: @@ -252,9 +280,9 @@ def write(self, record: Record) -> None: ) if self.sourcetype == SourceType.RECORDS: - rec = splunkify_key_value(record, self.tag) + rec = record_to_splunk_kv_line(record, self.tag) else: - rec = splunkify_json(self.packer, record, self.tag) + rec = self.json_converter(self.packer, record, self.tag) # Trail with a newline for line breaking. data = to_bytes(rec) + b"\n" diff --git a/tests/test_splunk_adapter.py b/tests/test_splunk_adapter.py index 40e1460..ada8bc0 100644 --- a/tests/test_splunk_adapter.py +++ b/tests/test_splunk_adapter.py @@ -9,24 +9,30 @@ import flow.record.adapter.splunk from flow.record import RecordDescriptor from flow.record.adapter.splunk import ( + ESCAPE, + RESERVED_FIELDS, Protocol, + SourceType, SplunkWriter, - splunkify_json, - splunkify_key_value, + escape_field_name, + record_to_splunk_http_api_json, + record_to_splunk_kv_line, + record_to_splunk_tcp_api_json, ) from flow.record.jsonpacker import JsonRecordPacker -BASE_FIELD_VALUES = { - "rd__source": None, - "rd__classification": None, - "rd__generated": ANY, +# These base fields are always part of the splunk output. As they are ordered +# and ordered last in the record fields we can append them to any check of the +# splunk output values. +BASE_FIELD_JSON_VALUES = { + f"{ESCAPE}_source": None, + f"{ESCAPE}_classification": None, + f"{ESCAPE}_generated": ANY, } +BASE_FIELDS_KV_SUFFIX = f'{ESCAPE}_source=None {ESCAPE}_classification=None {ESCAPE}_generated="' JSON_PACKER = JsonRecordPacker(pack_descriptors=False) -# Reserved fields is an ordered dict so we can make assertions with a static order of reserved fields. -RESERVED_FIELDS_KEY_VALUE_SUFFIX = 'rd__source=None rd__classification=None rd__generated="' - @pytest.fixture def mock_httpx_package(monkeypatch: pytest.MonkeyPatch) -> Iterator[MagicMock]: @@ -37,246 +43,265 @@ def mock_httpx_package(monkeypatch: pytest.MonkeyPatch) -> Iterator[MagicMock]: yield mock_httpx +escaped_fields = list( + RESERVED_FIELDS.union( + set(["_underscore_field"]), + ), +) + + +@pytest.mark.parametrize( + "field, escaped", list(zip(escaped_fields, [True] * len(escaped_fields))) + [("not_escaped", False)] +) +def test_escape_field_name(field, escaped): + if escaped: + assert escape_field_name(field) == f"{ESCAPE}{field}" + else: + assert escape_field_name(field) == field + + def test_splunkify_reserved_field(): - with patch.object( - flow.record.adapter.splunk, - "PREFIX_WITH_RD", - set(["foo", "_generated", "_classification", "_source"]), - ): - test_record_descriptor = RecordDescriptor( - "test/record", - [("string", "foo")], - ) + test_record_descriptor = RecordDescriptor( + "test/record", + [("string", "rdtag")], + ) - test_record = test_record_descriptor(foo="bar") + test_record = test_record_descriptor(rdtag="bar") - output_key_value = splunkify_key_value(test_record) - output_json = splunkify_json(JSON_PACKER, test_record) + output_key_value = record_to_splunk_kv_line(test_record) + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record) + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record) - assert output_key_value.startswith( - f'rdtype="test/record" rdtag=None rd_foo="bar" {RESERVED_FIELDS_KEY_VALUE_SUFFIX}' - ) + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + f"{ESCAPE}rdtag": "bar", + }, + **BASE_FIELD_JSON_VALUES, + ) - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": None, - "rdtype": "test/record", - "rd_foo": "bar", - }, - **BASE_FIELD_VALUES, - ) - } + assert output_key_value.startswith(f'rdtype="test/record" rdtag=None {ESCAPE}rdtag="bar" {BASE_FIELDS_KV_SUFFIX}') + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict def test_splunkify_normal_field(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): - test_record_descriptor = RecordDescriptor( - "test/record", - [("string", "foo")], - ) + test_record_descriptor = RecordDescriptor( + "test/record", + [("string", "foo")], + ) - test_record = test_record_descriptor(foo="bar") + test_record = test_record_descriptor(foo="bar") - output_key_value = splunkify_key_value(test_record) - output_json = splunkify_json(JSON_PACKER, test_record) - assert output_key_value.startswith( - f'rdtype="test/record" rdtag=None foo="bar" {RESERVED_FIELDS_KEY_VALUE_SUFFIX}' - ) - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": None, - "rdtype": "test/record", - "foo": "bar", - }, - **BASE_FIELD_VALUES, - ) - } + output_key_value = record_to_splunk_kv_line(test_record) + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record) + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record) + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + "foo": "bar", + }, + **BASE_FIELD_JSON_VALUES, + ) -def test_splunkify_source_field(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): - test_record_descriptor = RecordDescriptor( - "test/record", - [("string", "source")], - ) + assert output_key_value.startswith(f'rdtype="test/record" rdtag=None foo="bar" {BASE_FIELDS_KV_SUFFIX}') + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict - test_record = test_record_descriptor(source="file_on_target") - test_record._source = "path_of_target" - output_key_value = splunkify_key_value(test_record) - output_json = splunkify_json(JSON_PACKER, test_record) - assert output_key_value.startswith( - 'rdtype="test/record" rdtag=None rd_source="file_on_target" rd__source="path_of_target"' - ) +def test_splunkify_source_field(): + test_record_descriptor = RecordDescriptor( + "test/record", + [("string", "source")], + ) + + test_record = test_record_descriptor(source="file_on_target") + test_record._source = "path_of_target" + + output_key_value = record_to_splunk_kv_line(test_record) + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record) + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record) + + base_fields_kv_suffix = BASE_FIELDS_KV_SUFFIX.replace( + f"{ESCAPE}_source=None", + f'{ESCAPE}_source="{test_record._source}"', + ) + + base_field_json_values = BASE_FIELD_JSON_VALUES.copy() + base_field_json_values[f"{ESCAPE}_source"] = test_record._source + + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + f"{ESCAPE}source": "file_on_target", + }, + **base_field_json_values, + ) + + assert output_key_value.startswith( + f'rdtype="test/record" rdtag=None {ESCAPE}source="file_on_target" {base_fields_kv_suffix}' + ) + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict - expected_base_field_values = BASE_FIELD_VALUES.copy() - expected_base_field_values["rd__source"] = "path_of_target" - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": None, - "rdtype": "test/record", - "rd_source": "file_on_target", - "rd__source": "path_of_target", - "rd__generated": ANY, - "rd__classification": None, - }, - ), - } +def test_splunkify_rdtag_field(): + test_record_descriptor = RecordDescriptor("test/record", []) + test_record = test_record_descriptor() -def test_splunkify_rdtag_field(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): - test_record_descriptor = RecordDescriptor("test/record", []) + output_key_value = record_to_splunk_kv_line(test_record, tag="bar") + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record, tag="bar") + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record, tag="bar") - test_record = test_record_descriptor() + json_dict = dict( + { + "rdtag": "bar", + "rdtype": "test/record", + }, + **BASE_FIELD_JSON_VALUES, + ) - output_key_value = splunkify_key_value(test_record, tag="bar") - output_json = splunkify_json(JSON_PACKER, test_record, tag="bar") - assert output_key_value.startswith(f'rdtype="test/record" rdtag="bar" {RESERVED_FIELDS_KEY_VALUE_SUFFIX}') - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": "bar", - "rdtype": "test/record", - }, - **BASE_FIELD_VALUES, - ) - } + assert output_key_value.startswith(f'rdtype="test/record" rdtag="bar" {BASE_FIELDS_KV_SUFFIX}') + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict def test_splunkify_none_field(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): - test_record_descriptor = RecordDescriptor( - "test/record", - [("string", "foo")], - ) + test_record_descriptor = RecordDescriptor( + "test/record", + [("string", "foo")], + ) - test_record = test_record_descriptor() + test_record = test_record_descriptor() - output_key_value = splunkify_key_value(test_record) - output_json = splunkify_json(JSON_PACKER, test_record) - assert output_key_value.startswith( - f'rdtype="test/record" rdtag=None foo=None {RESERVED_FIELDS_KEY_VALUE_SUFFIX}' - ) - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": None, - "rdtype": "test/record", - "foo": None, - }, - **BASE_FIELD_VALUES, - ) - } + output_key_value = record_to_splunk_kv_line(test_record) + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record) + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record) + + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + "foo": None, + }, + **BASE_FIELD_JSON_VALUES, + ) + + assert output_key_value.startswith(f'rdtype="test/record" rdtag=None foo=None {BASE_FIELDS_KV_SUFFIX}') + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict def test_splunkify_byte_field(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): - test_record_descriptor = RecordDescriptor( - "test/record", - [("bytes", "foo")], - ) + test_record_descriptor = RecordDescriptor( + "test/record", + [("bytes", "foo")], + ) - test_record = test_record_descriptor(foo=b"bar") + test_record = test_record_descriptor(foo=b"bar") - output_key_value = splunkify_key_value(test_record) - output_json = splunkify_json(JSON_PACKER, test_record) - assert output_key_value.startswith( - f'rdtype="test/record" rdtag=None foo="YmFy" {RESERVED_FIELDS_KEY_VALUE_SUFFIX}' - ) - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": None, - "rdtype": "test/record", - "foo": "YmFy", - }, - **BASE_FIELD_VALUES, - ) - } + output_key_value = record_to_splunk_kv_line(test_record) + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record) + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record) + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + "foo": "YmFy", + }, + **BASE_FIELD_JSON_VALUES, + ) -def test_splunkify_backslash_quote_field(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): - test_record_descriptor = RecordDescriptor( - "test/record", - [("string", "foo")], - ) + assert output_key_value.startswith(f'rdtype="test/record" rdtag=None foo="YmFy" {BASE_FIELDS_KV_SUFFIX}') + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict - test_record = test_record_descriptor(foo=b'\\"') - output = splunkify_key_value(test_record) - output_json = splunkify_json(JSON_PACKER, test_record) - assert output.startswith(f'rdtype="test/record" rdtag=None foo="\\\\\\"" {RESERVED_FIELDS_KEY_VALUE_SUFFIX}') - assert json.loads(output_json) == { - "event": dict( - { - "rdtag": None, - "rdtype": "test/record", - "foo": '\\"', - }, - **BASE_FIELD_VALUES, - ) - } +def test_splunkify_backslash_quote_field(): + test_record_descriptor = RecordDescriptor( + "test/record", + [("string", "foo")], + ) + + test_record = test_record_descriptor(foo=b'\\"') + + output = record_to_splunk_kv_line(test_record) + output_http_json = record_to_splunk_http_api_json(JSON_PACKER, test_record) + output_tcp_json = record_to_splunk_tcp_api_json(JSON_PACKER, test_record) + + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + "foo": '\\"', + }, + **BASE_FIELD_JSON_VALUES, + ) + + assert output.startswith(f'rdtype="test/record" rdtag=None foo="\\\\\\"" {BASE_FIELDS_KV_SUFFIX}') + assert json.loads(output_http_json) == {"event": json_dict} + assert json.loads(output_tcp_json) == json_dict + + +def test_record_to_splunk_http_api_json_special_fields(): + test_record_descriptor = RecordDescriptor( + "test/record", + [ + ("datetime", "ts"), + ("string", "hostname"), + ("string", "foo"), + ], + ) + + # Datetimes should be converted to epoch + test_record = test_record_descriptor(ts=datetime.datetime(1970, 1, 1, 4, 0), hostname="RECYCLOPS", foo="bar") + + output = record_to_splunk_http_api_json(JSON_PACKER, test_record) + assert '"time": 14400.0,' in output + assert '"host": "RECYCLOPS"' in output + + +def test_tcp_protocol_records_sourcetype(): + with patch("socket.socket") as mock_socket: + tcp_writer = SplunkWriter("splunk:1337") + assert tcp_writer.host == "splunk" + assert tcp_writer.port == 1337 + assert tcp_writer.protocol == Protocol.TCP + assert tcp_writer.sourcetype == SourceType.RECORDS + mock_socket.assert_called() + mock_socket.return_value.connect.assert_called_with(("splunk", 1337)) -def test_splunkify_json_special_fields(): - with patch.object( - flow.record.adapter.splunk, - "RESERVED_SPLUNK_FIELDS", - set(), - ): test_record_descriptor = RecordDescriptor( "test/record", - [ - ("datetime", "ts"), - ("string", "hostname"), - ("string", "foo"), - ], + [("string", "foo")], ) - # Datetimes should be converted to epoch - test_record = test_record_descriptor(ts=datetime.datetime(1970, 1, 1, 4, 0), hostname="RECYCLOPS", foo="bar") + test_record = test_record_descriptor(foo="bar") + tcp_writer.write(test_record) - output = splunkify_json(JSON_PACKER, test_record) - assert '"time": 14400.0,' in output - assert '"host": "RECYCLOPS"' in output + args, _ = mock_socket.return_value.sendall.call_args + written_to_splunk = args[0] + assert written_to_splunk.startswith( + b'rdtype="test/record" rdtag=None foo="bar" ' + BASE_FIELDS_KV_SUFFIX.encode() + ) + assert written_to_splunk.endswith(b'"\n') -def test_tcp_protocol(): + +def test_tcp_protocol_json_sourcetype(): with patch("socket.socket") as mock_socket: - tcp_writer = SplunkWriter("splunk:1337") + tcp_writer = SplunkWriter("splunk:1337", sourcetype="json") assert tcp_writer.host == "splunk" assert tcp_writer.port == 1337 assert tcp_writer.protocol == Protocol.TCP + assert tcp_writer.sourcetype == SourceType.JSON mock_socket.assert_called() mock_socket.return_value.connect.assert_called_with(("splunk", 1337)) @@ -292,10 +317,17 @@ def test_tcp_protocol(): args, _ = mock_socket.return_value.sendall.call_args written_to_splunk = args[0] - assert written_to_splunk.startswith( - b'rdtype="test/record" rdtag=None foo="bar" ' + RESERVED_FIELDS_KEY_VALUE_SUFFIX.encode() + json_dict = dict( + { + "rdtag": None, + "rdtype": "test/record", + "foo": "bar", + }, + **BASE_FIELD_JSON_VALUES, ) - assert written_to_splunk.endswith(b'"\n') + + assert json.loads(written_to_splunk) == json_dict + assert written_to_splunk.endswith(b"\n") def test_https_protocol_records_sourcetype(mock_httpx_package: MagicMock): @@ -342,9 +374,7 @@ def test_https_protocol_records_sourcetype(mock_httpx_package: MagicMock): ) _, kwargs = mock_httpx_package.Client.return_value.post.call_args sent_data = kwargs["data"] - assert sent_data.startswith( - b'rdtype="test/record" rdtag=None foo="bar" ' + RESERVED_FIELDS_KEY_VALUE_SUFFIX.encode() - ) + assert sent_data.startswith(b'rdtype="test/record" rdtag=None foo="bar" ' + BASE_FIELDS_KV_SUFFIX.encode()) assert sent_data.endswith(b'"\n') @@ -388,7 +418,7 @@ def test_https_protocol_json_sourcetype(mock_httpx_package: MagicMock): "rdtype": "test/record", "foo": "bar", }, - **BASE_FIELD_VALUES, + **BASE_FIELD_JSON_VALUES, ) } assert json.loads(second_record_json) == { @@ -398,6 +428,6 @@ def test_https_protocol_json_sourcetype(mock_httpx_package: MagicMock): "rdtype": "test/record", "foo": "baz", }, - **BASE_FIELD_VALUES, + **BASE_FIELD_JSON_VALUES, ) }