From fba8f623325a29672dda2fb7b079670793af754a Mon Sep 17 00:00:00 2001 From: davidlion Date: Mon, 5 Jun 2023 21:17:49 -0400 Subject: [PATCH 01/27] Large refactor and update - Add all clp-ffi functions - Add reader and writer - Add unit tests - Add formatting and linting - Overhaul doc strings and terminology --- .github/ISSUE_TEMPLATE/bug-report.yml | 40 +++ .github/ISSUE_TEMPLATE/config.yml | 1 + .github/ISSUE_TEMPLATE/feature-request.yml | 23 ++ .github/PULL_REQUEST_TEMPLATE.md | 9 + .github/workflows/build.yml | 61 ++++ .gitignore | 3 + .gitmodules | 3 - BUILD.bazel | 53 +--- LICENSE | 3 +- README.rst | 43 +-- cpp/.clang-format | 147 +++++++++- cpp/.clang-tidy | 23 +- cpp/CMakeLists.txt | 129 ++++++--- cpp/clp | 1 - cpp/src/LogEvent.cpp | 6 - cpp/src/LogEvent.hpp | 13 - cpp/src/ffi_go/LogTypes.hpp | 26 ++ cpp/src/ffi_go/defs.h | 72 +++++ cpp/src/ffi_go/ir/LogTypes.hpp | 79 ++++++ cpp/src/ffi_go/ir/decoder.cpp | 104 +++++++ cpp/src/ffi_go/ir/decoder.h | 87 ++++++ cpp/src/ffi_go/ir/deserializer.cpp | 308 +++++++++++++++++++++ cpp/src/ffi_go/ir/deserializer.h | 157 +++++++++++ cpp/src/ffi_go/ir/encoder.cpp | 134 +++++++++ cpp/src/ffi_go/ir/encoder.h | 97 +++++++ cpp/src/ffi_go/ir/serializer.cpp | 153 ++++++++++ cpp/src/ffi_go/ir/serializer.h | 109 ++++++++ cpp/src/ffi_go/search/wildcard_query.cpp | 30 ++ cpp/src/ffi_go/search/wildcard_query.h | 70 +++++ cpp/src/ir/decoding.cpp | 92 ------ cpp/src/ir/decoding.h | 41 --- cpp/src/ir/encoding.cpp | 116 -------- cpp/src/ir/encoding.h | 51 ---- cpp/src/log_event.h | 14 - cpp/src/message/encoding.cpp | 91 ------ cpp/src/message/encoding.h | 33 --- ffi/BUILD.bazel | 4 - ffi/cgo_amd64.go | 10 - ffi/cgo_arm64.go | 10 - ffi/cgo_external.go | 10 - ffi/ffi.go | 84 ++---- generate.go | 2 +- go.mod | 2 +- include/ffi_go/defs.h | 72 +++++ include/ffi_go/ir/decoder.h | 87 ++++++ include/ffi_go/ir/deserializer.h | 157 +++++++++++ include/ffi_go/ir/encoder.h | 97 +++++++ include/ffi_go/ir/serializer.h | 109 ++++++++ include/ffi_go/search/wildcard_query.h | 70 +++++ ir/BUILD.bazel | 30 +- ir/cgo_amd64.go | 6 +- ir/cgo_arm64.go | 6 +- ir/cgo_defs.go | 98 +++++++ ir/cgo_external.go | 2 +- ir/decoder.go | 217 +++++---------- ir/deserializer.go | 284 +++++++++++++++++++ ir/encoder.go | 205 +++++--------- ir/encoder_test.go | 73 ----- ir/ir.go | 80 +++--- ir/ir_test.go | 215 ++++++++++++++ ir/irerror.go | 22 +- ir/irerror_string.go | 25 +- ir/reader.go | 264 +++++++++++------- ir/reader_test.go | 53 ++-- ir/serder_test.go | 161 +++++++++++ ir/serializer.go | 172 ++++++++++++ ir/writer.go | 135 +++++++++ ir/writeread_test.go | 70 +++++ lib/libclp_ffi_linux_amd64.a | Bin 0 -> 326866 bytes lib/libclp_ffi_linux_amd64.so | Bin 142208 -> 0 bytes message/BUILD.bazel | 30 -- message/cgo_amd64.go | 10 - message/cgo_arm64.go | 10 - message/encoding.go | 145 ---------- message/encoding_test.go | 100 ------- message/msgerror.go | 16 -- message/msgerror_string.go | 25 -- search/BUILD.bazel | 23 ++ search/cgo_amd64.go | 10 + search/cgo_arm64.go | 10 + {message => search}/cgo_external.go | 4 +- search/wildcard_query.go | 89 ++++++ test/BUILD.bazel | 14 - test/finalizers.go | 44 --- 84 files changed, 4147 insertions(+), 1637 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/build.yml delete mode 160000 cpp/clp delete mode 100644 cpp/src/LogEvent.cpp delete mode 100644 cpp/src/LogEvent.hpp create mode 100644 cpp/src/ffi_go/LogTypes.hpp create mode 100644 cpp/src/ffi_go/defs.h create mode 100644 cpp/src/ffi_go/ir/LogTypes.hpp create mode 100644 cpp/src/ffi_go/ir/decoder.cpp create mode 100644 cpp/src/ffi_go/ir/decoder.h create mode 100644 cpp/src/ffi_go/ir/deserializer.cpp create mode 100644 cpp/src/ffi_go/ir/deserializer.h create mode 100644 cpp/src/ffi_go/ir/encoder.cpp create mode 100644 cpp/src/ffi_go/ir/encoder.h create mode 100644 cpp/src/ffi_go/ir/serializer.cpp create mode 100644 cpp/src/ffi_go/ir/serializer.h create mode 100644 cpp/src/ffi_go/search/wildcard_query.cpp create mode 100644 cpp/src/ffi_go/search/wildcard_query.h delete mode 100644 cpp/src/ir/decoding.cpp delete mode 100644 cpp/src/ir/decoding.h delete mode 100644 cpp/src/ir/encoding.cpp delete mode 100644 cpp/src/ir/encoding.h delete mode 100644 cpp/src/log_event.h delete mode 100644 cpp/src/message/encoding.cpp delete mode 100644 cpp/src/message/encoding.h delete mode 100644 ffi/cgo_amd64.go delete mode 100644 ffi/cgo_arm64.go delete mode 100644 ffi/cgo_external.go create mode 100644 include/ffi_go/defs.h create mode 100644 include/ffi_go/ir/decoder.h create mode 100644 include/ffi_go/ir/deserializer.h create mode 100644 include/ffi_go/ir/encoder.h create mode 100644 include/ffi_go/ir/serializer.h create mode 100644 include/ffi_go/search/wildcard_query.h create mode 100644 ir/cgo_defs.go create mode 100644 ir/deserializer.go delete mode 100644 ir/encoder_test.go create mode 100644 ir/ir_test.go create mode 100644 ir/serder_test.go create mode 100644 ir/serializer.go create mode 100644 ir/writer.go create mode 100644 ir/writeread_test.go create mode 100644 lib/libclp_ffi_linux_amd64.a delete mode 100644 lib/libclp_ffi_linux_amd64.so delete mode 100644 message/BUILD.bazel delete mode 100644 message/cgo_amd64.go delete mode 100644 message/cgo_arm64.go delete mode 100644 message/encoding.go delete mode 100644 message/encoding_test.go delete mode 100644 message/msgerror.go delete mode 100644 message/msgerror_string.go create mode 100644 search/BUILD.bazel create mode 100644 search/cgo_amd64.go create mode 100644 search/cgo_arm64.go rename {message => search}/cgo_external.go (71%) create mode 100644 search/wildcard_query.go delete mode 100644 test/BUILD.bazel delete mode 100644 test/finalizers.go diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..32b7d56 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,40 @@ +name: "Bug Report" +description: Report software deficiencies +labels: ["bug"] +body: +- type: markdown + attributes: + value: | + Use this form to report any functional or performance bugs you've found in the software. + + Be sure to check if your [issue](https://github.com/y-scope/clp-ffi-go/issues) has already been reported. + +- type: textarea + attributes: + label: Bug + description: "Describe what's wrong and if applicable, what you expected instead." + validations: + required: true + +- type: input + attributes: + label: clp-ffi-go version + description: "The release version number or development commit hash that has the bug." + placeholder: "Version number or commit hash" + validations: + required: true + +- type: textarea + attributes: + label: Environment + description: "The environment in which you're running/using clp-ffi-go." + placeholder: "OS version, docker version, etc." + validations: + required: true + +- type: textarea + attributes: + label: Reproduction steps + description: "List each step required to reproduce the bug." + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..9484ca6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,23 @@ +name: "Feature/Change Request" +description: Request a feature or change +labels: ["enhancement"] +body: +- type: markdown + attributes: + value: | + Use this form to request a feature/change in the software, or the project as a whole. + +- type: textarea + attributes: + label: Request + description: "Describe your request and why it's important." + validations: + required: true + +- type: textarea + attributes: + label: Possible implementation + description: "Describe any implementations you have in mind." + validations: + required: true + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..9672f6d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +# References + + +# Description + + +# Validation performed + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..70848ac --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +name: Build + +on: + pull_request: + push: + workflow_call: + +jobs: + prebuilt-test: + strategy: + matrix: + # os: [macos-latest, ubuntu-latest] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-go@v4 + + - run: go build ./... + + - run: go test ./... + + build-lint-test: + strategy: + matrix: + # os: [macos-latest, ubuntu-latest] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-go@v4 + + - if: ${{ 'macos-latest' == matrix.os }} + run: | + brew update + brew install llvm + + - name: Remove repo's generated c++ libraries and go code + run: | + rm ./lib/* ./**/*_string.go + + - run: | + go install mvdan.cc/gofumpt@latest + go install github.com/segmentio/golines@latest + go install golang.org/x/tools/cmd/stringer@latest + + - run: go generate ./... + + - run: | + diff="$(golines -m 100 -t 4 --base-formatter='gofumpt' --dry-run .)" + if [[ -n "$diff" ]]; then echo "$diff"; exit 1; fi + + # - run: cmake -S ./cpp -B ./cpp/build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 + + - run: go test ./... diff --git a/.gitignore b/.gitignore index 567609b..cae36d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ build/ +cpp/.cache/ +cpp/clp/ +**/compile_commands.json diff --git a/.gitmodules b/.gitmodules index 22bf0de..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "clp"] - path = cpp/clp - url = git@github.com:y-scope/clp.git diff --git a/BUILD.bazel b/BUILD.bazel index c132f2e..79dee54 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,53 +1,18 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@rules_cc//cc:defs.bzl", "cc_library") cc_library( name = "libclp_ffi", - srcs = select({ - "@io_bazel_rules_go//go/platform:android_amd64": [ - "lib/libclp_ffi_linux_amd64.so", - ], - "@io_bazel_rules_go//go/platform:android_arm64": [ - "lib/libclp_ffi_linux_arm64.so", - ], - "@io_bazel_rules_go//go/platform:darwin_amd64": [ - "lib/libclp_ffi_darwin_amd64.so", - ], - "@io_bazel_rules_go//go/platform:darwin_arm64": [ - "lib/libclp_ffi_darwin_arm64.so", - ], - "@io_bazel_rules_go//go/platform:ios_amd64": [ - "lib/libclp_ffi_darwin_amd64.so", - ], - "@io_bazel_rules_go//go/platform:ios_arm64": [ - "lib/libclp_ffi_darwin_arm64.so", - ], - "@io_bazel_rules_go//go/platform:linux_amd64": [ - "lib/libclp_ffi_linux_amd64.so", - ], - "@io_bazel_rules_go//go/platform:linux_arm64": [ - "lib/libclp_ffi_linux_arm64.so", - ], - "//conditions:default": [], - }), - hdrs = glob([ - "cpp/src/**/*.h", - ]), + srcs = glob(["cpp/src/ffi_go/**"]) + [ + ], + hdrs = glob(["cpp/src/ffi_go/**/*.h"]), includes = [ "cpp/src", ], - visibility = ["//visibility:public"], -) - -go_library( - name = "clp-ffi-go", - srcs = ["generate.go"], - importpath = "github.com/y-scope/clp-ffi-go", - visibility = ["//visibility:public"], -) - -alias( - name = "go_default_library", - actual = ":clp-ffi-go", + deps = [ + "@com_github_y_scope_clp//:libclp_core", + ], + copts = [ + "-std=c++20", + ], visibility = ["//visibility:public"], ) diff --git a/LICENSE b/LICENSE index e1c64ab..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -186,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 YScope Inc. + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.rst b/README.rst index 0893714..a12cc12 100644 --- a/README.rst +++ b/README.rst @@ -15,7 +15,7 @@ Getting started To add the module to your project run: ``go get github.com/y-scope/clp-ffi-go`` Here's an example showing how to decode each log event containing "ERROR" from -a CLP IR stream. +a CLP IR byte stream. .. code:: golang @@ -24,21 +24,29 @@ a CLP IR stream. "time" "github.com/klauspost/compress/zstd" + "github.com/y-scope/clp-ffi-go/ffi" "github.com/y-scope/clp-ffi-go/ir" ) file, _ := os.Open("log-file.clp.zst") + defer file.Close() zstdReader, _ := zstd.NewReader(file) + defer zstdReader.Close() + irReader, _ := ir.NewReader(zstdReader) + defer irReader.Close() - irReader, _ := ir.ReadPreamble(zstdReader, 4096) + var err error for { - // To read every log event replace ReadToContains with - // ReadNextLogEvent(zstdReader) - log, err := irReader.ReadToContains(zstdReader, []byte("ERROR")) - if ir.Eof == err || io.EOF == err { + var log *ffi.LogEventView + // To read every log event replace ReadToContains with Read() + log, err = irReader.ReadToContains("ERROR") + if nil != err { break } - fmt.Printf("%v %v", time.UnixMilli(int64(log.Timestamp)), string(log.Msg)) + fmt.Printf("%v %v", time.UnixMilli(int64(log.Timestamp)), log.LogMessageView) + } + if ir.EndOfIr != err { + fmt.Printf("Reader.Read failed: %v", err) } Building @@ -49,15 +57,11 @@ as well as stringify ``Enum`` style types. 1. Install requirements: a. A C++ compiler that supports C++17 - #. CMake 3.5.1 or higher + #. CMake 3.11 or higher #. The Stringer tool: https://pkg.go.dev/golang.org/x/tools/cmd/stringer - ``go install golang.org/x/tools/cmd/stringer@latest`` -#. ``git submodule update --init --recursive`` - - - Pull all submodules in preparation for building - #. ``go generate ./...`` - Run all generate directives (note the 3 dots after '/') @@ -72,12 +76,12 @@ __ https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_l Testing ------- -To run all unit tests run: ``go test ./... -args $(readlink -f clp-ir-stream.clp.zst)`` +To run all unit tests run: ``go_test_ir="/path/to/my-ir.clp.zst" go test ./...`` -- The ``ir`` package's tests currently requries an existing CLP IR file - compressed with zstd. This file's path is taken as the only argument to the - test and is supplied after ``-args``. It can be an absolute path or a path - relative to the ``ir`` directory. +- Some of the ``ir`` package's tests currently requries an existing CLP IR file + compressed with zstd. This file's path is taken as an environment variable + named ``go_test_ir``. It can be an absolute path or a path relative to the + ``ir`` directory. Why not build with cgo? ''''''''''''''''''''''' @@ -100,5 +104,6 @@ For example, to run the tests using the ``external`` you can run: .. code:: bash - CGO_LDFLAGS="-L./lib -lclp_ffi_linux_amd64 -lstdc++" \ - go test -tags external,test ./... -args $(readlink -f clp-ir-stream.clp.zst) + CGO_LDFLAGS="-L/path/to/external_libs -lclp_ffi_linux_amd64 -Wl,-rpath=/path/to/external_libs" \ + go_test_ir="/path/to/my-ir.clp.zst" \ + go test -tags external ./... diff --git a/cpp/.clang-format b/cpp/.clang-format index e390658..f3fdee1 100644 --- a/cpp/.clang-format +++ b/cpp/.clang-format @@ -1,26 +1,157 @@ --- -BasedOnStyle: LLVM ColumnLimit: 100 IndentWidth: 4 --- Language: Cpp AccessModifierOffset: -4 +AlignAfterOpenBracket: BlockIndent +AlignArrayOfStructures: None +AlignConsecutiveAssignments: None +AlignConsecutiveBitFields: None +AlignConsecutiveDeclarations: None +AlignConsecutiveMacros: None +AlignEscapedNewlines: DontAlign +AlignOperands: Align +AlignTrailingComments: Never +AllowAllArgumentsOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: Yes BinPackArguments: false BinPackParameters: false +BitFieldColonSpacing: Both +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: MultiLine + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterExternBlock: false + AfterStruct: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyNamespace: false + SplitEmptyRecord: false +BreakAfterAttributes: Never +BreakBeforeBinaryOperators: All +BreakBeforeBraces: Custom +BreakBeforeConceptDeclarations: Always +BreakBeforeInlineASMColon: OnlyMultiline +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +CompactNamespaces: true +ConstructorInitializerIndentWidth: 8 ContinuationIndentWidth: 8 +Cpp11BracedListStyle: true +DerivePointerAlignment: false +DisableFormat: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: true +IncludeBlocks: Regroup +IncludeCategories: + # NOTE: A header is grouped by first matching regex + # Project headers + - Regex: '^$' + Priority: 1 + # C++ standard libraries + - Regex: '^<.*>$' + Priority: 2 +IndentAccessModifiers: false +IndentCaseBlocks: false +IndentCaseLabels: true +IndentExternBlock: Indent +IndentGotoLabels: false +IndentPPDirectives: BeforeHash +IndentRequiresClause: false +IndentWrappedFunctionNames: false +InsertBraces: true +InsertNewlineAtEOF: true +IntegerLiteralSeparator: + Binary: 4 + BinaryMinDigits: 4 + Decimal: 3 + DecimalMinDigits: 5 + Hex: 4 + HexMinDigits: 4 +KeepEmptyLinesAtTheStartOfBlocks: false +LambdaBodyIndentation: Signature +LineEnding: LF +MaxEmptyLinesToKeep: 1 NamespaceIndentation: Inner +PPIndentWidth: -1 PackConstructorInitializers: CurrentLine +PenaltyBreakAssignment: 50 +PenaltyBreakOpenParenthesis: 25 +PenaltyBreakBeforeFirstCallParameter: 25 +PenaltyReturnTypeOnItsOwnLine: 100 PointerAlignment: Left -QualifierAlignment: Right -ReflowComments: false +QualifierAlignment: Custom +QualifierOrder: + - static + - friend + - inline + # constexpr west as explained in https://www.youtube.com/watch?v=z6s6bacI424 + - constexpr + - type + - const + - volatile +ReferenceAlignment: Pointer +ReflowComments: true +RemoveBracesLLVM: false +RemoveSemicolon: true +RequiresClausePosition: OwnLine +RequiresExpressionIndentation: OuterScope +SeparateDefinitionBlocks: Always +ShortNamespaceLines: 0 +SortIncludes: CaseInsensitive +SortUsingDeclarations: Lexicographic +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceAroundPointerQualifiers: Default +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true -SpaceBeforeParens: Custom -SpaceBeforeParensOptions: - AfterControlStatements: true -# AfterFunctionDeclarationName: true -# AfterFunctionDefinitionName: true +SpaceBeforeParens: ControlStatements SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: false +SpacesInCStyleCastParentheses: false +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: -1 +SpacesInParentheses: false +SpacesInSquareBrackets: false Standard: Latest +TabWidth: 4 +UseTab: Never diff --git a/cpp/.clang-tidy b/cpp/.clang-tidy index 989dd55..5d30670 100644 --- a/cpp/.clang-tidy +++ b/cpp/.clang-tidy @@ -1,9 +1,25 @@ --- -Checks: 'cert-*,clang-analyzer-*,clang-diagnostic-*,cppcoreguidelines-*,modernize-*,performance-*,readability-*,-readability-identifier-length,-readability-simplify-boolean-expr' +Checks: >- + bugprone-*, + -bugprone-easily-swappable-parameters, + cert-*, + clang-analyzer-*, + clang-diagnostic-*, + concurrency-*, + cppcoreguidelines-*, + misc-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -readability-identifier-length, + -readability-named-parameter, + -readability-simplify-boolean-expr, + +WarningsAsErrors: '*' FormatStyle: file -HeaderFileExtensions: ['','h','hh','hpp','hxx','tpp'] -ImplementationFileExtensions: ['','c','cc','cpp','cxx'] CheckOptions: + misc-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic: true readability-identifier-naming.ClassCase: 'CamelCase' readability-identifier-naming.ClassMemberCase: 'lower_case' readability-identifier-naming.ClassMemberPrefix: 'm_' @@ -23,4 +39,5 @@ CheckOptions: readability-identifier-naming.ParameterCase: 'lower_case' readability-identifier-naming.StructCase: 'CamelCase' readability-identifier-naming.TypedefCase: 'CamelCase' + readability-identifier-naming.TypedefIgnoredRegexp: '[a-z_]+_t' readability-identifier-naming.UnionCase: 'CamelCase' diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 27bb313..7c45667 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -1,18 +1,27 @@ -cmake_minimum_required(VERSION 3.5.1) +cmake_minimum_required(VERSION 3.23) +include(FetchContent) project(clp_ffi LANGUAGES CXX C ) -# Set default build type +# Enable compile commands by default if the generator supports it. +if (NOT CMAKE_EXPORT_COMPILE_COMMANDS AND CMAKE_GENERATOR MATCHES "Ninja|Unix Makefiles") + set(CMAKE_EXPORT_COMPILE_COMMANDS ON CACHE BOOL + "Enable/Disable output of compile commands during generation." FORCE) +endif() + +# Set default build type to Release if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(default_build_type "Release") message(STATUS "No build type specified. Setting to '${default_build_type}'.") set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE) endif() -option(BUILD_SHARED_LIBS "Build using shared libraries" ON) +# Build/package static by default to simplify compatibility in other systems +option(BUILD_SHARED_LIBS "Build using shared libraries" OFF) +# Setup library name based on Go environment variables set by `go generate` set(LIB_NAME "clp_ffi" CACHE STRING "Library name containing os and arch.") if (DEFINED ENV{GOOS}) string(APPEND LIB_NAME "_$ENV{GOOS}") @@ -21,49 +30,97 @@ if (DEFINED ENV{GOARCH}) string(APPEND LIB_NAME "_$ENV{GOARCH}") endif() -add_library(${LIB_NAME} - clp/components/core/src/Defs.h - clp/components/core/src/ffi/ir_stream/encoding_methods.cpp - clp/components/core/src/ffi/ir_stream/encoding_methods.hpp - clp/components/core/src/ffi/ir_stream/decoding_methods.cpp - clp/components/core/src/ffi/ir_stream/decoding_methods.hpp - clp/components/core/src/ffi/encoding_methods.cpp - clp/components/core/src/ffi/encoding_methods.hpp - clp/components/core/src/ffi/encoding_methods.tpp - src/log_event.h - src/LogEvent.cpp - src/LogEvent.hpp - src/ir/encoding.cpp - src/ir/encoding.h - src/ir/decoding.cpp - src/ir/decoding.h - src/message/encoding.cpp - src/message/encoding.h +set(CLP_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/clp" CACHE STRING + "Directory containing CLP source, fetched from github if non existant.") +if (NOT EXISTS ${CLP_SRC_DIR}) + FetchContent_Declare( + clp-core + GIT_REPOSITORY https://github.com/y-scope/clp.git + GIT_TAG 084efa35b7e9a63aecc5e327b97aea2a1cef83bc + SOURCE_DIR ${CLP_SRC_DIR} + ) + message(STATUS "Fetching CLP from github.") + FetchContent_MakeAvailable(clp-core) +endif() + +add_library(${LIB_NAME}) + +set_target_properties(${LIB_NAME} + PROPERTIES + POSITION_INDEPENDENT_CODE ON +) + +# Macro providing the length of the absolute source directory path so we can +# create a relative (rather than absolute) __FILE__ macro +string(LENGTH "${CMAKE_CURRENT_SOURCE_DIR}/" SOURCE_PATH_SIZE) +target_compile_definitions(${LIB_NAME} + PUBLIC + SOURCE_PATH_SIZE=${SOURCE_PATH_SIZE} ) target_compile_features(${LIB_NAME} PRIVATE - cxx_std_17 + cxx_std_20 ) -target_include_directories(${LIB_NAME} +# Set warnings as errors +target_compile_options(${LIB_NAME} PRIVATE - ${CMAKE_SOURCE_DIR} - ${CMAKE_SOURCE_DIR}/src + $<$:/W4 /WX> + $<$>:-Wall -Wextra -Wpedantic -Werror> ) -# target_compile_options(${LIB_NAME} -# PRIVATE -# $<$:/W4 /WX> -# $<$>:-Wall -Wextra -Wpedantic -Werror> -# ) +target_include_directories(${LIB_NAME} + PRIVATE + ${CLP_SRC_DIR}/components/core/submodules + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/src +) -# Macro providing the length of the absolute source directory path so we can -# create a relative (rather than absolute) __FILE__ macro -string(LENGTH "${CMAKE_SOURCE_DIR}/" SOURCE_PATH_SIZE) -target_compile_definitions(${LIB_NAME} +target_sources(${LIB_NAME} PUBLIC PUBLIC - SOURCE_PATH_SIZE=${SOURCE_PATH_SIZE} + FILE_SET HEADERS + BASE_DIRS src/ + FILES + src/ffi_go/defs.h + src/ffi_go/ir/decoder.h + src/ffi_go/ir/deserializer.h + src/ffi_go/ir/encoder.h + src/ffi_go/ir/serializer.h + src/ffi_go/search/wildcard_query.h + PRIVATE + ${CLP_SRC_DIR}/components/core/src/BufferReader.cpp + ${CLP_SRC_DIR}/components/core/src/BufferReader.hpp + ${CLP_SRC_DIR}/components/core/src/Defs.h + ${CLP_SRC_DIR}/components/core/src/ErrorCode.hpp + ${CLP_SRC_DIR}/components/core/src/ReaderInterface.cpp + ${CLP_SRC_DIR}/components/core/src/ReaderInterface.hpp + ${CLP_SRC_DIR}/components/core/src/string_utils.cpp + ${CLP_SRC_DIR}/components/core/src/string_utils.hpp + ${CLP_SRC_DIR}/components/core/src/string_utils.inc + ${CLP_SRC_DIR}/components/core/src/TraceableException.hpp + ${CLP_SRC_DIR}/components/core/src/type_utils.hpp + ${CLP_SRC_DIR}/components/core/src/ffi/encoding_methods.cpp + ${CLP_SRC_DIR}/components/core/src/ffi/encoding_methods.hpp + ${CLP_SRC_DIR}/components/core/src/ffi/encoding_methods.inc + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/byteswap.hpp + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/encoding_methods.cpp + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/encoding_methods.hpp + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/decoding_methods.cpp + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/decoding_methods.hpp + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/decoding_methods.inc + ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/protocol_constants.hpp + src/ffi_go/LogTypes.hpp + src/ffi_go/ir/decoder.cpp + src/ffi_go/ir/deserializer.cpp + src/ffi_go/ir/encoder.cpp + src/ffi_go/ir/LogTypes.hpp + src/ffi_go/ir/serializer.cpp + src/ffi_go/search/wildcard_query.cpp ) -install(TARGETS ${LIB_NAME}) +include(GNUInstallDirs) +install(TARGETS ${LIB_NAME} + ARCHIVE + FILE_SET HEADERS +) diff --git a/cpp/clp b/cpp/clp deleted file mode 160000 index 7e7fbc5..0000000 --- a/cpp/clp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7e7fbc5e412b0c949b707027ea1cdbe14adb29d4 diff --git a/cpp/src/LogEvent.cpp b/cpp/src/LogEvent.cpp deleted file mode 100644 index a9e3d82..0000000 --- a/cpp/src/LogEvent.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include -#include - -void delete_log_event(void* log_event) { - delete reinterpret_cast(log_event); -} diff --git a/cpp/src/LogEvent.hpp b/cpp/src/LogEvent.hpp deleted file mode 100644 index 4ea8beb..0000000 --- a/cpp/src/LogEvent.hpp +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef LOG_EVENT_HPP -#define LOG_EVENT_HPP - -#include - -struct LogEvent { - LogEvent() : msg{} {} - LogEvent(size_t cap) { msg.reserve(cap); } - - std::string msg; -}; - -#endif // LOG_EVENT_HPP diff --git a/cpp/src/ffi_go/LogTypes.hpp b/cpp/src/ffi_go/LogTypes.hpp new file mode 100644 index 0000000..16bc6a2 --- /dev/null +++ b/cpp/src/ffi_go/LogTypes.hpp @@ -0,0 +1,26 @@ +#ifndef FFI_GO_LOG_TYPES_HPP +#define FFI_GO_LOG_TYPES_HPP + +#include + +namespace ffi_go { +/** + * The backing storage for a Go ffi.LogMessageView. + * Mutating it will invalidate the corresponding View (slice) stored in the + * ffi.LogMessageView (without any warning or way to guard in Go). + */ +using LogMessage = std::string; + +/** + * The backing storage for a Go ffi.LogEventView. + * Mutating a field will invalidate the corresponding View (slice) stored in the + * ffi.LogEventView (without any warning or way to guard in Go). + */ +struct LogEvent { + auto reserve(size_t cap) -> void { m_log_message.reserve(cap); } + + LogMessage m_log_message; +}; +} // namespace ffi_go + +#endif // FFI_GO_LOG_TYPES_HPP diff --git a/cpp/src/ffi_go/defs.h b/cpp/src/ffi_go/defs.h new file mode 100644 index 0000000..563c89a --- /dev/null +++ b/cpp/src/ffi_go/defs.h @@ -0,0 +1,72 @@ +#ifndef FFI_GO_DEF_H +#define FFI_GO_DEF_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-using) + +#include +#include +#include + +// TODO: replace with clp c-compatible header once it exists +typedef int64_t epoch_time_ms_t; + +/** + * A span of a bool array passed down through Cgo. + */ +typedef struct { + bool* m_data; + size_t m_size; +} BoolSpan; + +/** + * A span of a byte array passed down through Cgo. + */ +typedef struct { + void* m_data; + size_t m_size; +} ByteSpan; + +/** + * A span of a Go int32 array passed down through Cgo. + */ +typedef struct { + int32_t* m_data; + size_t m_size; +} Int32tSpan; + +/** + * A span of a Go int64 array passed down through Cgo. + */ +typedef struct { + int64_t* m_data; + size_t m_size; +} Int64tSpan; + +/** + * A span of a Go int/C.size_t array passed down through Cgo. + */ +typedef struct { + size_t* m_data; + size_t m_size; +} SizetSpan; + +/** + * A view of a Go string passed down through Cgo. + */ +typedef struct { + char const* m_data; + size_t m_size; +} StringView; + +/** + * A view of a Go ffi.LogEvent passed down through Cgo. + */ +typedef struct { + StringView m_log_message; + epoch_time_ms_t m_timestamp; +} LogEventView; + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_DEF_H diff --git a/cpp/src/ffi_go/ir/LogTypes.hpp b/cpp/src/ffi_go/ir/LogTypes.hpp new file mode 100644 index 0000000..471ec7a --- /dev/null +++ b/cpp/src/ffi_go/ir/LogTypes.hpp @@ -0,0 +1,79 @@ +#ifndef FFI_GO_IR_LOG_TYPES_HPP +#define FFI_GO_IR_LOG_TYPES_HPP + +#include +#include + +#include +#include + +#include + +namespace ffi_go::ir { + +template +[[maybe_unused]] constexpr bool cAlwaysFalse{false}; + +template +struct LogMessage { + auto reserve(size_t cap) -> void { m_logtype.reserve(cap); } + + std::string m_logtype; + std::vector m_vars; + std::vector m_dict_vars; + std::vector m_dict_var_end_offsets; +}; + +/** + * The backing storage for a Go ir.Decoder. + * Mutating a field will invalidate the corresponding View (slice) stored in the + * ir.Decoder (without any warning or way to guard in Go). + */ +struct Decoder { + ffi_go::LogMessage m_log_message; +}; + +/** + * The backing storage for a Go ir.Encoder. + * Mutating a field will invalidate the corresponding View (slice) stored in the + * ir.Encoder (without any warning or way to guard in Go). + */ +template +struct Encoder { + LogMessage m_log_message; +}; + +/** + * The backing storage for a Go ir.Deserializer. + * Mutating a field will invalidate the corresponding View (slice) stored in the + * ir.Deserializer (without any warning or way to guard in Go). + */ +struct Deserializer { + ffi_go::LogEvent m_log_event; + ffi::epoch_time_ms_t m_timestamp{}; +}; + +/** + * The backing storage for a Go ir.Serializer. + * Mutating a field will invalidate the corresponding View (slice) stored in the + * ir.Serializer (without any warning or way to guard in Go). + */ +struct Serializer { + /** + * Reserve capacity for the logtype and ir buffer. + * We reserve 1.5x the size of the log message type as a heuristic for the + * full IR buffer size. The log message type of a log event is not + * guaranteed to be less than or equal to the size of the actual log + * message, but in general this is true. + */ + auto reserve(size_t cap) -> void { + m_logtype.reserve(cap); + m_ir_buf.reserve(cap + cap / 2); + } + + std::string m_logtype; + std::vector m_ir_buf; +}; +} // namespace ffi_go::ir + +#endif // FFI_GO_IR_LOG_TYPES_HPP diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp new file mode 100644 index 0000000..f5fd268 --- /dev/null +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -0,0 +1,104 @@ +#include "decoder.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace ffi_go::ir { +using namespace ffi::ir_stream; + +namespace { + /** + * Generic helper for ir_decoder_decode_*_log_message + */ + template + auto decode_log_message( + StringView logtype, + encoded_var_view_t vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_msg_view + ) -> int { + typedef typename std::conditional< + std::is_same_v, + ffi::eight_byte_encoded_variable_t, + ffi::four_byte_encoded_variable_t>::type encoded_var_t; + Decoder* decoder{static_cast(ir_decoder)}; + ffi_go::LogMessage& log_msg = decoder->m_log_message; + log_msg.reserve(logtype.m_size + dict_vars.m_size); + + IRErrorCode err{IRErrorCode_Success}; + try { + log_msg = ffi::decode_message( + std::string_view(logtype.m_data, logtype.m_size), + vars.m_data, + vars.m_size, + std::string_view(dict_vars.m_data, dict_vars.m_size), + dict_var_end_offsets.m_data, + dict_var_end_offsets.m_size + ); + } catch (ffi::EncodingException& e) { + err = IRErrorCode_Decode_Error; + } + + log_msg_view->m_data = log_msg.data(); + log_msg_view->m_size = log_msg.size(); + return static_cast(err); + } +} // namespace + +extern "C" auto ir_decoder_new() -> void* { + return new Decoder{}; +} + +extern "C" auto ir_decoder_close(void* ir_decoder) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast(ir_decoder); +} + +extern "C" auto ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +) -> int { + return decode_log_message( + logtype, + vars, + dict_vars, + dict_var_end_offsets, + ir_decoder, + log_message + ); +} + +extern "C" auto ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +) -> int { + return decode_log_message( + logtype, + vars, + dict_vars, + dict_var_end_offsets, + ir_decoder, + log_message + ); +} +} // namespace ffi_go::ir diff --git a/cpp/src/ffi_go/ir/decoder.h b/cpp/src/ffi_go/ir/decoder.h new file mode 100644 index 0000000..f1efef7 --- /dev/null +++ b/cpp/src/ffi_go/ir/decoder.h @@ -0,0 +1,87 @@ +#ifndef FFI_GO_IR_DECODER_H +#define FFI_GO_IR_DECODER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) +// NOLINTBEGIN(modernize-use-using) + +#include +#include + +#include + +/** + * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. + * @return New ir::Decoder's address + */ +void* ir_decoder_new(); + +/** + * Clean up the underlying ir::Decoder of a Go ir.Decoder. + * @param[in] ir_encoder Address of a ir::Decoder created and returned by + * ir_decoder_new + */ +void ir_decoder_close(void* decoder); + +/** + * Given the fields of a CLP IR encoded log message with eight byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); + +/** + * Given the fields of a CLP IR encoded log message with four byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_DECODER_H diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp new file mode 100644 index 0000000..b5da8b8 --- /dev/null +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -0,0 +1,308 @@ +#include "deserializer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ffi_go::ir { +using namespace ffi; +using namespace ffi::ir_stream; + +namespace { + /** + * Generic helper for ir_deserializer_deserialize_*_log_event + */ + template + auto deserialize_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event + ) -> int; + + /** + * Generic helper for ir_deserializer_deserialize_*_wildcard_match + */ + template + auto deserialize_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + WildcardQueryView queries, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query + ) -> int; + + template + auto deserialize_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event + ) -> int { + BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; + Deserializer* deserializer{static_cast(ir_deserializer)}; + + IRErrorCode err{}; + epoch_time_ms_t timestamp{}; + if constexpr (std::is_same_v) { + err = eight_byte_encoding::decode_next_message( + ir_buf, + deserializer->m_log_event.m_log_message, + timestamp + ); + } else if constexpr (std::is_same_v) { + epoch_time_ms_t timestamp_delta{}; + err = four_byte_encoding::decode_next_message( + ir_buf, + deserializer->m_log_event.m_log_message, + timestamp_delta + ); + timestamp = deserializer->m_timestamp + timestamp_delta; + } else { + static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); + } + if (IRErrorCode_Success != err) { + return static_cast(err); + } + deserializer->m_timestamp = timestamp; + + size_t pos{0}; + if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { + return static_cast(IRErrorCode_Decode_Error); + } + *ir_pos = pos; + log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); + log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); + log_event->m_timestamp = deserializer->m_timestamp; + return static_cast(IRErrorCode_Success); + } + + template + auto deserialize_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query + ) -> int { + BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; + Deserializer* deserializer{static_cast(ir_deserializer)}; + std::string_view const query_view{ + merged_query.m_queries.m_data, + merged_query.m_queries.m_size}; + std::span const end_offsets{ + merged_query.m_end_offsets.m_data, + merged_query.m_end_offsets.m_size}; + std::span const case_sensitivity{ + merged_query.m_case_sensitivity.m_data, + merged_query.m_case_sensitivity.m_size}; + + std::vector> queries(merged_query.m_end_offsets.m_size); + size_t pos{0}; + for (size_t i{0}; i < merged_query.m_end_offsets.m_size; i++) { + queries[i].first = query_view.substr(pos, end_offsets[i]); + queries[i].second = case_sensitivity[i]; + pos += end_offsets[i]; + } + + std::function(ffi_go::LogMessage const&)> query_fn; + if (false == queries.empty()) { + query_fn = [&](ffi_go::LogMessage const& log_message) -> std::pair { + auto const found_query = std::find_if( + queries.cbegin(), + queries.cend(), + [&](std::pair const& query) -> bool { + return wildcard_match_unsafe(log_message, query.first, query.second); + } + ); + return {queries.cend() != found_query, found_query - queries.cbegin()}; + }; + } else { + query_fn = [](ffi_go::LogMessage const&) -> std::pair { + return {true, 0}; + }; + } + + IRErrorCode err{}; + while (true) { + epoch_time_ms_t timestamp{}; + if constexpr (std::is_same_v) { + err = eight_byte_encoding::decode_next_message( + ir_buf, + deserializer->m_log_event.m_log_message, + timestamp + ); + } else if constexpr (std::is_same_v) { + epoch_time_ms_t timestamp_delta{}; + err = four_byte_encoding::decode_next_message( + ir_buf, + deserializer->m_log_event.m_log_message, + timestamp_delta + ); + timestamp = deserializer->m_timestamp + timestamp_delta; + } else { + static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); + } + if (IRErrorCode_Success != err) { + return static_cast(err); + } + deserializer->m_timestamp = timestamp; + + if (time_interval.m_upper <= deserializer->m_timestamp) { + // TODO this is an extremely fragile hack until the CLP ffi ir + // code is refactored and IRErrorCode includes things beyond + // decoding. + return static_cast(IRErrorCode_Incomplete_IR + 1); + } + if (time_interval.m_lower > deserializer->m_timestamp) { + continue; + } + std::pair const match{query_fn(deserializer->m_log_event.m_log_message)}; + if (match.first) { + size_t pos{0}; + if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { + return static_cast(IRErrorCode_Decode_Error); + } + *ir_pos = pos; + log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); + log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); + log_event->m_timestamp = deserializer->m_timestamp; + *matching_query = match.second; + return static_cast(IRErrorCode_Success); + } + } + } +} // namespace + +extern "C" auto ir_deserializer_close(void* ir_deserializer) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast(ir_deserializer); +} + +extern "C" auto ir_deserializer_deserialize_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr +) -> int { + BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; + + bool four_byte_encoding{}; + if (IRErrorCode const err{get_encoding_type(ir_buf, four_byte_encoding)}; + IRErrorCode_Success != err) + { + return static_cast(err); + } + *ir_encoding = four_byte_encoding ? 1 : 0; + + if (IRErrorCode const err{ + decode_preamble(ir_buf, *metadata_type, *metadata_pos, *metadata_size)}; + IRErrorCode_Success != err) + { + return static_cast(err); + } + + size_t pos{0}; + if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { + return static_cast(IRErrorCode_Decode_Error); + } + *ir_pos = pos; + auto* deserializer{new Deserializer()}; + *ir_deserializer_ptr = deserializer; + *timestamp_ptr = &deserializer->m_timestamp; + return static_cast(IRErrorCode_Success); +} + +extern "C" auto ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +) -> int { + return deserialize_log_event( + ir_view, + ir_deserializer, + ir_pos, + log_event + ); +} + +extern "C" auto ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +) -> int { + return deserialize_log_event( + ir_view, + ir_deserializer, + ir_pos, + log_event + ); +} + +extern "C" auto ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +) -> int { + return deserialize_wildcard_match( + ir_view, + ir_deserializer, + time_interval, + merged_query, + ir_pos, + log_event, + matching_query + ); +} + +extern "C" auto ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +) -> int { + return deserialize_wildcard_match( + ir_view, + ir_deserializer, + time_interval, + merged_query, + ir_pos, + log_event, + matching_query + ); +} +} // namespace ffi_go::ir diff --git a/cpp/src/ffi_go/ir/deserializer.h b/cpp/src/ffi_go/ir/deserializer.h new file mode 100644 index 0000000..572c056 --- /dev/null +++ b/cpp/src/ffi_go/ir/deserializer.h @@ -0,0 +1,157 @@ +#ifndef FFI_GO_IR_DESERIALIZER_H +#define FFI_GO_IR_DESERIALIZER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) + +#include +#include + +#include +#include + +/** + * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. + * @param[in] ir_deserializer The address of a ir::Deserializer created and + * returned by ir_deserializer_deserialize_preamble + */ +void ir_deserializer_close(void* ir_deserializer); + +/** + * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and + * extract its information. An ir::Deserializer will be allocated to use as the + * backing storage for a Go ir.Deserializer (i.e. subsequent calls to + * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read + * the metadata based on the returned type. All pointer parameters must be + * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[out] ir_pos Position in ir_view read to + * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) + * @param[out] metadata_type Type of metadata in preamble (e.g. json) + * @param[out] metadata_pos Position in ir_view where the metadata begins + * @param[out] metadata_size Size of the metadata (in bytes) + * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer + * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer + * to be filled in by Go using the metadata contents + * @return ffi::ir_stream::IRErrorCode forwarded from either + * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble + */ +int ir_deserializer_deserialize_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr +); + +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::decode_next_message + */ +int ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); + +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + */ +int ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); + +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event until finding an event that is both within the time interval and + * matches any query. If queries is empty, the first log event within the time + * interval is treated as a match. Returns the components of the found log event + * and the buffer position it ends at. All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the first matching query or + * 0 if queries is empty + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +int ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); + +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log event + * until finding an event that is both within the time interval and matches any + * query. If queries is empty, the first log event within the time interval is + * treated as a match. Returns the components of the found log event and the + * buffer position it ends at. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the matching query + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +int ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); + +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_DESERIALIZER_H diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp new file mode 100644 index 0000000..54ae633 --- /dev/null +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -0,0 +1,134 @@ +#include "encoder.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace ffi_go::ir { +using namespace ffi::ir_stream; + +namespace { + /** + * Generic helper for ir_encoder_encode_*_log_message + */ + template + auto encode_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + encoded_var_view_t* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets + ) -> int { + typedef typename std::conditional< + std::is_same_v, + ffi::eight_byte_encoded_variable_t, + ffi::four_byte_encoded_variable_t>::type encoded_var_t; + Encoder* encoder{static_cast*>(ir_encoder)}; + LogMessage& ir_log_msg = encoder->m_log_message; + ir_log_msg.reserve(log_message.m_size); + + std::string_view const log_msg_view{log_message.m_data, log_message.m_size}; + std::vector dict_var_offsets; + if (false + == ffi::encode_message( + log_msg_view, + ir_log_msg.m_logtype, + ir_log_msg.m_vars, + dict_var_offsets + )) + { + return static_cast(IRErrorCode_Corrupted_IR); + } + + // dict_var_offsets contains begin_pos followed by end_pos of each + // dictionary variable in the message + int32_t prev_end_off = 0; + for (size_t i = 0; i < dict_var_offsets.size(); i += 2) { + int32_t const begin_pos = dict_var_offsets[i]; + int32_t const end_pos = dict_var_offsets[i + 1]; + ir_log_msg.m_dict_vars.insert( + ir_log_msg.m_dict_vars.begin() + prev_end_off, + log_msg_view.begin() + begin_pos, + log_msg_view.begin() + end_pos + ); + prev_end_off = prev_end_off + (end_pos - begin_pos); + ir_log_msg.m_dict_var_end_offsets.push_back(prev_end_off); + } + + logtype->m_data = ir_log_msg.m_logtype.data(); + logtype->m_size = ir_log_msg.m_logtype.size(); + vars->m_data = ir_log_msg.m_vars.data(); + vars->m_size = ir_log_msg.m_vars.size(); + dict_vars->m_data = ir_log_msg.m_dict_vars.data(); + dict_vars->m_size = ir_log_msg.m_dict_vars.size(); + dict_var_end_offsets->m_data = ir_log_msg.m_dict_var_end_offsets.data(); + dict_var_end_offsets->m_size = ir_log_msg.m_dict_var_end_offsets.size(); + return static_cast(IRErrorCode_Success); + } +} // namespace + +extern "C" auto ir_encoder_eight_byte_new() -> void* { + return new Encoder{}; +} + +extern "C" auto ir_encoder_four_byte_new() -> void* { + return new Encoder{}; +} + +extern "C" auto ir_encoder_eight_byte_close(void* ir_encoder) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast*>(ir_encoder); +} + +extern "C" auto ir_encoder_four_byte_close(void* ir_encoder) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast*>(ir_encoder); +} + +extern "C" auto ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars_ptr, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +) -> int { + return encode_log_message( + log_message, + ir_encoder, + logtype, + vars_ptr, + dict_vars, + dict_var_end_offsets + ); +} + +extern "C" auto ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +) -> int { + return encode_log_message( + log_message, + ir_encoder, + logtype, + vars, + dict_vars, + dict_var_end_offsets + ); +} +} // namespace ffi_go::ir diff --git a/cpp/src/ffi_go/ir/encoder.h b/cpp/src/ffi_go/ir/encoder.h new file mode 100644 index 0000000..aafa2a4 --- /dev/null +++ b/cpp/src/ffi_go/ir/encoder.h @@ -0,0 +1,97 @@ +#ifndef FFI_GO_IR_ENCODER_H +#define FFI_GO_IR_ENCODER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) +// NOLINTBEGIN(modernize-use-using) + +#include +#include + +#include + +/** + * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. + * @return New ir::Encoder's address + */ +void* ir_encoder_eight_byte_new(); + +/** + * @copydoc ir_encoder_eight_byte_new() + */ +void* ir_encoder_four_byte_new(); + +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_eight_byte_new + */ +void ir_encoder_eight_byte_close(void* ir_encoder); + +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_four_byte_new + */ +void ir_encoder_four_byte_close(void* ir_encoder); + +/** + * Given a log message, encode it into a CLP IR object with eight byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); + +/** + * Given a log message, encode it into a CLP IR object with four byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_ENCODER_H diff --git a/cpp/src/ffi_go/ir/serializer.cpp b/cpp/src/ffi_go/ir/serializer.cpp new file mode 100644 index 0000000..bf0ffaf --- /dev/null +++ b/cpp/src/ffi_go/ir/serializer.cpp @@ -0,0 +1,153 @@ +#include "serializer.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +namespace ffi_go::ir { +using namespace ffi; +using namespace ffi::ir_stream; + +namespace { + /** + * Generic helper for ir_serializer_serialize_*_log_event + */ + template + auto serialize_log_event( + StringView log_message, + epoch_time_ms_t timestamp_or_delta, + void* ir_serializer, + ByteSpan* ir_view + ) -> int { + Serializer* serializer{static_cast(ir_serializer)}; + serializer->m_ir_buf.clear(); + + bool success{false}; + if constexpr (std::is_same_v) { + success = eight_byte_encoding::encode_message( + timestamp_or_delta, + std::string_view{log_message.m_data, log_message.m_size}, + serializer->m_logtype, + serializer->m_ir_buf + ); + } else if constexpr (std::is_same_v) { + success = four_byte_encoding::encode_message( + timestamp_or_delta, + std::string_view{log_message.m_data, log_message.m_size}, + serializer->m_logtype, + serializer->m_ir_buf + ); + } else { + static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); + } + if (false == success) { + return static_cast(IRErrorCode_Corrupted_IR); + } + + ir_view->m_data = serializer->m_ir_buf.data(); + ir_view->m_size = serializer->m_ir_buf.size(); + return static_cast(IRErrorCode_Success); + } +} // namespace + +extern "C" auto ir_serializer_close(void* ir_serializer) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast(ir_serializer); +} + +extern "C" auto ir_serializer_serialize_eight_byte_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view +) -> int { + Serializer* serializer{new Serializer{}}; + *ir_serializer_ptr = serializer; + if (false + == eight_byte_encoding::encode_preamble( + std::string_view{ts_pattern.m_data, ts_pattern.m_size}, + std::string_view{ts_pattern_syntax.m_data, ts_pattern_syntax.m_size}, + std::string_view{time_zone_id.m_data, time_zone_id.m_size}, + serializer->m_ir_buf + )) + { + return static_cast(IRErrorCode_Corrupted_IR); + } + + ir_view->m_data = serializer->m_ir_buf.data(); + ir_view->m_size = serializer->m_ir_buf.size(); + return static_cast(IRErrorCode_Success); +} + +extern "C" auto ir_serializer_serialize_four_byte_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +) -> int { + if (nullptr == ir_serializer_ptr || nullptr == ir_view) { + return static_cast(IRErrorCode_Corrupted_IR); + } + Serializer* serializer{new Serializer{}}; + if (nullptr == serializer) { + return static_cast(IRErrorCode_Corrupted_IR); + } + *ir_serializer_ptr = serializer; + if (false + == four_byte_encoding::encode_preamble( + std::string_view{ts_pattern.m_data, ts_pattern.m_size}, + std::string_view{ts_pattern_syntax.m_data, ts_pattern_syntax.m_size}, + std::string_view{time_zone_id.m_data, time_zone_id.m_size}, + reference_ts, + serializer->m_ir_buf + )) + { + return static_cast(IRErrorCode_Corrupted_IR); + } + + ir_view->m_data = serializer->m_ir_buf.data(); + ir_view->m_size = serializer->m_ir_buf.size(); + return static_cast(IRErrorCode_Success); +} + +extern "C" auto ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view +) -> int { + return serialize_log_event( + log_message, + timestamp, + ir_serializer, + ir_view + ); +} + +extern "C" auto ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view +) -> int { + return serialize_log_event( + log_message, + timestamp_delta, + ir_serializer, + ir_view + ); +} +} // namespace ffi_go::ir diff --git a/cpp/src/ffi_go/ir/serializer.h b/cpp/src/ffi_go/ir/serializer.h new file mode 100644 index 0000000..0e200a8 --- /dev/null +++ b/cpp/src/ffi_go/ir/serializer.h @@ -0,0 +1,109 @@ +#ifndef FFI_GO_IR_SERIALIZER_H +#define FFI_GO_IR_SERIALIZER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) + +#include +#include + +#include + +/** + * Clean up the underlying ir::Serializer of a Go ir.Serializer. + * @param[in] ir_serializer Address of a ir::Serializer created and returned by + * ir_serializer_serialize_*_preamble + */ +void ir_serializer_close(void* ir_serializer); + +/** + * Given the fields of a CLP IR premable, serialize them into an IR byte stream + * with eight byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::eight_byte_encoding::encode_preamble + */ +int ir_serializer_serialize_eight_byte_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view +); + +/** + * Given the fields of a CLP IR premable, serialize them into an IR byte stream + * with four byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::four_byte_encoding::encode_preamble + */ +int ir_serializer_serialize_four_byte_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +); + +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * eight byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message of the log event to serialize + * @param[in] timestamp Timestamp of the log event to serialize + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::eight_byte_encoding::encode_message + */ +int ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view +); + +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * four byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to serialize + * @param[in] timestamp_delta Timestamp delta to the previous log event in the + * IR stream + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::four_byte_encoding::encode_message + */ +int ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view +); + +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_SERIALIZER_H diff --git a/cpp/src/ffi_go/search/wildcard_query.cpp b/cpp/src/ffi_go/search/wildcard_query.cpp new file mode 100644 index 0000000..f3f4380 --- /dev/null +++ b/cpp/src/ffi_go/search/wildcard_query.cpp @@ -0,0 +1,30 @@ +#include "wildcard_query.h" + +#include +#include + +#include + +#include + +namespace ffi_go::search { +extern "C" auto wildcard_query_delete(void* str) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast(str); +} + +extern "C" auto wildcard_query_clean(StringView query, void** ptr) -> StringView { + auto* clean = new std::string{ + clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size})}; + *ptr = clean; + return {clean->data(), clean->size()}; +} + +extern "C" auto wildcard_query_match(StringView target, WildcardQueryView query) -> int { + return static_cast(wildcard_match_unsafe( + std::string_view{target.m_data, target.m_size}, + std::string_view{query.m_query.m_data, query.m_query.m_size}, + static_cast(query.m_case_sensitive) + )); +} +} // namespace ffi_go::search diff --git a/cpp/src/ffi_go/search/wildcard_query.h b/cpp/src/ffi_go/search/wildcard_query.h new file mode 100644 index 0000000..f995e29 --- /dev/null +++ b/cpp/src/ffi_go/search/wildcard_query.h @@ -0,0 +1,70 @@ +#ifndef FFI_GO_IR_WILDCARD_QUERY_H +#define FFI_GO_IR_WILDCARD_QUERY_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) +// NOLINTBEGIN(modernize-use-using) + +#include + +#include + +/** + * A timestamp interval of [m_lower, m_upper). + */ +typedef struct { + epoch_time_ms_t m_lower; + epoch_time_ms_t m_upper; +} TimestampInterval; + +/** + * A view of a wildcard query passed down from Go. The query string is assumed + * to have been cleaned using the CLP function `clean_up_wildcard_search_string` + * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case + * insensitive). + */ +typedef struct { + StringView m_query; + bool m_case_sensitive; +} WildcardQueryView; + +/** + * A view of a Go search.MergedWildcardQuery passed down through Cgo. The + * string is a concatenation of all wildcard queries, while m_end_offsets stores + * the size of each query. + */ +typedef struct { + StringView m_queries; + SizetSpan m_end_offsets; + BoolSpan m_case_sensitivity; +} MergedWildcardQueryView; + +/** + * Delete a std::string holding a wildcard query. + * @param[in] str Address of a std::string created and returned by + * clean_wildcard_query + */ +void wildcard_query_delete(void* str); + +/** + * Given a query string, clean it to be safe for matching. See + * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ +StringView wildcard_query_clean(StringView query, void** ptr); + +/** + * Given a target string perform CLP wildcard matching using query. See + * `wildcard_match_unsafe` in CLP src/string_utils.hpp. + * @param[in] target String to perform matching on + * @param[in] query Query to use for matching + * @return 1 if query matches target, 0 otherwise + */ +int wildcard_query_match(StringView target, WildcardQueryView query); + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_WILDCARD_QUERY_H diff --git a/cpp/src/ir/decoding.cpp b/cpp/src/ir/decoding.cpp deleted file mode 100644 index 7ec821f..0000000 --- a/cpp/src/ir/decoding.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "encoding.h" -#include -#include -#include - -#include -#include -#include - -using namespace ffi::ir_stream; - -int decode_preamble(void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - int8_t* ir_encoding, - int8_t* metadata_type, - size_t* metadata_pos, - uint16_t* metadata_size) { - IrBuffer ir_buf{reinterpret_cast(buf_ptr), buf_size}; - ir_buf.set_cursor_pos(*buf_offset); - - bool four_byte_encoding; - if (IRErrorCode err{get_encoding_type(ir_buf, four_byte_encoding)}; - IRErrorCode_Success != err) { - return static_cast(err); - } - *ir_encoding = four_byte_encoding ? 1 : 0; - - if (IRErrorCode err{decode_preamble(ir_buf, *metadata_type, *metadata_pos, *metadata_size)}; - IRErrorCode_Success != err) { - return static_cast(err); - } - - *buf_offset = ir_buf.get_cursor_pos(); - return static_cast(IRErrorCode_Success); -} - -int decode_next_log_event(IRErrorCode (*decode_fp)(IrBuffer&, std::string&, epoch_time_ms_t&), - void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - void** log_event_ptr, - char** log_event, - size_t* log_event_size, - epoch_time_ms_t* timestamp) { - IrBuffer ir_buf{reinterpret_cast(buf_ptr), buf_size}; - ir_buf.set_cursor_pos(*buf_offset); - auto event = std::make_unique(buf_size); - if (IRErrorCode err{decode_fp(ir_buf, event->msg, *timestamp)}; IRErrorCode_Success != err) { - return static_cast(err); - } - *buf_offset = ir_buf.get_cursor_pos(); - - *log_event = event->msg.data(); - *log_event_size = event->msg.size(); - *log_event_ptr = event.release(); - return static_cast(IRErrorCode_Success); -} - -int eight_byte_decode_next_log_event(void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - void** log_event_ptr, - char** log_event, - size_t* log_event_size, - epoch_time_ms_t* timestamp) { - return decode_next_log_event(eight_byte_encoding::decode_next_message, - buf_ptr, - buf_size, - buf_offset, - log_event_ptr, - log_event, - log_event_size, - timestamp); -} - -int four_byte_decode_next_log_event(void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - void** log_event_ptr, - char** log_event, - size_t* log_event_size, - epoch_time_ms_t* timestamp_delta) { - return decode_next_log_event(four_byte_encoding::decode_next_message, - buf_ptr, - buf_size, - buf_offset, - log_event_ptr, - log_event, - log_event_size, - timestamp_delta); -} diff --git a/cpp/src/ir/decoding.h b/cpp/src/ir/decoding.h deleted file mode 100644 index e0e1632..0000000 --- a/cpp/src/ir/decoding.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef IR_DECODING_H -#define IR_DECODING_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -typedef int64_t epoch_time_ms_t; - -int decode_preamble(void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - int8_t* ir_encoding, - int8_t* metadata_type, - size_t* metadata_pos, - uint16_t* metadata_size); - -int eight_byte_decode_next_log_event(void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - void** decoded_message_ptr, - char** message, - size_t* message_size, - epoch_time_ms_t* timestamp); - -int four_byte_decode_next_log_event(void* buf_ptr, - size_t buf_size, - size_t* buf_offset, - void** message_ptr, - char** message, - size_t* message_size, - epoch_time_ms_t* timestamp_delta); - -#ifdef __cplusplus -} -#endif - -#endif // IR_DECODING_H diff --git a/cpp/src/ir/encoding.cpp b/cpp/src/ir/encoding.cpp deleted file mode 100644 index b7070cf..0000000 --- a/cpp/src/ir/encoding.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include -#include -#include - -#include -#include -#include - -struct IrStreamState { - std::string logtype; - std::vector ir_buf; -}; - -void* eight_byte_encode_preamble(void* ts_pattern_ptr, - size_t ts_pattern_size, - void* ts_pattern_syntax_ptr, - size_t ts_pattern_syntax_size, - void* time_zone_id_ptr, - size_t time_zone_id_size, - void** ir_buf_ptr, - void* ir_buf_size) { - std::string_view ts_pattern(reinterpret_cast(ts_pattern_ptr), ts_pattern_size); - std::string_view ts_pattern_syntax(reinterpret_cast(ts_pattern_syntax_ptr), - ts_pattern_syntax_size); - std::string_view time_zone_id(reinterpret_cast(time_zone_id_ptr), - time_zone_id_size); - - IrStreamState* irs = new IrStreamState(); - if (false == ffi::ir_stream::eight_byte_encoding::encode_preamble( - ts_pattern, ts_pattern_syntax, time_zone_id, irs->ir_buf)) { - delete irs; - return nullptr; - } - - *ir_buf_ptr = irs->ir_buf.data(); - *static_cast(ir_buf_size) = irs->ir_buf.size(); - return irs; -} - -void* four_byte_encode_preamble(void* ts_pattern_ptr, - size_t ts_pattern_size, - void* ts_pattern_syntax_ptr, - size_t ts_pattern_syntax_size, - void* time_zone_id_ptr, - size_t time_zone_id_size, - ffi::epoch_time_ms_t reference_ts, - void** ir_buf_ptr, - void* ir_buf_size) { - std::string_view ts_pattern(reinterpret_cast(ts_pattern_ptr), ts_pattern_size); - std::string_view ts_pattern_syntax(reinterpret_cast(ts_pattern_syntax_ptr), - ts_pattern_syntax_size); - std::string_view time_zone_id(reinterpret_cast(time_zone_id_ptr), - time_zone_id_size); - - IrStreamState* irs = new IrStreamState(); - if (false == ffi::ir_stream::four_byte_encoding::encode_preamble( - ts_pattern, ts_pattern_syntax, time_zone_id, reference_ts, irs->ir_buf)) { - delete irs; - return nullptr; - } - - *ir_buf_ptr = irs->ir_buf.data(); - *static_cast(ir_buf_size) = irs->ir_buf.size(); - return irs; -} - -int encode_message( - bool (*em_fp)(ffi::epoch_time_ms_t, std::string_view, std::string&, std::vector&), - void* irstream, - ffi::epoch_time_ms_t timestamp_or_delta, - void* message_ptr, - size_t message_size, - void** ir_buf_ptr, - void* ir_buf_size) { - IrStreamState* irs(reinterpret_cast(irstream)); - std::string_view message(reinterpret_cast(message_ptr), message_size); - irs->ir_buf.clear(); - if (false == em_fp(timestamp_or_delta, message, irs->logtype, irs->ir_buf)) { - return -1; - } - *ir_buf_ptr = irs->ir_buf.data(); - *static_cast(ir_buf_size) = irs->ir_buf.size(); - return 0; -} - -int eight_byte_encode_message(void* irstream, - ffi::epoch_time_ms_t timestamp, - void* message_ptr, - size_t message_size, - void** ir_buf_ptr, - void* ir_buf_size) { - return encode_message(ffi::ir_stream::eight_byte_encoding::encode_message, - irstream, - timestamp, - message_ptr, - message_size, - ir_buf_ptr, - ir_buf_size); -} - -int four_byte_encode_message(void* irstream, - ffi::epoch_time_ms_t timestamp_delta, - void* message_ptr, - size_t message_size, - void** ir_buf_ptr, - void* ir_buf_size) { - return encode_message(ffi::ir_stream::four_byte_encoding::encode_message, - irstream, - timestamp_delta, - message_ptr, - message_size, - ir_buf_ptr, - ir_buf_size); -} - -void delete_ir_stream_state(void* irs) { delete (IrStreamState*)irs; } diff --git a/cpp/src/ir/encoding.h b/cpp/src/ir/encoding.h deleted file mode 100644 index 774399f..0000000 --- a/cpp/src/ir/encoding.h +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef IR_ENCODING_H -#define IR_ENCODING_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -// TODO: replace with clp c-compatible header once it is created -typedef int64_t epoch_time_ms_t; - -void* eight_byte_encode_preamble(void* ts_pattern_ptr, - size_t ts_pattern_size, - void* ts_pattern_syntax_ptr, - size_t ts_pattern_syntax_size, - void* time_zone_id_ptr, - size_t time_zone_id_size, - void** ir_buf_ptr, - void* ir_buf_size); -void* four_byte_encode_preamble(void* ts_pattern_ptr, - size_t ts_pattern_size, - void* ts_pattern_syntax_ptr, - size_t ts_pattern_syntax_size, - void* time_zone_id_ptr, - size_t time_zone_id_size, - epoch_time_ms_t reference_ts, - void** ir_buf_ptr, - void* ir_buf_size); - -int eight_byte_encode_message(void* irstream, - epoch_time_ms_t timestamp, - void* message_ptr, - size_t message_size, - void** ir_buf_ptr, - void* ir_buf_size); -int four_byte_encode_message(void* irstream, - epoch_time_ms_t timestamp_delta, - void* message_ptr, - size_t message_size, - void** ir_buf_ptr, - void* ir_buf_size); - -void delete_ir_stream_state(void* irs); - -#ifdef __cplusplus -} -#endif - -#endif // IR_ENCODING_H diff --git a/cpp/src/log_event.h b/cpp/src/log_event.h deleted file mode 100644 index f130477..0000000 --- a/cpp/src/log_event.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef LOG_EVENT_H -#define LOG_EVENT_H - -#ifdef __cplusplus -extern "C" { -#endif - -void delete_log_event(void* log_event); - -#ifdef __cplusplus -} -#endif - -#endif // LOG_EVENT_H diff --git a/cpp/src/message/encoding.cpp b/cpp/src/message/encoding.cpp deleted file mode 100644 index f6c868d..0000000 --- a/cpp/src/message/encoding.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -// Note: dict_var_end_offsets is int32_t due to JNI putting limitations on -// encode_message. -struct EncodedMessage { - std::string logtype; - std::vector vars; - std::vector dict_vars; - std::vector dict_var_end_offsets; -}; - -int decode_message(void* encoded_msg, - void** log_event_ptr, - char** log_event, - size_t* log_event_size) { - EncodedMessage* em(reinterpret_cast(encoded_msg)); - auto event = std::make_unique(em->logtype.size() * 2); - event->msg = ffi::decode_message( - em->logtype, - em->vars.data(), - em->vars.size(), - std::string_view(reinterpret_cast(em->dict_vars.data()), - em->dict_vars.size()), - em->dict_var_end_offsets.data(), - em->dict_var_end_offsets.size()); - - *log_event = event->msg.data(); - *log_event_size = event->msg.size(); - *log_event_ptr = event.release(); - return 0; -} - -void* encode_message(void* src_msg, - size_t src_size, - void** logtype, - void* logtype_size, - void** vars, - void* vars_size, - void** dict_vars, - void* dict_vars_size, - void** dict_var_end_offsets, - void* dict_var_end_offsets_size) { - // We cannot use unique_ptr here as we want the Go code to hold any - // references. Storing references in cpp (to avoid the unique_ptr falling - // out of scope) means we need to synchronize the updates to that storage - // as different go user threads could either encode a new message or free a - // stored encoded message. We also cannot return/move a unique_ptr back up - // to Go. - EncodedMessage* em = new EncodedMessage(); - std::string_view msg(reinterpret_cast(src_msg), src_size); - - std::vector dict_var_offsets; - if (false == ffi::encode_message(msg, em->logtype, em->vars, dict_var_offsets)) { - delete em; - return nullptr; - } - - // dict_var_offsets contains begin_pos followed by end_pos of each - // dictionary variable in msg - int32_t prev_end_off = 0; - for (size_t i = 0; i < dict_var_offsets.size(); i += 2) { - int32_t begin_pos = dict_var_offsets[i]; - int32_t end_pos = dict_var_offsets[i + 1]; - em->dict_vars.insert(em->dict_vars.begin() + prev_end_off, - msg.begin() + begin_pos, - msg.begin() + end_pos); - prev_end_off = prev_end_off + (end_pos - begin_pos); - em->dict_var_end_offsets.push_back(prev_end_off); - } - - *logtype = em->logtype.data(); - *static_cast(logtype_size) = em->logtype.size(); - *vars = em->vars.data(); - *static_cast(vars_size) = em->vars.size(); - *dict_vars = em->dict_vars.data(); - *static_cast(dict_vars_size) = em->dict_vars.size(); - *dict_var_end_offsets = em->dict_var_end_offsets.data(); - *static_cast(dict_var_end_offsets_size) = em->dict_var_end_offsets.size(); - return em; -} - -void delete_encoded_message(void* encoded_msg) { delete (EncodedMessage*)encoded_msg; } diff --git a/cpp/src/message/encoding.h b/cpp/src/message/encoding.h deleted file mode 100644 index b465172..0000000 --- a/cpp/src/message/encoding.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef MESSAGE_ENCODING_H -#define MESSAGE_ENCODING_H - -#ifdef __cplusplus -extern "C" { -#endif - -#include -#include - -int decode_message(void* encoded_msg, - void** log_event_ptr, - char** log_event, - size_t* log_event_size); - -void* encode_message(void* src_msg, - size_t src_size, - void** logtype, - void* logtype_size, - void** vars, - void* vars_size, - void** dict_vars, - void* dict_vars_size, - void** dict_var_end_offsets, - void* dict_var_end_offsets_size); - -void delete_encoded_message(void* encoded_msg); - -#ifdef __cplusplus -} -#endif - -#endif // MESSAGE_ENCODING_H diff --git a/ffi/BUILD.bazel b/ffi/BUILD.bazel index e0c5912..49b023b 100644 --- a/ffi/BUILD.bazel +++ b/ffi/BUILD.bazel @@ -3,12 +3,8 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "ffi", srcs = [ - "cgo_amd64.go", - "cgo_arm64.go", "ffi.go", ], - cgo = True, - cdeps = ["//:libclp_ffi"], importpath = "github.com/y-scope/clp-ffi-go/ffi", visibility = ["//visibility:public"], ) diff --git a/ffi/cgo_amd64.go b/ffi/cgo_amd64.go deleted file mode 100644 index a698102..0000000 --- a/ffi/cgo_amd64.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !external && amd64 - -package ffi - -/* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ -#cgo linux LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_linux_amd64 -Wl,-rpath=${SRCDIR}/../lib/ -#cgo darwin LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_darwin_amd64 -Wl,-rpath=${SRCDIR}/../lib/ -*/ -import "C" diff --git a/ffi/cgo_arm64.go b/ffi/cgo_arm64.go deleted file mode 100644 index e2e23f0..0000000 --- a/ffi/cgo_arm64.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build !external && arm64 - -package ffi - -/* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ -#cgo linux LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_linux_arm64 -Wl,-rpath=${SRCDIR}/../lib/ -#cgo darwin LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_darwin_arm64 -Wl,-rpath=${SRCDIR}/../lib/ -*/ -import "C" diff --git a/ffi/cgo_external.go b/ffi/cgo_external.go deleted file mode 100644 index cd314f6..0000000 --- a/ffi/cgo_external.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build external - -// When using `external` build manually set linkage with `CGO_LDFLAGS`. -package ffi - -/* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ -#cgo external LDFLAGS: -*/ -import "C" diff --git a/ffi/ffi.go b/ffi/ffi.go index 3f8105d..1d6c738 100644 --- a/ffi/ffi.go +++ b/ffi/ffi.go @@ -1,69 +1,33 @@ +// The ffi package contains the general types representing log events created +// and used by logging functions or libraries. In other words log events with no +// sort of CLP encoding or serializing. package ffi -/* -#include -*/ -import "C" - -import ( - "runtime" - "unsafe" -) - -// Mirrors cpp type epoch_time_ms_t defined in: -// src/ir/encoding.h -// src/ir/decoding.h +// Mirrors cpp type epoch_time_ms_t type EpochTimeMs int64 -type cppReference struct { - cptr unsafe.Pointer -} - -type LogMessage struct { - Msg []byte - cref *cppReference -} - -// Creates a new LogMessage backed by C-allocated memory and sets -// [finalizeLogMessage] as a finalizer. -func NewLogMessage (msg unsafe.Pointer, msgSize uint64, obj unsafe.Pointer) LogMessage { - ref := &cppReference{obj} - log := LogMessage{unsafe.Slice((*byte)(msg), msgSize), ref} - runtime.SetFinalizer(ref, finalizeLogMessage) - return log -} - -// DeleteLogMessage calls down to C where any additional clean up occurs before -// calling delete on the stored class pointer. After calling this function log -// is in an empty/nil state and the finalizer is unset. This function is only -// useful if the memory overhead of relying on the finalizer to call delete is -// a concern. -func DeleteLogMessage(log *LogMessage) { - if nil != log.cref { - log.Msg = nil - C.delete_log_event(log.cref.cptr) - runtime.SetFinalizer(log.cref, nil) - log.cref = nil - } -} - -// All LogMessages created with NewLogMessage will use this function as a -// finalizer to mimic GC. If memory overhead is a concern call -// [DeleteLogMessage] to immediately call delete (it will also clean up -// LogMessage and guards against double free). -// -// The rules for finalizers running are not perfectly equivalent to -// Go-allocated memory being GC'd, but in the case of LogMessages the -// C-allocated memory should eventually be deleted in similar fashion to a -// Go-allocated equivalent object. See -// https://pkg.go.dev/runtime#SetFinalizer. -func finalizeLogMessage(obj *cppReference) { - if nil != obj { - C.delete_log_event(obj.cptr) - } -} +// A ffi.LogMessage represents the text (message) component of a log event. +// A LogMessageView is a LogMessage that is backed by C++ allocated memory +// rather than the Go heap. A LogMessageView, x, is valid when returned and will +// remain valid until a new LogMessageView is returned by the same object (e.g. +// an ir.Deserializer) that retuend x. +type ( + LogMessageView = string + LogMessage = string +) +// LogEvent provides programmatic access to the various components of a log +// event. type LogEvent struct { LogMessage Timestamp EpochTimeMs } + +// The underlying memory of LogEventView is C-allocated and owned by the object +// (e.g. reader, desializer, etc) that returned it. Using an existing +// LogEventView after a new view has been returned by the same producing object +// is undefined (different producing objects own their own memory for views). +type LogEventView struct { + LogMessageView + Timestamp EpochTimeMs +} diff --git a/generate.go b/generate.go index 260f04c..50602d6 100644 --- a/generate.go +++ b/generate.go @@ -2,4 +2,4 @@ //go:generate cmake --build cpp/build -j //go:generate cmake --install cpp/build --prefix . -package ffi +package clp_ffi_go diff --git a/go.mod b/go.mod index 13e2c3a..3185152 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/y-scope/clp-ffi-go -go 1.19 +go 1.20 require github.com/klauspost/compress v1.16.5 diff --git a/include/ffi_go/defs.h b/include/ffi_go/defs.h new file mode 100644 index 0000000..563c89a --- /dev/null +++ b/include/ffi_go/defs.h @@ -0,0 +1,72 @@ +#ifndef FFI_GO_DEF_H +#define FFI_GO_DEF_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-using) + +#include +#include +#include + +// TODO: replace with clp c-compatible header once it exists +typedef int64_t epoch_time_ms_t; + +/** + * A span of a bool array passed down through Cgo. + */ +typedef struct { + bool* m_data; + size_t m_size; +} BoolSpan; + +/** + * A span of a byte array passed down through Cgo. + */ +typedef struct { + void* m_data; + size_t m_size; +} ByteSpan; + +/** + * A span of a Go int32 array passed down through Cgo. + */ +typedef struct { + int32_t* m_data; + size_t m_size; +} Int32tSpan; + +/** + * A span of a Go int64 array passed down through Cgo. + */ +typedef struct { + int64_t* m_data; + size_t m_size; +} Int64tSpan; + +/** + * A span of a Go int/C.size_t array passed down through Cgo. + */ +typedef struct { + size_t* m_data; + size_t m_size; +} SizetSpan; + +/** + * A view of a Go string passed down through Cgo. + */ +typedef struct { + char const* m_data; + size_t m_size; +} StringView; + +/** + * A view of a Go ffi.LogEvent passed down through Cgo. + */ +typedef struct { + StringView m_log_message; + epoch_time_ms_t m_timestamp; +} LogEventView; + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_DEF_H diff --git a/include/ffi_go/ir/decoder.h b/include/ffi_go/ir/decoder.h new file mode 100644 index 0000000..f1efef7 --- /dev/null +++ b/include/ffi_go/ir/decoder.h @@ -0,0 +1,87 @@ +#ifndef FFI_GO_IR_DECODER_H +#define FFI_GO_IR_DECODER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) +// NOLINTBEGIN(modernize-use-using) + +#include +#include + +#include + +/** + * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. + * @return New ir::Decoder's address + */ +void* ir_decoder_new(); + +/** + * Clean up the underlying ir::Decoder of a Go ir.Decoder. + * @param[in] ir_encoder Address of a ir::Decoder created and returned by + * ir_decoder_new + */ +void ir_decoder_close(void* decoder); + +/** + * Given the fields of a CLP IR encoded log message with eight byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); + +/** + * Given the fields of a CLP IR encoded log message with four byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_DECODER_H diff --git a/include/ffi_go/ir/deserializer.h b/include/ffi_go/ir/deserializer.h new file mode 100644 index 0000000..572c056 --- /dev/null +++ b/include/ffi_go/ir/deserializer.h @@ -0,0 +1,157 @@ +#ifndef FFI_GO_IR_DESERIALIZER_H +#define FFI_GO_IR_DESERIALIZER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) + +#include +#include + +#include +#include + +/** + * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. + * @param[in] ir_deserializer The address of a ir::Deserializer created and + * returned by ir_deserializer_deserialize_preamble + */ +void ir_deserializer_close(void* ir_deserializer); + +/** + * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and + * extract its information. An ir::Deserializer will be allocated to use as the + * backing storage for a Go ir.Deserializer (i.e. subsequent calls to + * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read + * the metadata based on the returned type. All pointer parameters must be + * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[out] ir_pos Position in ir_view read to + * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) + * @param[out] metadata_type Type of metadata in preamble (e.g. json) + * @param[out] metadata_pos Position in ir_view where the metadata begins + * @param[out] metadata_size Size of the metadata (in bytes) + * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer + * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer + * to be filled in by Go using the metadata contents + * @return ffi::ir_stream::IRErrorCode forwarded from either + * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble + */ +int ir_deserializer_deserialize_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr +); + +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::decode_next_message + */ +int ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); + +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + */ +int ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); + +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event until finding an event that is both within the time interval and + * matches any query. If queries is empty, the first log event within the time + * interval is treated as a match. Returns the components of the found log event + * and the buffer position it ends at. All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the first matching query or + * 0 if queries is empty + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +int ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); + +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log event + * until finding an event that is both within the time interval and matches any + * query. If queries is empty, the first log event within the time interval is + * treated as a match. Returns the components of the found log event and the + * buffer position it ends at. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the matching query + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +int ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); + +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_DESERIALIZER_H diff --git a/include/ffi_go/ir/encoder.h b/include/ffi_go/ir/encoder.h new file mode 100644 index 0000000..aafa2a4 --- /dev/null +++ b/include/ffi_go/ir/encoder.h @@ -0,0 +1,97 @@ +#ifndef FFI_GO_IR_ENCODER_H +#define FFI_GO_IR_ENCODER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) +// NOLINTBEGIN(modernize-use-using) + +#include +#include + +#include + +/** + * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. + * @return New ir::Encoder's address + */ +void* ir_encoder_eight_byte_new(); + +/** + * @copydoc ir_encoder_eight_byte_new() + */ +void* ir_encoder_four_byte_new(); + +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_eight_byte_new + */ +void ir_encoder_eight_byte_close(void* ir_encoder); + +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_four_byte_new + */ +void ir_encoder_four_byte_close(void* ir_encoder); + +/** + * Given a log message, encode it into a CLP IR object with eight byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); + +/** + * Given a log message, encode it into a CLP IR object with four byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +int ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_ENCODER_H diff --git a/include/ffi_go/ir/serializer.h b/include/ffi_go/ir/serializer.h new file mode 100644 index 0000000..0e200a8 --- /dev/null +++ b/include/ffi_go/ir/serializer.h @@ -0,0 +1,109 @@ +#ifndef FFI_GO_IR_SERIALIZER_H +#define FFI_GO_IR_SERIALIZER_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) + +#include +#include + +#include + +/** + * Clean up the underlying ir::Serializer of a Go ir.Serializer. + * @param[in] ir_serializer Address of a ir::Serializer created and returned by + * ir_serializer_serialize_*_preamble + */ +void ir_serializer_close(void* ir_serializer); + +/** + * Given the fields of a CLP IR premable, serialize them into an IR byte stream + * with eight byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::eight_byte_encoding::encode_preamble + */ +int ir_serializer_serialize_eight_byte_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view +); + +/** + * Given the fields of a CLP IR premable, serialize them into an IR byte stream + * with four byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::four_byte_encoding::encode_preamble + */ +int ir_serializer_serialize_four_byte_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +); + +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * eight byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message of the log event to serialize + * @param[in] timestamp Timestamp of the log event to serialize + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::eight_byte_encoding::encode_message + */ +int ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view +); + +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * four byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to serialize + * @param[in] timestamp_delta Timestamp delta to the previous log event in the + * IR stream + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwared from + * ffi::ir_stream::four_byte_encoding::encode_message + */ +int ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view +); + +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_SERIALIZER_H diff --git a/include/ffi_go/search/wildcard_query.h b/include/ffi_go/search/wildcard_query.h new file mode 100644 index 0000000..f995e29 --- /dev/null +++ b/include/ffi_go/search/wildcard_query.h @@ -0,0 +1,70 @@ +#ifndef FFI_GO_IR_WILDCARD_QUERY_H +#define FFI_GO_IR_WILDCARD_QUERY_H +// header must support C, making modernize checks inapplicable +// NOLINTBEGIN(modernize-deprecated-headers) +// NOLINTBEGIN(modernize-use-trailing-return-type) +// NOLINTBEGIN(modernize-use-using) + +#include + +#include + +/** + * A timestamp interval of [m_lower, m_upper). + */ +typedef struct { + epoch_time_ms_t m_lower; + epoch_time_ms_t m_upper; +} TimestampInterval; + +/** + * A view of a wildcard query passed down from Go. The query string is assumed + * to have been cleaned using the CLP function `clean_up_wildcard_search_string` + * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case + * insensitive). + */ +typedef struct { + StringView m_query; + bool m_case_sensitive; +} WildcardQueryView; + +/** + * A view of a Go search.MergedWildcardQuery passed down through Cgo. The + * string is a concatenation of all wildcard queries, while m_end_offsets stores + * the size of each query. + */ +typedef struct { + StringView m_queries; + SizetSpan m_end_offsets; + BoolSpan m_case_sensitivity; +} MergedWildcardQueryView; + +/** + * Delete a std::string holding a wildcard query. + * @param[in] str Address of a std::string created and returned by + * clean_wildcard_query + */ +void wildcard_query_delete(void* str); + +/** + * Given a query string, clean it to be safe for matching. See + * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ +StringView wildcard_query_clean(StringView query, void** ptr); + +/** + * Given a target string perform CLP wildcard matching using query. See + * `wildcard_match_unsafe` in CLP src/string_utils.hpp. + * @param[in] target String to perform matching on + * @param[in] query Query to use for matching + * @return 1 if query matches target, 0 otherwise + */ +int wildcard_query_match(StringView target, WildcardQueryView query); + +// NOLINTEND(modernize-use-using) +// NOLINTEND(modernize-use-trailing-return-type) +// NOLINTEND(modernize-deprecated-headers) +#endif // FFI_GO_IR_WILDCARD_QUERY_H diff --git a/ir/BUILD.bazel b/ir/BUILD.bazel index 41755be..b4a0d30 100644 --- a/ir/BUILD.bazel +++ b/ir/BUILD.bazel @@ -3,20 +3,26 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "ir", srcs = [ - "cgo_amd64.go", - "cgo_arm64.go", - "decoder.go", - "encoder.go", - "ir.go", - "irerror.go", - "irerror_string.go", - "reader.go", + "cgo_defs.go", + "decoder.go", + "deserializer.go", + "encoder.go", + "ir.go", + "irerror.go", + "irerror_string.go", + "reader.go", + "serializer.go", ], cgo = True, - cdeps = ["//:libclp_ffi"], + cdeps = [ + "//:libclp_ffi", + ], importpath = "github.com/y-scope/clp-ffi-go/ir", visibility = ["//visibility:public"], - deps = ["//ffi"], + deps = [ + "//ffi", + "//search", + ], ) alias( @@ -28,12 +34,12 @@ alias( go_test( name = "ir_test", srcs = [ - "encoder_test.go", + "ir_test.go", "reader_test.go", + "serder_test.go", ], embed = [":ir"], deps = [ - "//test", "@com_github_klauspost_compress//zstd", ], ) diff --git a/ir/cgo_amd64.go b/ir/cgo_amd64.go index d90f319..269cf2d 100644 --- a/ir/cgo_amd64.go +++ b/ir/cgo_amd64.go @@ -3,8 +3,8 @@ package ir /* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ -#cgo linux LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_linux_amd64 -Wl,-rpath=${SRCDIR}/../lib/ -#cgo darwin LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_darwin_amd64 -Wl,-rpath=${SRCDIR}/../lib/ +#cgo CPPFLAGS: -I${SRCDIR}/../include/ +#cgo linux LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_linux_amd64.a -lstdc++ +#cgo darwin LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_darwin_amd64.a -lstdc++ */ import "C" diff --git a/ir/cgo_arm64.go b/ir/cgo_arm64.go index d73aaab..da2a837 100644 --- a/ir/cgo_arm64.go +++ b/ir/cgo_arm64.go @@ -3,8 +3,8 @@ package ir /* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ -#cgo linux LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_linux_arm64 -Wl,-rpath=${SRCDIR}/../lib/ -#cgo darwin LDFLAGS: -L${SRCDIR}/../lib/ -lclp_ffi_darwin_arm64 -Wl,-rpath=${SRCDIR}/../lib/ +#cgo CPPFLAGS: -I${SRCDIR}/../include/ +#cgo linux LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_linux_arm64.a -lstdc++ +#cgo darwin LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_darwin_arm64.a -lstdc++ */ import "C" diff --git a/ir/cgo_defs.go b/ir/cgo_defs.go new file mode 100644 index 0000000..8ed33fc --- /dev/null +++ b/ir/cgo_defs.go @@ -0,0 +1,98 @@ +package ir + +/* +#include +#include +*/ +import "C" + +import ( + "unsafe" + + "github.com/y-scope/clp-ffi-go/search" +) + +// The follow functions are helpers to cleanup Cgo related code. The underlying +// Go type created from a 'C' type is not exported and recreated in each +// package. Therefore, these helpers must be redefined in any package wishing to +// use them, so that they reference the correct underlying Go type of the +// package (see: https://pkg.go.dev/cmd/cgo). This problem could be alivated by +// using Go generate to create/add these helpers to a package necessary. + +func newCByteSpan(s []byte) C.ByteSpan { + return C.ByteSpan{ + unsafe.Pointer(unsafe.SliceData(s)), + C.size_t(len(s)), + } +} + +func newCInt32tSpan(s []int32) C.Int32tSpan { + return C.Int32tSpan{ + (*C.int32_t)(unsafe.Pointer(unsafe.SliceData(s))), + C.size_t(len(s)), + } +} + +func newCInt64tSpan(s []int64) C.Int64tSpan { + return C.Int64tSpan{ + (*C.int64_t)(unsafe.Pointer(unsafe.SliceData(s))), + C.size_t(len(s)), + } +} + +func newCStringView(s string) C.StringView { + return C.StringView{ + (*C.char)(unsafe.Pointer(unsafe.StringData(s))), + C.size_t(len(s)), + } +} + +func newMergedWildcardQueryView(mergedQuery search.MergedWildcardQuery) C.MergedWildcardQueryView { + return C.MergedWildcardQueryView{ + newCStringView(mergedQuery.Queries()), + C.SizetSpan{ + (*C.size_t)(unsafe.Pointer(unsafe.SliceData(mergedQuery.EndOffsets()))), + C.size_t(len(mergedQuery.EndOffsets())), + }, + C.BoolSpan{ + (*C.bool)(unsafe.Pointer(unsafe.SliceData(mergedQuery.CaseSensitivity()))), + C.size_t(len(mergedQuery.CaseSensitivity())), + }, + } +} + +func newLogMessageView[Tgo EightByteEncoding | FourByteEncoding, Tc C.Int64tSpan | C.Int32tSpan]( + logtype C.StringView, + vars Tc, + dictVars C.StringView, + dictVarEndOffsets C.Int32tSpan, +) *LogMessageView[Tgo] { + var msgView LogMessageView[Tgo] + msgView.Logtype = unsafe.String((*byte)(unsafe.Pointer(logtype.m_data)), logtype.m_size) + switch any(msgView.Vars).(type) { + case []EightByteEncoding: + dst := any(&msgView.Vars).(*[]EightByteEncoding) + src := any(vars).(C.Int64tSpan) + if 0 < src.m_size && nil != src.m_data { + *dst = unsafe.Slice((*EightByteEncoding)(src.m_data), src.m_size) + } + case []FourByteEncoding: + dst := any(&msgView.Vars).(*[]FourByteEncoding) + src := any(vars).(C.Int32tSpan) + if 0 < src.m_size && nil != src.m_data { + *dst = unsafe.Slice((*FourByteEncoding)(src.m_data), src.m_size) + } + default: + return nil + } + if 0 < dictVars.m_size && nil != dictVars.m_data { + msgView.Logtype = unsafe.String((*byte)(unsafe.Pointer(dictVars.m_data)), dictVars.m_size) + } + if 0 < dictVarEndOffsets.m_size && nil != dictVarEndOffsets.m_data { + msgView.DictVarEndOffsets = unsafe.Slice( + (*int32)(dictVarEndOffsets.m_data), + dictVarEndOffsets.m_size, + ) + } + return &msgView +} diff --git a/ir/cgo_external.go b/ir/cgo_external.go index 2556d0d..96db50a 100644 --- a/ir/cgo_external.go +++ b/ir/cgo_external.go @@ -4,7 +4,7 @@ package ir /* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ +#cgo CPPFLAGS: -I${SRCDIR}/../include/ #cgo external LDFLAGS: */ import "C" diff --git a/ir/decoder.go b/ir/decoder.go index 3893af7..165772d 100644 --- a/ir/decoder.go +++ b/ir/decoder.go @@ -1,179 +1,94 @@ package ir /* -#include -#include +#include */ import "C" import ( - "encoding/json" - "strconv" "unsafe" "github.com/y-scope/clp-ffi-go/ffi" ) -// TODO once we reach go >= 1.20 -// change &buf[:1][0] to unsafe.SliceData(buf) -// https://pkg.go.dev/unsafe#SliceData - -// IrDecoder exports functions to decode log events in an IR stream and also -// inspect the timnestamp information of the stream. An IrDecoder manages the -// internal state of the IR stream, such that the next log event in the stream -// can be decoded. The maintence of the buffer containing the IR stream is left -// to the caller. -type IrDecoder interface { - DecodeNextLogEvent(buf []byte) (ffi.LogEvent, int, error) - TimestampInfo() TimestampInfo +// A Decoder takes objects encoded in CLP IR as input and returns them in their +// natural state prior to encoding. Close must be called to free the underlying +// memory and failure to do so will result in a memory leak. +type Decoder[T EightByteEncoding | FourByteEncoding] interface { + DecodeLogMessage(irMessage LogMessage[T]) (*ffi.LogMessageView, error) + Close() error } -// DecodePreamble attempts to read an IR stream preamble from buf, returning an -// IrDecoder (of the correct stream encoding size), the offset read to in buf -// (the end of the preamble), and an error. Note the metadata stored in the -// preamble is sparse and certain fields in TimestampInfo may be 0 value. -// Return values: -// - nil == error: successful decode -// - nil != error: IrDecode will be nil, offset may be non-zero for debugging purposes -// - type [IRError]: CLP failed to successfully decode -// - type from [encoding/json]: unmarshalling the metadata failed -func DecodePreamble(buf []byte) (IrDecoder, int, error) { - var offset C.size_t - var ir_encoding C.int8_t - var metadata_type C.int8_t - var metadata_pos C.size_t - var metadata_size C.uint16_t - - if err := IRError(C.decode_preamble( - unsafe.Pointer(&buf[:1][0]), - C.size_t(len(buf)), - &offset, - &ir_encoding, - &metadata_type, - &metadata_pos, - &metadata_size)); Success != err { - return nil, int(offset), err - } - - if 1 != metadata_type { - return nil, int(offset), UnsupportedVersion - } - - var metadata map[string]interface{} - if err := json.Unmarshal(buf[metadata_pos:metadata_pos+C.size_t(metadata_size)], &metadata); nil != err { - return nil, int(offset), err - } - - var tsInfo TimestampInfo - if tsPat, ok := metadata["TIMESTAMP_PATTERN"].(string); ok { - tsInfo.Pattern = tsPat - } - if tsSyn, ok := metadata["TIMESTAMP_PATTERN_SYNTAX"].(string); ok { - tsInfo.PatternSyntax = tsSyn - } - if tzid, ok := metadata["TZ_ID"].(string); ok { - tsInfo.TimeZoneId = tzid - } - - var decoder IrDecoder - if 1 == ir_encoding { - var refTs ffi.EpochTimeMs = 0 - if tsStr, ok := metadata["REFERENCE_TIMESTAMP"].(string); ok { - if tsInt, err := strconv.ParseInt(tsStr, 10, 64); nil == err { - refTs = ffi.EpochTimeMs(tsInt) - } - } - decoder = &FourByteIrStream{ - irStream: irStream[FourByteEncodedVariable]{tsInfo: tsInfo, cPtr: nil}, - prevTimestamp: refTs, - } - } else { - decoder = &EightByteIrStream{irStream[EightByteEncodedVariable]{tsInfo, nil}} - } - - return decoder, int(offset), nil +// Return a new Decoder for IR using [EightByteEncoding]. +func EightByteDecoder() (Decoder[EightByteEncoding], error) { + return &eightByteDecoder{commonDecoder{C.ir_decoder_new()}}, nil } -// DecodeNextLogEvent attempts to read the next LogEvent from the IR stream in -// buf, returning the LogEvent, the offset read to in buf (the end of the -// LogEvent in buf), and an error. -// Return values: -// - nil == error: successful decode -// - nil != error: ffi.LogEvent will be nil, offset may be non-zero for debugging purposes -// - [Eof]: CLP found the IR stream EOF tag -// - [IRError]: CLP failed to successfully decode -func (self *EightByteIrStream) DecodeNextLogEvent(buf []byte) (ffi.LogEvent, int, error) { - return decodeNextLogEvent(self, buf) +// Return a new Decoder for IR using [FourByteEncoding]. +func FourByteDecoder() (Decoder[FourByteEncoding], error) { + return &fourByteDecoder{commonDecoder{C.ir_decoder_new()}}, nil } -// DecodeNextLogEvent attempts to read the next LogEvent from the IR stream in -// buf, returning the LogEvent, the offset read to in buf (the end of the -// LogEvent in buf), and an error. -// Return values: -// - nil == error: successful decode -// - nil != error: ffi.LogEvent will be nil, offset may be non-zero for debugging purposes -// - [Eof]: CLP found the IR stream EOF tag -// - [IRError]: CLP failed to successfully decode -func (self *FourByteIrStream) DecodeNextLogEvent(buf []byte) (ffi.LogEvent, int, error) { - return decodeNextLogEvent(self, buf) +type commonDecoder struct { + cptr unsafe.Pointer } -// decodeNextLogEvent performs the actual work for DecodeNextLogEvent in a -// generic way. -func decodeNextLogEvent[T EightByteIrStream | FourByteIrStream]( - irstream *T, - buf []byte, -) (ffi.LogEvent, int, error) { - if 0 >= len(buf) { - return ffi.LogEvent{}, 0, IncompleteIR +// Close will delete the underlying C++ allocated memory used by the +// deserializer. Failure to call Close will result in a memory leak. +func (self *commonDecoder) Close() error { + if nil != self.cptr { + C.ir_decoder_close(self.cptr) + self.cptr = nil } - var offset C.size_t - var msgObj unsafe.Pointer - var msg *C.char - var msgSize C.size_t - var timestampOrDelta C.int64_t + return nil +} - var err error - switch any(irstream).(type) { - case *EightByteIrStream: - err = IRError(C.eight_byte_decode_next_log_event( - unsafe.Pointer(&buf[:1][0]), - C.size_t(len(buf)), - &offset, - &msgObj, - &msg, - &msgSize, - ×tampOrDelta)) - case *FourByteIrStream: - err = IRError(C.four_byte_decode_next_log_event( - unsafe.Pointer(&buf[:1][0]), - C.size_t(len(buf)), - &offset, - &msgObj, - &msg, - &msgSize, - ×tampOrDelta)) - default: - return ffi.LogEvent{}, 0, UnsupportedVersion - } +type eightByteDecoder struct { + commonDecoder +} + +// Decode an IR encoded log message, returning a view of the original +// (non-encoded) log message. +func (self *eightByteDecoder) DecodeLogMessage( + irMessage LogMessage[EightByteEncoding], +) (*ffi.LogMessageView, error) { + var msg C.StringView + err := IrError(C.ir_decoder_decode_eight_byte_log_message( + newCStringView(irMessage.Logtype), + newCInt64tSpan(irMessage.Vars), + newCStringView(irMessage.DictVars), + newCInt32tSpan(irMessage.DictVarEndOffsets), + self.cptr, + &msg, + )) if Success != err { - return ffi.LogEvent{}, int(offset), err + return nil, DecodeError } + view := unsafe.String((*byte)(unsafe.Pointer(msg.m_data)), msg.m_size) + return &view, nil +} - var ts ffi.EpochTimeMs - switch irs := any(irstream).(type) { - case *EightByteIrStream: - ts = ffi.EpochTimeMs(timestampOrDelta) - case *FourByteIrStream: - ts = irs.prevTimestamp + ffi.EpochTimeMs(timestampOrDelta) - irs.prevTimestamp = ts - default: - return ffi.LogEvent{}, 0, UnsupportedVersion - } +type fourByteDecoder struct { + commonDecoder +} - event := ffi.LogEvent{ - LogMessage: ffi.NewLogMessage(unsafe.Pointer(msg), uint64(msgSize), msgObj), - Timestamp: ts, +// Decode an IR encoded log message, returning a view of the original +// (non-encoded) log message. +func (self *fourByteDecoder) DecodeLogMessage( + irMessage LogMessage[FourByteEncoding], +) (*ffi.LogMessageView, error) { + var msg C.StringView + err := IrError(C.ir_decoder_decode_four_byte_log_message( + newCStringView(irMessage.Logtype), + newCInt32tSpan(irMessage.Vars), + newCStringView(irMessage.DictVars), + newCInt32tSpan(irMessage.DictVarEndOffsets), + self.cptr, + &msg, + )) + if Success != err { + return nil, DecodeError } - return event, int(offset), nil + view := unsafe.String((*byte)(unsafe.Pointer(msg.m_data)), msg.m_size) + return &view, nil } diff --git a/ir/deserializer.go b/ir/deserializer.go new file mode 100644 index 0000000..27df183 --- /dev/null +++ b/ir/deserializer.go @@ -0,0 +1,284 @@ +package ir + +/* +#include +#include +#include +*/ +import "C" + +import ( + "encoding/json" + "strconv" + "unsafe" + + "github.com/y-scope/clp-ffi-go/ffi" + "github.com/y-scope/clp-ffi-go/search" +) + +// A Deserializer exports functions to deserialize log events from a CLP IR byte +// stream. Deserializatoin functions take an IR buffer as input, but how that +// buffer is materialized is left to the user. These functions return views +// (slices) of the log events extracted from the IR. Each Deserializer owns its +// own unique underlying memory for the views it produces/returns. This memory +// is reused for each view, so to persist the contents the memory must be copied +// into another object. Close must be called to free the underlying memory and +// failure to do so will result in a memory leak. +type Deserializer interface { + DeserializeLogEvent(irBuf []byte) (*ffi.LogEventView, int, error) + DeserializeWildcardMatch( + irBuf []byte, + timeInterval search.TimestampInterval, + mergedQuery search.MergedWildcardQuery, + ) (*ffi.LogEventView, int, int, error) + TimestampInfo() TimestampInfo + Close() error +} + +// DeserializePreamble attempts to read an IR stream preamble from irBuf, +// returning an Deserializer (of the correct stream encoding size), the position +// read to in irBuf (the end of the preamble), and an error. Note the metadata +// stored in the preamble is sparse and certain fields in TimestampInfo may be 0 +// value. On error returns: +// - nil Deserializer +// - 0 position +// - [IrError] error: CLP failed to successfully deserialize +// - [encoding/json] error: unmarshalling the metadata failed +func DeserializePreamble(irBuf []byte) (Deserializer, int, error) { + if 0 >= len(irBuf) { + return nil, 0, IncompleteIr + } + + var pos C.size_t + var irEncoding C.int8_t + var metadataType C.int8_t + var metadataPos C.size_t + var metadataSize C.uint16_t + var deserializerCptr unsafe.Pointer + var timestampCptr unsafe.Pointer + if err := IrError(C.ir_deserializer_deserialize_preamble( + newCByteSpan(irBuf), + &pos, + &irEncoding, + &metadataType, + &metadataPos, + &metadataSize, + &deserializerCptr, + ×tampCptr, + )); Success != err { + return nil, int(pos), err + } + + if 1 != metadataType { + return nil, 0, UnsupportedVersion + } + + var metadata map[string]interface{} + if err := json.Unmarshal( + irBuf[metadataPos:metadataPos+C.size_t(metadataSize)], + &metadata, + ); nil != err { + return nil, 0, err + } + + var tsInfo TimestampInfo + if tsPat, ok := metadata["TIMESTAMP_PATTERN"].(string); ok { + tsInfo.Pattern = tsPat + } + if tsSyn, ok := metadata["TIMESTAMP_PATTERN_SYNTAX"].(string); ok { + tsInfo.PatternSyntax = tsSyn + } + if tzid, ok := metadata["TZ_ID"].(string); ok { + tsInfo.TimeZoneId = tzid + } + + var deserializer Deserializer + if 1 == irEncoding { + var refTs ffi.EpochTimeMs = 0 + if tsStr, ok := metadata["REFERENCE_TIMESTAMP"].(string); ok { + if tsInt, err := strconv.ParseInt(tsStr, 10, 64); nil == err { + refTs = ffi.EpochTimeMs(tsInt) + *(*ffi.EpochTimeMs)(timestampCptr) = refTs + } + } + deserializer = &fourByteDeserializer{commonDeserializer{tsInfo, deserializerCptr}, refTs} + } else { + deserializer = &eightByteDeserializer{commonDeserializer{tsInfo, deserializerCptr}} + } + + return deserializer, int(pos), nil +} + +// commonDeserializer contains fields common to all types of CLP IR encoding. +// TimestampInfo stores information common to all timestamps found in the IR. +// cptr holds a reference to the underlying C++ objected used as backing storage +// for the Views returned by the deserializer. Close must be called to free this +// underlying memory and failure to do so will result in a memory leak. +type commonDeserializer struct { + tsInfo TimestampInfo + cptr unsafe.Pointer +} + +// Close will delete the underlying C++ allocated memory used by the +// deserializer. Failure to call Close will result in a memory leak. +func (self *commonDeserializer) Close() error { + if nil != self.cptr { + C.ir_deserializer_close(self.cptr) + self.cptr = nil + } + return nil +} + +// Returns the TimestampInfo used by the Deserializer. +func (self commonDeserializer) TimestampInfo() TimestampInfo { + return self.tsInfo +} + +type eightByteDeserializer struct { + commonDeserializer +} + +// DeserializeLogEvent attempts to read the next log event from the IR stream in +// irBuf, returning the deserialized [ffi.LogEventView], the position read to in +// irBuf (the end of the log event in irBuf), and an error. On error returns: +// - nil *ffi.LogEventView +// - 0 position +// - [IrError] error: CLP failed to successfully deserialize +// - [EndOfIr] error: CLP found the IR stream EOF tag +func (self *eightByteDeserializer) DeserializeLogEvent( + irBuf []byte, +) (*ffi.LogEventView, int, error) { + return deserializeLogEvent(self, irBuf) +} + +// fourByteDeserializer contains both a common CLP IR deserializer and stores +// the previously seen log event's timestamp. The previous timestamp is +// necessary to calculate the current timestamp as four byte encoding only +// encodes the timestamp delta between the current log event and the previous. +type fourByteDeserializer struct { + commonDeserializer + prevTimestamp ffi.EpochTimeMs +} + +// DeserializeLogEvent attempts to read the next log event from the IR stream in +// irBuf, returning the deserialized [ffi.LogEventView], the position read to in +// irBuf (the end of the log event in irBuf), and an error. On error returns: +// - nil *ffi.LogEventView +// - 0 position +// - [IrError] error: CLP failed to successfully deserialize +// - [EndOfIr] error: CLP found the IR stream EOF tag +func (self *fourByteDeserializer) DeserializeLogEvent( + irBuf []byte, +) (*ffi.LogEventView, int, error) { + return deserializeLogEvent(self, irBuf) +} + +func (self *eightByteDeserializer) DeserializeWildcardMatch( + irBuf []byte, + timeInterval search.TimestampInterval, + mergedQuery search.MergedWildcardQuery, +) (*ffi.LogEventView, int, int, error) { + return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) +} + +func (self *fourByteDeserializer) DeserializeWildcardMatch( + irBuf []byte, + timeInterval search.TimestampInterval, + mergedQuery search.MergedWildcardQuery, +) (*ffi.LogEventView, int, int, error) { + return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) +} + +func deserializeLogEvent( + deserializer Deserializer, + irBuf []byte, +) (*ffi.LogEventView, int, error) { + if 0 >= len(irBuf) { + return nil, 0, IncompleteIr + } + + var pos C.size_t + var event C.LogEventView + var err error + switch irs := deserializer.(type) { + case *eightByteDeserializer: + err = IrError(C.ir_deserializer_deserialize_eight_byte_log_event( + newCByteSpan(irBuf), + irs.cptr, + &pos, + &event, + )) + case *fourByteDeserializer: + err = IrError(C.ir_deserializer_deserialize_four_byte_log_event( + newCByteSpan(irBuf), + irs.cptr, + &pos, + &event, + )) + } + if Success != err { + return nil, 0, err + } + + return &ffi.LogEventView{ + LogMessageView: unsafe.String( + (*byte)((unsafe.Pointer)(event.m_log_message.m_data)), + event.m_log_message.m_size, + ), + Timestamp: ffi.EpochTimeMs(event.m_timestamp), + }, + int(pos), + nil +} + +func deserializeWildcardMatch( + deserializer Deserializer, + irBuf []byte, + time search.TimestampInterval, + mergedQuery search.MergedWildcardQuery, +) (*ffi.LogEventView, int, int, error) { + if 0 >= len(irBuf) { + return nil, 0, -1, IncompleteIr + } + + var pos C.size_t + var event C.LogEventView + var match C.size_t + var err error + switch irs := deserializer.(type) { + case *eightByteDeserializer: + err = IrError(C.ir_deserializer_deserialize_eight_byte_wildcard_match( + newCByteSpan(irBuf), + irs.cptr, + C.TimestampInterval{C.int64_t(time.Lower), C.int64_t(time.Upper)}, + newMergedWildcardQueryView(mergedQuery), + &pos, + &event, + &match, + )) + case *fourByteDeserializer: + err = IrError(C.ir_deserializer_deserialize_four_byte_wildcard_match( + newCByteSpan(irBuf), + irs.cptr, + C.TimestampInterval{C.int64_t(time.Lower), C.int64_t(time.Upper)}, + newMergedWildcardQueryView(mergedQuery), + &pos, + &event, + &match, + )) + } + if Success != err { + return nil, 0, -1, err + } + + return &ffi.LogEventView{ + LogMessageView: unsafe.String( + (*byte)((unsafe.Pointer)(event.m_log_message.m_data)), + event.m_log_message.m_size, + ), + Timestamp: ffi.EpochTimeMs(event.m_timestamp), + }, + int(pos), + int(match), + nil +} diff --git a/ir/encoder.go b/ir/encoder.go index d16ef18..a6570bb 100644 --- a/ir/encoder.go +++ b/ir/encoder.go @@ -1,169 +1,102 @@ package ir /* -#include +#include */ import "C" import ( - "runtime" "unsafe" "github.com/y-scope/clp-ffi-go/ffi" ) -type IrEncoder interface { - EncodeMessage(ts ffi.EpochTimeMs, msg string) ([]byte, int) - EncodeMessageUnsafe(ts ffi.EpochTimeMs, msg string) ([]byte, int) - TimestampInfo() TimestampInfo +// An Encoder takes logging objects (commonly used/created by logging libraries) +// and encodes them as CLP IR. Close must be called to free the underlying +// memory and failure to do so will result in a memory leak. +type Encoder[T EightByteEncoding | FourByteEncoding] interface { + EncodeLogMessage(logMessage ffi.LogMessage) (*LogMessageView[T], error) + Close() error } -func EightByteEncodePreamble( - ts_pattern string, - ts_pattern_syntax string, - time_zone_id string, -) (EightByteIrStream, []byte, int) { - irs, preamble, ret := EightByteEncodePreambleUnsafe(ts_pattern, ts_pattern_syntax, - time_zone_id) - if 0 != ret { - return irs, nil, ret - } - safePreamble := make([]byte, len(preamble)) - copy(safePreamble, preamble) - return irs, safePreamble, 0 +// Return a new Encoder that produces IR using [EightByteEncoding]. +func EightByteEncoder() (Encoder[EightByteEncoding], error) { + return &eightByteEncoder{C.ir_encoder_eight_byte_new()}, nil } -func EightByteEncodePreambleUnsafe( - ts_pattern string, - ts_pattern_syntax string, - time_zone_id string, -) (EightByteIrStream, []byte, int) { - var bufPtr unsafe.Pointer - var bufSize uint64 - irs := EightByteIrStream{ - irStream[EightByteEncodedVariable]{ - TimestampInfo{ts_pattern, ts_pattern_syntax, time_zone_id}, nil, - }, - } - irs.cPtr = C.eight_byte_encode_preamble( - unsafe.Pointer(&[]byte(ts_pattern)[0]), C.size_t(len(ts_pattern)), - unsafe.Pointer(&[]byte(ts_pattern_syntax)[0]), C.size_t(len(ts_pattern_syntax)), - unsafe.Pointer(&[]byte(time_zone_id)[0]), C.size_t(len(time_zone_id)), - &bufPtr, unsafe.Pointer(&bufSize)) - buf := unsafe.Slice((*byte)(bufPtr), bufSize) - if nil == buf { - return irs, nil, -2 - } - runtime.SetFinalizer(&irs, - func(irs *EightByteIrStream) { C.delete_ir_stream_state(irs.cPtr) }) - return irs, buf, 0 +// Return a new Encoder that produces IR using [FourByteEncoding]. +func FourByteEncoder() (Encoder[FourByteEncoding], error) { + return &fourByteEncoder{C.ir_encoder_four_byte_new()}, nil } -func FourByteEncodePreamble( - ts_pattern string, - ts_pattern_syntax string, - time_zone_id string, - reference_ts ffi.EpochTimeMs, -) (FourByteIrStream, []byte, int) { - irs, preamble, ret := FourByteEncodePreambleUnsafe(ts_pattern, ts_pattern_syntax, - time_zone_id, reference_ts) - if 0 != ret { - return irs, nil, ret - } - safePreamble := make([]byte, len(preamble)) - copy(safePreamble, preamble) - return irs, safePreamble, 0 +type eightByteEncoder struct { + cptr unsafe.Pointer } -func FourByteEncodePreambleUnsafe( - ts_pattern string, - ts_pattern_syntax string, - time_zone_id string, - reference_ts ffi.EpochTimeMs, -) (FourByteIrStream, []byte, int) { - var bufPtr unsafe.Pointer - var bufSize uint64 - irs := FourByteIrStream{ - irStream[FourByteEncodedVariable]{ - TimestampInfo{ts_pattern, ts_pattern_syntax, time_zone_id}, nil, - }, - reference_ts, - } - irs.cPtr = C.four_byte_encode_preamble( - unsafe.Pointer(&[]byte(ts_pattern)[0]), C.size_t(len(ts_pattern)), - unsafe.Pointer(&[]byte(ts_pattern_syntax)[0]), C.size_t(len(ts_pattern_syntax)), - unsafe.Pointer(&[]byte(time_zone_id)[0]), C.size_t(len(time_zone_id)), - C.int64_t(reference_ts), &bufPtr, unsafe.Pointer(&bufSize)) - buf := unsafe.Slice((*byte)(bufPtr), bufSize) - if nil == buf { - return irs, nil, -2 +// Close will delete the underlying C++ allocated memory used by the +// deserializer. Failure to call Close will result in a memory leak. +func (self *eightByteEncoder) Close() error { + if nil != self.cptr { + C.ir_encoder_eight_byte_close(self.cptr) + self.cptr = nil } - runtime.SetFinalizer(&irs, - func(irs *FourByteIrStream) { C.delete_ir_stream_state(irs.cPtr) }) - return irs, buf, 0 -} - -func (self *EightByteIrStream) EncodeMessage(ts ffi.EpochTimeMs, msg string) ([]byte, int) { - return encodeMessage(self, ts, msg) -} - -func (self *FourByteIrStream) EncodeMessage(ts ffi.EpochTimeMs, msg string) ([]byte, int) { - return encodeMessage(self, ts, msg) + return nil } -func encodeMessage(irEncoder IrEncoder, ts ffi.EpochTimeMs, msg string) ([]byte, int) { - buf, ret := irEncoder.EncodeMessageUnsafe(ts, msg) - if 0 != ret { - return nil, ret +// Encode a log message into CLP IR, returning a view of the encoded message. +func (self *eightByteEncoder) EncodeLogMessage( + logMessage ffi.LogMessage, +) (*LogMessageView[EightByteEncoding], error) { + var logtype C.StringView + var vars C.Int64tSpan + var dictVars C.StringView + var dictVarEndOffsets C.Int32tSpan + err := IrError(C.ir_encoder_encode_eight_byte_log_message( + newCStringView(logMessage), + self.cptr, + &logtype, + &vars, + &dictVars, + &dictVarEndOffsets, + )) + if Success != err { + return nil, EncodeError } - safeBuf := make([]byte, len(buf)) - copy(safeBuf, buf) - return safeBuf, 0 + return newLogMessageView[EightByteEncoding](logtype, vars, dictVars, dictVarEndOffsets), nil } -func (self *EightByteIrStream) EncodeMessageUnsafe(ts ffi.EpochTimeMs, msg string) ([]byte, int) { - return encodeMessageUnsafe(self, ts, msg) +type fourByteEncoder struct { + cptr unsafe.Pointer } -func (self *FourByteIrStream) EncodeMessageUnsafe(ts ffi.EpochTimeMs, msg string) ([]byte, int) { - buf, ret := encodeMessageUnsafe(self, self.prevTimestamp-ts, msg) - if 0 != ret { - return nil, ret +// Close will delete the underlying C++ allocated memory used by the +// deserializer. Failure to call Close will result in a memory leak. +func (self *fourByteEncoder) Close() error { + if nil != self.cptr { + C.ir_encoder_four_byte_close(self.cptr) + self.cptr = nil } - self.prevTimestamp = ts - return buf, ret + return nil } -// returns 0 on success, >0 on error, <0 on c error -// returned byte slice points to c memory and is only valid until the next call -// to encodeMessage (from either EncodeMessage or EncodeMessageUnsafe) -func encodeMessageUnsafe[T EightByteIrStream | FourByteIrStream]( - irstream *T, - timestampOrDelta ffi.EpochTimeMs, - msg string, -) ([]byte, int) { - var ret C.int - var bufPtr unsafe.Pointer - var bufSize uint64 - - switch irs := any(irstream).(type) { - case *EightByteIrStream: - ret = C.eight_byte_encode_message(irs.cPtr, C.int64_t(timestampOrDelta), - unsafe.Pointer(&[]byte(msg)[0]), C.size_t(len(msg)), - &bufPtr, unsafe.Pointer(&bufSize)) - case *FourByteIrStream: - ret = C.four_byte_encode_message(irs.cPtr, C.int64_t(timestampOrDelta), - unsafe.Pointer(&[]byte(msg)[0]), C.size_t(len(msg)), - &bufPtr, unsafe.Pointer(&bufSize)) - default: - return nil, 2 - } - if 0 > ret { - return nil, int(ret) - } - buf := unsafe.Slice((*byte)(bufPtr), bufSize) - if nil == buf { - return nil, 3 +// Encode a log message into CLP IR, returning a view of the encoded message. +func (self *fourByteEncoder) EncodeLogMessage( + logMessage ffi.LogMessage, +) (*LogMessageView[FourByteEncoding], error) { + var logtype C.StringView + var vars C.Int32tSpan + var dictVars C.StringView + var dictVarEndOffsets C.Int32tSpan + err := IrError(C.ir_encoder_encode_four_byte_log_message( + newCStringView(logMessage), + self.cptr, + &logtype, + &vars, + &dictVars, + &dictVarEndOffsets, + )) + if Success != err { + return nil, EncodeError } - return buf, 0 + return newLogMessageView[FourByteEncoding](logtype, vars, dictVars, dictVarEndOffsets), nil } diff --git a/ir/encoder_test.go b/ir/encoder_test.go deleted file mode 100644 index 3610df3..0000000 --- a/ir/encoder_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package ir - -import ( - "fmt" - "io" - "os" - "testing" - "time" - - "github.com/y-scope/clp-ffi-go/ffi" -) - -type WriteCloser interface { - io.Writer - io.Closer -} - -func openIrEncoder(t *testing.T, eightByte bool) (WriteCloser, IrEncoder) { - f, err := os.Create(fmt.Sprintf("../testdata/%s.clp", t.Name())) - if err != nil { - t.Fatalf("os.Create: %v", err) - } - - timestampPattern := "yyyy-MM-dd HH:mm:ss,SSS" - timestampPatternSyntax := "java::SimpleDateFormat" - timeZoneId := "America/Toronto" - - var irEncoder IrEncoder - var preamble []byte - var ret int - if eightByte { - var ebIrs EightByteIrStream - ebIrs, preamble, ret = EightByteEncodePreambleUnsafe(timestampPattern, - timestampPatternSyntax, timeZoneId) - irEncoder = &ebIrs - } else { - var fbIrs FourByteIrStream - fbIrs, preamble, ret = FourByteEncodePreambleUnsafe(timestampPattern, - timestampPatternSyntax, timeZoneId, ffi.EpochTimeMs(time.Now().UnixMilli())) - irEncoder = &fbIrs - } - if 0 != ret { - t.Fatalf("*EncodePreamble failed: %v", ret) - } - n, err := f.Write(preamble) - if n != len(preamble) { - t.Fatalf("short write for preamble: %v/%v", n, len(preamble)) - } - if err != nil { - t.Fatalf("io.Writer.Write preamble: %v", err) - } - return f, irEncoder -} - -func writeIrEncoder(t *testing.T, writer io.Writer, irs IrEncoder) { - msg, ret := irs.EncodeMessageUnsafe(ffi.EpochTimeMs(time.Now().UnixMilli()), "log") - if 0 != ret { - t.Fatalf("EncodeMessageUnsafe failed: %v", ret) - } - n, err := writer.Write(msg) - if n != len(msg) { - t.Fatalf("short write for message: %v/%v", n, len(msg)) - } - if err != nil { - t.Fatalf("io.Writer.Write message: %v", err) - } -} - -func TestUnsafeFourByteIrEncoder(t *testing.T) { - writer, irEncoder := openIrEncoder(t, false) - defer writer.Close() - writeIrEncoder(t, writer, irEncoder) -} diff --git a/ir/ir.go b/ir/ir.go index 84ef775..4e40b53 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -1,63 +1,53 @@ -// Package ir implements interfaces for the encoding and decoding of [CLP] IR -// (intermediate representation) streams through CLP's FFI (foreign function -// interface). More details on CLP IR streams are described in this [Uber -// blog]. -// Log events compressed in IR format can be viewed in the [log viewer] or -// programmatically analyzed using APIs provided here. They can also be -// decompressed back into plain-text log files using CLP (in a future release). +// The ir package implements interfaces for the encoding, decoding, +// serialization, and deserialization of [CLP] IR (intermediate representation) +// streams through CLP's FFI (foreign function interface). More details on CLP +// IR streams are described in this [Uber blog]. +// Log events in IR format can be viewed in the [log viewer] or programmatically +// analyzed using APIs provided in this package. // // [CLP]: https://github.com/y-scope/clp // [Uber blog]: https://www.uber.com/blog/reducing-logging-cost-by-two-orders-of-magnitude-using-clp/ // [log viewer]: https://github.com/y-scope/yscope-log-viewer package ir -import ( - "unsafe" +/* +#include +*/ +import "C" - "github.com/y-scope/clp-ffi-go/ffi" +// Must match c++ equivalent types +type ( + EightByteEncoding = int64 + FourByteEncoding = int32 ) -// TimestampInfo contains information relevant to all timestamps in the IR -// stream. This information comes from the metadata in the IR preamble. +// TimestampInfo contains general information applying to all timestamps in +// contiguous IR. This information comes from the metadata in the IR preamble. type TimestampInfo struct { Pattern string PatternSyntax string TimeZoneId string } -// Empty types used to constrain irStream to ensure the correct encoding size -// is used during encoding and decoding. -type ( - EightByteEncodedVariable struct{} - FourByteEncodedVariable struct{} - EncodedVariable interface { - EightByteEncodedVariable | FourByteEncodedVariable - } -) - -// irStream is constrained by EncodedVariable to prevent mistaken usage of an -// incorrect sized stream. -type irStream[T EncodedVariable] struct { - tsInfo TimestampInfo - cPtr unsafe.Pointer // currently unused in the decoder path -} - -// Returns the TimestampInfo of an irStream. -func (self irStream[T]) TimestampInfo() TimestampInfo { - return self.tsInfo -} - -// Returns the TimestampInfo of an irStream. -type EightByteIrStream struct { - irStream[EightByteEncodedVariable] +// ir.BufView is a slice of CLP IR backed by C++ allocated memory rather than +// the Go heap. A BufView, x, is valid when returned and will remain valid until +// a new BufView is returned by the same object (e.g. an [ir.Serializer]) that +// retuend x. +type BufView = []byte + +// A ir.LogMessage contains all the different components of a log message +// ([ffi.LogMessage]) encoded/separated into fields. +type LogMessage[T EightByteEncoding | FourByteEncoding] struct { + Logtype string + Vars []T + DictVars string + DictVarEndOffsets []int32 } -// FourByteIrStream contains both a CLP IR stream (irStream) and keeps track of -// the previous timestamp seen in the stream. Four byte encoding encodes log -// event timestamps as time deltas from the previous log event. Therefore, we -// must track the previous timestamp to be able to calculate the full timestamp -// of a log event. -type FourByteIrStream struct { - irStream[FourByteEncodedVariable] - prevTimestamp ffi.EpochTimeMs +// A ir.LogMessageView is a [ir.LogMessage] that is backed by C++ allocated +// memory rather than the Go heap. A LogMessageView, x, is valid when returned +// and will remain valid until a new LogMessageView is returned by the same +// object (e.g. an [ir.Encoder]) that retuend x. +type LogMessageView[T EightByteEncoding | FourByteEncoding] struct { + LogMessage[T] } diff --git a/ir/ir_test.go b/ir/ir_test.go new file mode 100644 index 0000000..3b75b3f --- /dev/null +++ b/ir/ir_test.go @@ -0,0 +1,215 @@ +package ir + +import ( + "fmt" + "io" + "math" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/klauspost/compress/zstd" + "github.com/y-scope/clp-ffi-go/ffi" +) + +const ( + defaultTimestampPattern string = "yyyy-MM-dd HH:mm:ss,SSS" + defaultTimestampPatternSyntax string = "java::SimpleDateFormat" + defaultTimeZoneId string = "America/Toronto" +) + +type testArg int + +const ( + eightByteEncoding testArg = iota + fourByteEncoding + noCompression + zstdCompression +) + +var testArgStr = []string{ + "eightByteEncoding", + "fourByteEncoding", + "noCompression", + "zstdCompression", +} + +type testArgs struct { + encoding testArg + compression testArg + name string + filePath string +} + +type preambleFields struct { + TimestampInfo + prevTimestamp ffi.EpochTimeMs +} + +func TestLogMessagesCombo(t *testing.T) { + messages := []ffi.LogMessage{ + "static text dict=var notint123 -1.234 4321.", + "static123 text321 dict=var0123 321.1234 -3210.", + } + testLogMessages(t, messages) +} + +func TestLogMessagesDict(t *testing.T) { + messages := []ffi.LogMessage{ + "textint1234 textequal=variable", + fmt.Sprintf("test=bigint %v", math.MaxInt32+1), + } + testLogMessages(t, messages) +} + +func TestLogMessagesFloat(t *testing.T) { + messages := []ffi.LogMessage{ + "float 1.0 1.2 1.23 1.234", + "-float -1.0 -1.2 -1.23 -1.234", + } + testLogMessages(t, messages) +} + +func TestLogMessagesInt(t *testing.T) { + messages := []ffi.LogMessage{ + "int 1 12 123 1234", + "-int -1 -12 -123 -1234", + } + testLogMessages(t, messages) +} + +func TestLogMessagesStatic(t *testing.T) { + messages := []ffi.LogMessage{ + "static text log zero.", + "static text log one.", + } + testLogMessages(t, messages) +} + +func TestLogMessagesLongLogs(t *testing.T) { + const eightMB int = 8 * 1024 * 1024 + messages := []ffi.LogMessage{ + strings.Repeat("x", eightMB), + strings.Repeat("x", eightMB-1), + } + testLogMessages(t, messages) +} + +func assertEndOfIr( + t *testing.T, + reader io.Reader, + irreader *Reader, +) { + _, err := irreader.Read() + if EndOfIr != err { + t.Fatalf("assertEndOfIr failed got: %v", err) + } +} + +func assertIrLogEvent( + t *testing.T, + reader io.Reader, + irreader *Reader, + event ffi.LogEvent, +) { + log, err := irreader.Read() + if nil != err { + t.Fatalf("Reader.Read failed: %v", err) + } + if event.Timestamp != log.Timestamp { + t.Fatalf("Reader.Read wrong timestamp: '%v' != '%v'", log.Timestamp, event.Timestamp) + } + if event.LogMessage != log.LogMessageView { + t.Fatalf("Reader.Read wrong message: '%v' != '%v'", log.LogMessageView, event.LogMessage) + } + t.Logf("'%v' : '%.128v'\n", log.Timestamp, log.LogMessageView) +} + +func generateTestArgs(t *testing.T, prefix string) []testArgs { + var tests []testArgs + tmpdir := t.TempDir() + for _, encoding := range []testArg{eightByteEncoding, fourByteEncoding} { + for _, compression := range []testArg{noCompression, zstdCompression} { + testName := prefix + "-" + testArgStr[encoding] + "-" + testArgStr[compression] + fileName := testName + ".clp" + if zstdCompression == compression { + fileName += ".zst" + } + filePath := filepath.Join(tmpdir, fileName) + tests = append(tests, testArgs{encoding, compression, testName, filePath}) + } + } + return tests +} + +func testLogMessages(t *testing.T, messages []ffi.LogMessage) { + for _, args := range generateTestArgs(t, t.Name()+"-SerDer") { + args := args // capture range variable for func literal + t.Run( + args.name, + func(t *testing.T) { t.Parallel(); testSerDerLogMessages(t, args, messages) }, + ) + } + for _, args := range generateTestArgs(t, t.Name()+"-WriteRead") { + args := args // capture range variable for func literal + t.Run( + args.name, + func(t *testing.T) { t.Parallel(); testWriteReadLogMessages(t, args, messages) }, + ) + } +} + +func openIoReader(t *testing.T, args testArgs) io.ReadCloser { + file, err := os.Open(args.filePath) + if nil != err { + t.Fatalf("os.Open: %v", err) + } + var reader io.ReadCloser + switch args.compression { + case noCompression: + reader = file + case zstdCompression: + reader, err = newZstdReader(file) + if nil != err { + t.Fatalf("zstd.NewReader failed: %v", err) + } + default: + t.Fatalf("unsupported compression: %v", args.compression) + } + return reader +} + +func openIoWriter(t *testing.T, args testArgs) io.WriteCloser { + file, err := os.Create(args.filePath) + if nil != err { + t.Fatalf("os.Create: %v", err) + } + var writer io.WriteCloser + switch args.compression { + case noCompression: + writer = file + case zstdCompression: + writer, err = zstd.NewWriter(file) + if nil != err { + t.Fatalf("zstd.NewWriter failed: %v", err) + } + default: + t.Fatalf("unsupported compression: %v", args.compression) + } + return writer +} + +type zstdReader struct { + *zstd.Decoder +} + +func newZstdReader(reader io.Reader) (*zstdReader, error) { + zreader, err := zstd.NewReader(reader) + return &zstdReader{zreader}, err +} + +func (self *zstdReader) Close() error { + self.Decoder.Close() + return nil +} diff --git a/ir/irerror.go b/ir/irerror.go index d29044a..e1b83e9 100644 --- a/ir/irerror.go +++ b/ir/irerror.go @@ -1,20 +1,22 @@ package ir -// IRError mirrors cpp type IRErrorCode defined in: +// IrError mirrors cpp type IRErrorCode defined in: // clp/components/core/src/ffi/ir_stream/decoding_methods.hpp -//go:generate stringer -type=IRError -type IRError int +// +//go:generate stringer -type=IrError +type IrError int const ( - Success IRError = iota + Success IrError = iota DecodeError - Eof - CorruptedIR - CorruptedMetadata - IncompleteIR - UnsupportedVersion + EndOfIr + CorruptedIr + IncompleteIr + QueryNotFound // must be IncompleteIr + 1 + EncodeError // not from clp + UnsupportedVersion // not from clp ) -func (self IRError) Error() string { +func (self IrError) Error() string { return self.String() } diff --git a/ir/irerror_string.go b/ir/irerror_string.go index cf2585e..39133ad 100644 --- a/ir/irerror_string.go +++ b/ir/irerror_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=IRError"; DO NOT EDIT. +// Code generated by "stringer -type=IrError"; DO NOT EDIT. package ir @@ -10,20 +10,21 @@ func _() { var x [1]struct{} _ = x[Success-0] _ = x[DecodeError-1] - _ = x[Eof-2] - _ = x[CorruptedIR-3] - _ = x[CorruptedMetadata-4] - _ = x[IncompleteIR-5] - _ = x[UnsupportedVersion-6] + _ = x[EndOfIr-2] + _ = x[CorruptedIr-3] + _ = x[IncompleteIr-4] + _ = x[QueryNotFound-5] + _ = x[EncodeError-6] + _ = x[UnsupportedVersion-7] } -const _IRError_name = "SuccessDecodeErrorEofCorruptedIRCorruptedMetadataIncompleteIRUnsupportedVersion" +const _IrError_name = "SuccessDecodeErrorEndOfIrCorruptedIrIncompleteIrQueryNotFoundEncodeErrorUnsupportedVersion" -var _IRError_index = [...]uint8{0, 7, 18, 21, 32, 49, 61, 79} +var _IrError_index = [...]uint8{0, 7, 18, 25, 36, 48, 61, 72, 90} -func (i IRError) String() string { - if i < 0 || i >= IRError(len(_IRError_index)-1) { - return "IRError(" + strconv.FormatInt(int64(i), 10) + ")" +func (i IrError) String() string { + if i < 0 || i >= IrError(len(_IrError_index)-1) { + return "IrError(" + strconv.FormatInt(int64(i), 10) + ")" } - return _IRError_name[_IRError_index[i]:_IRError_index[i+1]] + return _IrError_name[_IrError_index[i]:_IrError_index[i+1]] } diff --git a/ir/reader.go b/ir/reader.go index 5511cd8..c19eb45 100644 --- a/ir/reader.go +++ b/ir/reader.go @@ -1,118 +1,128 @@ package ir import ( - "bytes" "io" + "strings" "github.com/y-scope/clp-ffi-go/ffi" + "github.com/y-scope/clp-ffi-go/search" ) -// IrReader abstracts maintenance of a buffer containing an IR stream. It keeps -// track of the range in the buffer containing valid, unconsumed IR. It does -// not store a Reader to allow callers to mutate the Reader as necessary. -type IrReader struct { - IrDecoder - buf []byte - start int - end int +// Reader abstracts maintenance of a buffer containing a [Deserializer]. It +// keeps track of the range [start, end) in the buffer containing valid, +// unconsumed CLP IR. [NewReader] will construct a Reader with the appropriate +// Deserializer based on the consumed CLP IR preamble. The buffer will grow if +// it is not large enough to service a read call (e.g. it cannot hold the next +// log event in the IR). Close must be called to free the underlying memory and +// failure to do so will result in a memory leak. +type Reader struct { + Deserializer + ioReader io.Reader + buf []byte + start int + end int } -// adjust_buf mutates the IrReader.buf so that the next read call has space to -// fill. If the start of IrReader.buf is not 0 the contents of buf will be -// shifted back, so that end -= start and start = 0. If start is already 0 the -// buffer is grown. -func (self *IrReader) adjust_buf() int { - if 0 == self.start { - buf := make([]byte, len(self.buf)*2) - copy(buf, self.buf[self.start:self.end]) - self.buf = buf - } else { - copy(self.buf, self.buf[self.start:self.end]) +// NewReaderSize creates a new [Reader] and uses [DeserializePreamble] to read a +// CLP IR preamble from the [io.Reader], r. size denotes the initial size to use +// for the Reader's buffer that the io.Reader is read into. This buffer will +// grow if it is too small to contain the preamble or next log event. Returns: +// - success: valid [*Reader], nil +// - error: nil [*Reader], error propagated from [DeserializePreamble] or +// [ioReader.Read] +func NewReaderSize(r io.Reader, size int) (*Reader, error) { + irr := &Reader{nil, r, make([]byte, size), 0, 0} + var err error + if _, err = irr.read(); nil != err { + return nil, err } - self.end -= self.start - self.start = 0 - return len(self.buf) -} - -// read is a wrapper around the Read call to the io.Reader. It uses the correct -// range in buf and adjusts the range accordingly. On success nil is returned. -// On failure an error whose type depends on the io.Reader is returned. -// Note we do not return io.EOF if n > 0 as we have not yet consumed the IR. -func (self *IrReader) read(r io.Reader) error { - n, err := r.Read(self.buf[self.end:]) - if nil != err && io.EOF != err { - return err + for { + irr.Deserializer, irr.start, err = DeserializePreamble(irr.buf[irr.start:irr.end]) + if IncompleteIr != err { + break + } + if _, err = irr.fillBuf(); nil != err { + break + } } - self.end += n - return nil + if nil != err { + return nil, err + } + return irr, nil } -// ReadPreamble uses [DecodePreamble] to read an IR stream preamble from r. -// bufSize denotes the initial size to use for the underlying buffer io.Reader -// is read into. This buffer will grow if it is too small to contain the -// preamble or next log event. -// Return values: -// - nil == error: success -// - [IRError] or [encoding/json]: error propagated from [DecodePreamble] -// - [io] error type or underlying reader type: io.Reader.Read failed -func ReadPreamble(r io.Reader, bufSize int) (IrReader, error) { - irr := IrReader{nil, make([]byte, bufSize), 0, 0} +// Returns [NewReaderSize] with a default buffer size of 1MB. +func NewReader(r io.Reader) (*Reader, error) { + return NewReaderSize(r, 1024*1024) +} - if err := irr.read(r); nil != err { - return irr, err - } +// Close will delete the underlying C++ allocated memory used by the +// deserializer. Failure to call Close will result in a memory leak. +func (self *Reader) Close() error { + return self.Deserializer.Close() +} +// Read uses [DeserializeLogEvent] to read from the CLP IR byte stream. The +// underlying buffer will grow if it is too small to contain the next log event. +// On error returns: +// - nil *ffi.LogEventView +// - error propagated from [DeserializeLogEvent] or [ioReader.Read] +func (self *Reader) Read() (*ffi.LogEventView, error) { + var event *ffi.LogEventView + var pos int + var err error for { - var err error - irr.IrDecoder, irr.start, err = DecodePreamble(irr.buf[irr.start:irr.end]) - if nil == err { - return irr, nil - } else if IncompleteIR == err { - irr.adjust_buf() - if err := irr.read(r); nil != err { - return irr, err - } - } else { - return irr, err + event, pos, err = self.DeserializeLogEvent(self.buf[self.start:self.end]) + if IncompleteIr != err { + break + } + if _, err = self.fillBuf(); nil != err { + break } } + if nil != err { + return nil, err + } + self.start += pos + return event, nil } - -// ReadNextLogEvent uses [DecodeNextLogEvent] to read from the IR stream in r. -// bufSize denotes the initial size to use for the underlying buffer io.Reader -// is read into. This buffer will grow if it is too small to contain the -// preamble or next log event. -// Return values: -// - nil == error: success -// - IRError.Eof: CLP found the IR stream EOF tag -// - io.EOF: io.Reader.Read got EOF -// - else: -// - type [IRError]: error propagated from [DecodeNextLogEvent] -// - type from io.Reader: io.Reader.Read failed -func (self *IrReader) ReadNextLogEvent(r io.Reader) (ffi.LogEvent, error) { +func (self *Reader) ReadToWildcardMatch( + timeInterval search.TimestampInterval, + queries []search.WildcardQuery, +) (*ffi.LogEventView, int, error) { + var event *ffi.LogEventView + var pos int + var matchingQuery int + var err error + mergedQuery := search.MergeWildcardQueries(queries) for { - event, offset, err := self.DecodeNextLogEvent(self.buf[self.start:self.end]) - if nil == err { - self.start += offset - return event, nil - } else if IncompleteIR == err { - self.adjust_buf() - if err := self.read(r); nil != err { - return event, err - } - } else { - return event, err + event, pos, matchingQuery, err = self.DeserializeWildcardMatch( + self.buf[self.start:self.end], + timeInterval, + mergedQuery, + ) + if IncompleteIr != err { + break + } + if _, err = self.fillBuf(); nil != err { + break } } + if nil != err { + return nil, -1, err + } + self.start += pos + return event, matchingQuery, nil } -// Read the IR stream using the io.Reader until f returns true for a -// [ffi.LogEvent]. The succeeding LogEvent is returned. Errors are propagated -// from ReadNextLogEvent. -func (self *IrReader) ReadToFunc(r io.Reader, f func(ffi.LogEvent) bool) (ffi.LogEvent, error) { +// Read the CLP IR byte stream until f returns true for a [ffi.LogEventView]. +// The successful LogEvent is returned. Errors are propagated from [Read]. +func (self *Reader) ReadToFunc( + f func(*ffi.LogEventView) bool, +) (*ffi.LogEventView, error) { for { - event, err := self.ReadNextLogEvent(r) + event, err := self.Read() if nil != err { return event, err } @@ -122,26 +132,72 @@ func (self *IrReader) ReadToFunc(r io.Reader, f func(ffi.LogEvent) bool) (ffi.Lo } } -// Read the IR stream using the io.Reader until [ffi.LogEvent.Timestamp] >= -// time. Errors are propagated from ReadNextLogEvent. -func (self *IrReader) ReadToEpochTime(r io.Reader, time ffi.EpochTimeMs) (ffi.LogEvent, error) { - return self.ReadToFunc(r, func(e ffi.LogEvent) bool { return e.Timestamp >= time }) +// Read the CLP IR stream until a [ffi.LogEventView] is greater than or equal to +// the given timestamp. Errors are propagated from [ReadToFunc]. +func (self *Reader) ReadToEpochTime( + time ffi.EpochTimeMs, +) (*ffi.LogEventView, error) { + return self.ReadToFunc(func(event *ffi.LogEventView) bool { return event.Timestamp >= time }) +} + +// Read the CLP IR stream until [strings/Contains] returns true for a +// [ffi.LogEventView] and the given sub string. Errors are propagated from +// [ReadToFunc]. +func (self *Reader) ReadToContains(substr string) (*ffi.LogEventView, error) { + fn := func(event *ffi.LogEventView) bool { + return strings.Contains(event.LogMessageView, substr) + } + return self.ReadToFunc(fn) +} + +// Read the CLP IR stream until [strings/HasPrefix] returns true for a +// [ffi.LogEventView] and the given prefix. Errors are propagated from +// [ReadToFunc]. +func (self *Reader) ReadToPrefix(prefix string) (*ffi.LogEventView, error) { + fn := func(event *ffi.LogEventView) bool { + return strings.HasPrefix(event.LogMessageView, prefix) + } + return self.ReadToFunc(fn) } -// Read the IR stream using the io.Reader until [bytes/Contains] returns true -// for [ffi.LogEvent.Msg] and subslice. Errors are propagated from ReadNextLogEvent. -func (self *IrReader) ReadToContains(r io.Reader, subslice []byte) (ffi.LogEvent, error) { - return self.ReadToFunc(r, func(e ffi.LogEvent) bool { return bytes.Contains(e.Msg, subslice) }) +// Read the CLP IR stream until [strings/HasSuffix] returns true for a +// [ffi.LogEventView] and the given suffix. Errors are propagated from +// [ReadToFunc]. +func (self *Reader) ReadToSuffix(suffix string) (*ffi.LogEventView, error) { + fn := func(event *ffi.LogEventView) bool { + return strings.HasSuffix(event.LogMessageView, suffix) + } + return self.ReadToFunc(fn) } -// Read the IR stream using the io.Reader until [bytes/HasPrefix] returns true -// for [ffi.LogEvent.Msg] and prefix. Errors are propagated from ReadNextLogEvent. -func (self *IrReader) ReadToPrefix(r io.Reader, prefix []byte) (ffi.LogEvent, error) { - return self.ReadToFunc(r, func(e ffi.LogEvent) bool { return bytes.HasPrefix(e.Msg, prefix) }) +// fillBuf shifts the remaining valid IR in [Reader.buf] to the front and then +// calls [ioReader.Read] to fill the remainder with more IR. Before reading into +// the buffer, it is grown by 1.5x if more than 1/4th of it is unconsumed IR. +// Forwards the return of [ioReader.Read]. +func (self *Reader) fillBuf() (int, error) { + if (self.end - self.start) > len(self.buf)>>2 { + buf := make([]byte, len(self.buf)+len(self.buf)/2) + copy(buf, self.buf[self.start:self.end]) + self.buf = buf + } else { + copy(self.buf, self.buf[self.start:self.end]) + } + self.end -= self.start + self.start = 0 + n, err := self.read() + return n, err } -// Read the IR stream using the io.Reader until [bytes/HasSuffix] returns true -// for [ffi.LogEvent.Msg] field and suffix. Errors are propagated from ReadNextLogEvent. -func (self *IrReader) ReadToSuffix(r io.Reader, suffix []byte) (ffi.LogEvent, error) { - return self.ReadToFunc(r, func(e ffi.LogEvent) bool { return bytes.HasSuffix(e.Msg, suffix) }) +// read is a wrapper around a io.Reader.Read call. It uses the correct range in +// buf and adjusts the range accordingly. Always returns the number of bytes +// read. On success nil is returned. On failure an error is forwarded from +// [io.Reader], unless n > 0 and io.EOF == err as we have not yet consumed the +// CLP IR. +func (self *Reader) read() (int, error) { + n, err := self.ioReader.Read(self.buf[self.end:]) + self.end += n + if nil != err && io.EOF != err { + return n, err + } + return n, nil } diff --git a/ir/reader_test.go b/ir/reader_test.go index 796f6d4..a37cd32 100644 --- a/ir/reader_test.go +++ b/ir/reader_test.go @@ -1,24 +1,24 @@ package ir import ( - "fmt" - "io" + "math" "os" "testing" "time" - "runtime" "github.com/klauspost/compress/zstd" - "github.com/y-scope/clp-ffi-go/test" + "github.com/y-scope/clp-ffi-go/ffi" + "github.com/y-scope/clp-ffi-go/search" ) -func TestFourByteIrReader(t *testing.T) { - if 0 == len(os.Args) { - t.Fatalf("This test requires an input ir stream from -args: %v", os.Args) +func TestIrReader(t *testing.T) { + var fpath string = os.Getenv("go_test_ir") + if "" == fpath { + t.Skip("Set an input ir stream using the env variable: go_test_ir") } var err error var file *os.File - if file, err = os.Open(os.Args[len(os.Args)-1]); nil != err { + if file, err = os.Open(fpath); nil != err { t.Fatalf("os.Open failed: %v", err) } defer file.Close() @@ -26,25 +26,32 @@ func TestFourByteIrReader(t *testing.T) { reader, _ := zstd.NewReader(file) defer reader.Close() - var irr IrReader - if irr, err = ReadPreamble(reader, 4096); nil != err { - t.Fatalf("ReadPreamble failed: %v", err) + var irr *Reader + if irr, err = NewReaderSize(reader, 512*1024*1024); nil != err { + t.Fatalf("NewReader failed: %v", err) } + defer irr.Close() - fins := []test.Finalizer{} + interval := search.TimestampInterval{Lower: 0, Upper: math.MaxInt64} + queries := []search.WildcardQuery{ + search.NewWildcardQuery("*ERROR*", true), + search.NewWildcardQuery("*WARN*", true), + } for { - // log, err := irr.ReadNextLogEvent(reader) - log, err := irr.ReadToContains(reader, []byte("ERROR")) - // run GC to try and test that log.Msg isn't freed by finalizer - runtime.GC() - if nil == err { - fmt.Printf("msg: %v | %v", time.UnixMilli(int64(log.Timestamp)), string(log.Msg)) - } else if Eof == err || io.EOF == err { + var log *ffi.LogEventView + // log, err = irr.Read() + // log, err = irr.ReadToContains("ERROR") + // var _ search.WildcardQuery + log, _, err = irr.ReadToWildcardMatch( + interval, + queries, + ) + if nil != err { break - } else { - t.Fatalf("ReadNextLogEvent failed: %v", err) } - fins = append(fins, test.NewFinalizer(&log)) + t.Logf("msg: %v | %v", time.UnixMilli(int64(log.Timestamp)), log.LogMessageView) + } + if EndOfIr != err { + t.Fatalf("Reader.Read failed: %v", err) } - test.AssertFinalizers(t, fins...) } diff --git a/ir/serder_test.go b/ir/serder_test.go new file mode 100644 index 0000000..f75fb31 --- /dev/null +++ b/ir/serder_test.go @@ -0,0 +1,161 @@ +package ir + +import ( + "io" + "testing" + "time" + + "github.com/y-scope/clp-ffi-go/ffi" +) + +func TestPreamble(t *testing.T) { + preamble := preambleFields{ + TimestampInfo{defaultTimestampPattern, defaultTimestampPatternSyntax, defaultTimeZoneId}, + ffi.EpochTimeMs(time.Now().UnixMilli()), + } + for _, args := range generateTestArgs(t, t.Name()) { + args := args // capture range variable for func literal + t.Run(args.name, func(t *testing.T) { t.Parallel(); testPreamble(t, args, preamble) }) + } +} + +func testPreamble(t *testing.T, args testArgs, preamble preambleFields) { + writer := openIoWriter(t, args) + irSerializer := serializeIrPreamble(t, args, preamble, writer) + + writer.Close() + irSerializer.Close() + + reader := openIoReader(t, args) + assertIrPreamble(t, args, reader, preamble) +} + +func testSerDerLogMessages( + t *testing.T, + args testArgs, + logMessages []ffi.LogMessage, +) { + ioWriter := openIoWriter(t, args) + + preamble := preambleFields{ + TimestampInfo{defaultTimestampPattern, defaultTimestampPatternSyntax, defaultTimeZoneId}, + ffi.EpochTimeMs(time.Now().UnixMilli()), + } + irSerializer := serializeIrPreamble(t, args, preamble, ioWriter) + + var events []ffi.LogEvent + for _, msg := range logMessages { + event := ffi.LogEvent{ + LogMessage: msg, + Timestamp: ffi.EpochTimeMs(time.Now().UnixMilli()), + } + irView, err := irSerializer.SerializeLogEvent(event) + if nil != err { + t.Fatalf("SerializeLogEvent failed: %v", err) + } + _, err = ioWriter.Write(irView) + if nil != err { + t.Fatalf("io.Writer.Write message: %v", err) + } + events = append(events, event) + } + irSerializer.Close() + ioWriter.Write([]byte{0x0}) + ioWriter.Close() + + ioReader := openIoReader(t, args) + defer ioReader.Close() + irReader := assertIrPreamble(t, args, ioReader, preamble) + defer irReader.Close() + + for _, event := range events { + assertIrLogEvent(t, ioReader, irReader, event) + } + assertEndOfIr(t, ioReader, irReader) +} + +func serializeIrPreamble( + t *testing.T, + args testArgs, + preamble preambleFields, + writer io.Writer, +) Serializer { + var err error + var serializer Serializer + var preambleIr BufView + switch args.encoding { + case eightByteEncoding: + serializer, preambleIr, err = EightByteSerializer( + preamble.Pattern, + preamble.PatternSyntax, + preamble.TimeZoneId, + ) + case fourByteEncoding: + serializer, preambleIr, err = FourByteSerializer( + preamble.Pattern, + preamble.PatternSyntax, + preamble.TimeZoneId, + preamble.prevTimestamp, + ) + default: + t.Fatalf("unsupported encoding: %v", args.encoding) + } + if nil != err { + t.Fatalf("constructor failed: %v", err) + } + n, err := writer.Write(preambleIr) + if n != len(preambleIr) { + t.Fatalf("short write for preamble: %v/%v", n, len(preambleIr)) + } + if nil != err { + t.Fatalf("io.Writer.Write preamble: %v", err) + } + return serializer +} + +func assertIrPreamble( + t *testing.T, + args testArgs, + reader io.Reader, + preamble preambleFields, +) *Reader { + irreader, err := NewReaderSize(reader, 4096) + if nil != err { + t.Fatalf("NewReader failed: %v", err) + } + if irreader.TimestampInfo().Pattern != preamble.Pattern { + t.Fatalf( + "NewReader wrong pattern: '%v' != '%v'", + irreader.TimestampInfo().Pattern, + preamble.Pattern, + ) + } + if irreader.TimestampInfo().PatternSyntax != preamble.PatternSyntax { + t.Fatalf( + "NewReader wrong pattern syntax: '%v' != '%v'", + irreader.TimestampInfo().PatternSyntax, + preamble.PatternSyntax, + ) + } + if irreader.TimestampInfo().TimeZoneId != preamble.TimeZoneId { + t.Fatalf( + "NewReader wrong time zone id: '%v' != '%v'", + irreader.TimestampInfo().TimeZoneId, + preamble.TimeZoneId, + ) + } + if fourByteEncoding == args.encoding { + deserializer, ok := irreader.Deserializer.(*fourByteDeserializer) + if false == ok { + t.Fatalf("casting Deserializer to *fourByteDeserializer failed for fourByteEncoding.") + } + if deserializer.prevTimestamp != preamble.prevTimestamp { + t.Fatalf( + "NewReader wrong reference timestamp: '%v' != '%v'", + deserializer.prevTimestamp, + preamble.prevTimestamp, + ) + } + } + return irreader +} diff --git a/ir/serializer.go b/ir/serializer.go new file mode 100644 index 0000000..2aad104 --- /dev/null +++ b/ir/serializer.go @@ -0,0 +1,172 @@ +package ir + +/* +#include +#include +*/ +import "C" + +import ( + "unsafe" + + "github.com/y-scope/clp-ffi-go/ffi" +) + +// A Serializer exports functions to serialize log events into a CLP IR byte +// stream. Serialization functions only return views (slices) of IR bytes, +// leaving their use to the user. Each Serializer owns its own unique underlying +// memory for the views it produces/returns. This memory is reused for each +// view, so to persist the contents the memory must be copied into another +// object. Close must be called to free the underlying memory and failure to do +// so will result in a memory leak. +type Serializer interface { + SerializeLogEvent(event ffi.LogEvent) (BufView, error) + TimestampInfo() TimestampInfo + Close() error +} + +// EightByteSerializer creates and returns a new Serializer that writes eight +// byte encoded CLP IR and serializes a IR preamble into a BufView using it. On +// error returns: +// - nil Serializer +// - nil BufView +// - [IrError] error: CLP failed to successfully serialize +func EightByteSerializer( + tsPattern string, + tsPatternSyntax string, + timeZoneId string, +) (Serializer, BufView, error) { + var irView C.ByteSpan + irs := eightByteSerializer{ + commonSerializer{TimestampInfo{tsPattern, tsPatternSyntax, timeZoneId}, nil}, + } + if err := IrError(C.ir_serializer_serialize_eight_byte_preamble( + newCStringView(tsPattern), + newCStringView(tsPatternSyntax), + newCStringView(timeZoneId), + &irs.cptr, + &irView, + )); Success != err { + return nil, nil, err + } + return &irs, unsafe.Slice((*byte)(irView.m_data), irView.m_size), nil +} + +// FourByteSerializer creates and returns a new Serializer that writes four byte +// encoded CLP IR and serializes a IR preamble into a BufView using it. On error +// returns: +// - nil Serializer +// - nil BufView +// - [IrError] error: CLP failed to successfully serialize +func FourByteSerializer( + tsPattern string, + tsPatternSyntax string, + timeZoneId string, + referenceTs ffi.EpochTimeMs, +) (Serializer, BufView, error) { + var irView C.ByteSpan + irs := fourByteSerializer{ + commonSerializer{TimestampInfo{tsPattern, tsPatternSyntax, timeZoneId}, nil}, + referenceTs, + } + if err := IrError(C.ir_serializer_serialize_four_byte_preamble( + newCStringView(tsPattern), + newCStringView(tsPatternSyntax), + newCStringView(timeZoneId), + C.int64_t(referenceTs), + &irs.cptr, + &irView, + )); Success != err { + return nil, nil, err + } + return &irs, unsafe.Slice((*byte)(irView.m_data), irView.m_size), nil +} + +// commonSerializer contains fields common to all types of CLP IR encoding. +// TimestampInfo stores information common to all timestamps found in the IR. +// cptr holds a reference to the underlying C++ objected used as backing storage +// for the Views returned by the serializer. Close must be called to free this +// underlying memory and failure to do so will result in a memory leak. +type commonSerializer struct { + tsInfo TimestampInfo + cptr unsafe.Pointer +} + +// Close will delete the underlying C++ allocated memory used by the +// deserializer. Failure to call Close will result in a memory leak. +func (self *commonSerializer) Close() error { + if nil != self.cptr { + C.ir_serializer_close(self.cptr) + self.cptr = nil + } + return nil +} + +// Returns the TimestampInfo of the Serializer. +func (self commonSerializer) TimestampInfo() TimestampInfo { + return self.tsInfo +} + +type eightByteSerializer struct { + commonSerializer +} + +// SerializeLogEvent attempts to serialize the log event, event, into a eight +// byte encoded CLP IR byte stream. On error returns: +// - a nil BufView +// - [IrError] based on the failure of the Cgo call +func (self *eightByteSerializer) SerializeLogEvent( + event ffi.LogEvent, +) (BufView, error) { + return serializeLogEvent(self, event) +} + +// fourByteSerializer contains both a common CLP IR serializer and stores the +// previously seen log event's timestamp. The previous timestamp is necessary to +// calculate the current timestamp as four byte encoding only encodes the +// timestamp delta between the current log event and the previous. +type fourByteSerializer struct { + commonSerializer + prevTimestamp ffi.EpochTimeMs +} + +// SerializeLogEvent attempts to serialize the log event, event, into a four +// byte encoded CLP IR byte stream. On error returns: +// - nil BufView +// - [IrError] based on the failure of the Cgo call +func (self *fourByteSerializer) SerializeLogEvent( + event ffi.LogEvent, +) (BufView, error) { + return serializeLogEvent(self, event) +} + +func serializeLogEvent( + serializer Serializer, + event ffi.LogEvent, +) (BufView, error) { + var irView C.ByteSpan + var err error + switch irs := serializer.(type) { + case *eightByteSerializer: + err = IrError(C.ir_serializer_serialize_eight_byte_log_event( + newCStringView(event.LogMessage), + C.int64_t(event.Timestamp), + irs.cptr, + &irView, + )) + case *fourByteSerializer: + err = IrError(C.ir_serializer_serialize_four_byte_log_event( + newCStringView(event.LogMessage), + C.int64_t(event.Timestamp-irs.prevTimestamp), + irs.cptr, + &irView, + )) + if Success == err { + irs.prevTimestamp = event.Timestamp + } + } + if Success != err { + return nil, err + } + return unsafe.Slice((*byte)(irView.m_data), irView.m_size), nil +} diff --git a/ir/writer.go b/ir/writer.go new file mode 100644 index 0000000..e3f6d11 --- /dev/null +++ b/ir/writer.go @@ -0,0 +1,135 @@ +package ir + +import ( + "bytes" + "fmt" + "io" + "time" + + "github.com/y-scope/clp-ffi-go/ffi" +) + +// Writer builds up a buffer of serialized CLP IR using a [Serializer]. +// [NewWriter] will construct a Writer with the appropriate Serializer based on +// the arguments used. Close must be called to free the underlying memory and +// failure to do so will result in a memory leak. To write a complete IR stream +// Close must be called before the final WriteTo call. +type Writer struct { + Serializer + buf bytes.Buffer +} + +// Returns [NewWriterSize] with a FourByteEncoding Serializer using the local +// time zone, and a buffer size of 1MB. +func NewWriter() (*Writer, error) { + return NewWriterSize[FourByteEncoding](1024*1024, time.Local.String()) +} + +// NewWriterSize creates a new [Writer] with a [Serializer] based on T, and +// writes a CLP IR preamble. The preamble is stored inside the Writer's internal +// buffer to be written out later. The size parameter denotes the initial buffer +// size to use and timeZoneId denotes the time zone of the source producing the +// log events, so that local times (any time that is not a unix timestamp) are +// handled correctly. +// - success: valid [*Writer], nil +// - error: nil [*Writer], invalid type error or an error propagated from +// [FourByteSerializer], [EightByteSerializer], or [bytes.Buffer.Write] +func NewWriterSize[T EightByteEncoding | FourByteEncoding]( + size int, + timeZoneId string, +) (*Writer, error) { + var irw Writer + irw.buf.Grow(size) + + var irView BufView + var err error + var t T + switch any(t).(type) { + case EightByteEncoding: + irw.Serializer, irView, err = EightByteSerializer( + "", + "", + timeZoneId, + ) + case FourByteEncoding: + irw.Serializer, irView, err = FourByteSerializer( + "", + "", + timeZoneId, + ffi.EpochTimeMs(time.Now().UnixMilli()), + ) + default: + err = fmt.Errorf("Invalid type: %T", t) + } + if nil != err { + return nil, err + } + _, err = irw.buf.Write(irView) + if nil != err { + return nil, err + } + return &irw, nil +} + +// Close will write a null byte denoting the end of the IR stream and delete the +// underlying C++ allocated memory used by the serializer. Failure to call Close +// will result in a memory leak. +func (self *Writer) Close() error { + self.buf.WriteByte(0x0) + return self.Serializer.Close() +} + +// CloseTo is a combination of [Close] and [WriteTo]. It will completely close +// the Writer (and underlying serializer) and write the data out to the +// io.Writer. +// Returns: +// - success: number of bytes written, nil +// - error: number of bytes written, error propagated from [WriteTo] +func (self *Writer) CloseTo(w io.Writer) (int64, error) { + self.Close() + return self.WriteTo(w) +} + +// Bytes returns a slice of the Writer's internal buffer. The slice is valid for +// use only until the next buffer modification (that is, only until the next +// call to Write, WriteTo, or Reset). +func (self *Writer) Bytes() []byte { + return self.buf.Bytes() +} + +// Reset resets the buffer to be empty, but it retains the underlying storage +// for use by future writes. +func (self *Writer) Reset() { + self.buf.Reset() +} + +// Write uses [SerializeLogEvent] to serialize the provided log event to CLP IR +// and then stores it in the internal buffer. Returns: +// - success: number of bytes written, nil +// - error: number of bytes written (can be 0), error propagated from +// [SerializeLogEvent] or [bytes.Buffer.Write] +func (self *Writer) Write(event ffi.LogEvent) (int, error) { + irView, err := self.SerializeLogEvent(event) + if nil != err { + return 0, err + } + n, err := self.buf.Write(irView) + if nil != err { + return n, err + } + return n, nil +} + +// WriteTo writes data to w until the buffer is drained or an error occurs. If +// no error occurs the buffer is reset. On an error the user is expected to use +// [self.Bytes] and [self.Reset] to manually handle the buffer's contents before +// continuing. Returns: +// - success: number of bytes written, nil +// - error: number of bytes written, error propagated from [bytes.Buffer.WriteTo] +func (self *Writer) WriteTo(w io.Writer) (int64, error) { + n, err := self.buf.WriteTo(w) + if nil == err { + self.buf.Reset() + } + return n, err +} diff --git a/ir/writeread_test.go b/ir/writeread_test.go new file mode 100644 index 0000000..d529898 --- /dev/null +++ b/ir/writeread_test.go @@ -0,0 +1,70 @@ +package ir + +import ( + "io" + "testing" + "time" + + "github.com/y-scope/clp-ffi-go/ffi" +) + +func testWriteReadLogMessages( + t *testing.T, + args testArgs, + messages []ffi.LogMessage, +) { + ioWriter := openIoWriter(t, args) + irWriter := openIrWriter(t, args, ioWriter) + + var events []ffi.LogEvent + for _, msg := range messages { + event := ffi.LogEvent{ + LogMessage: msg, + Timestamp: ffi.EpochTimeMs(time.Now().UnixMilli()), + } + _, err := irWriter.Write(event) + if nil != err { + t.Fatalf("ir.Writer.Write failed: %v", err) + } + events = append(events, event) + } + _, err := irWriter.CloseTo(ioWriter) + if nil != err { + t.Fatalf("ir.Writer.CloseTo failed: %v", err) + } + ioWriter.Close() + + ioReader := openIoReader(t, args) + defer ioReader.Close() + irReader, err := NewReader(ioReader) + if nil != err { + t.Fatalf("NewReader failed: %v", err) + } + defer irReader.Close() + + for _, event := range events { + assertIrLogEvent(t, ioReader, irReader, event) + } + assertEndOfIr(t, ioReader, irReader) +} + +func openIrWriter( + t *testing.T, + args testArgs, + writer io.Writer, +) *Writer { + var irWriter *Writer + var err error + switch args.encoding { + case eightByteEncoding: + irWriter, err = NewWriterSize[EightByteEncoding](1024*1024, defaultTimeZoneId) + case fourByteEncoding: + irWriter, err = NewWriterSize[FourByteEncoding](1024*1024, defaultTimeZoneId) + default: + t.Fatalf("unsupported encoding: %v", args.encoding) + } + if nil != err { + t.Fatalf("NewWriterSize failed: %v", err) + } + return irWriter +} diff --git a/lib/libclp_ffi_linux_amd64.a b/lib/libclp_ffi_linux_amd64.a new file mode 100644 index 0000000000000000000000000000000000000000..aa630bac3ec7c4844f45461e0dff6adc85b2c5b5 GIT binary patch literal 326866 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 literal 0 HcmV?d00001 diff --git a/lib/libclp_ffi_linux_amd64.so b/lib/libclp_ffi_linux_amd64.so deleted file mode 100644 index 4e5a5edce3fd3c0f81689edaf3bcd56ba1c0b0c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 142208 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 mOy2QWaf{#6;ot67AoXC -#include -*/ -import "C" - -import ( - "runtime" - "unsafe" - - "github.com/y-scope/clp-ffi-go/ffi" -) - -/* TODO outdated -There are two sets of structs exposed: -1. DecodedMessage, EncodedMessage -The fields of these structs point to regular go memory. This memory was -populated by copying the data returned by the native calls. The behaviour of -these structs is the same as any normal go struct, at the expense of extra -copying. -To greatly simplify decoding an EncodedMessage uses a private reference to the -DecodedMessageUnsafe that created it. Holding this reference prevents the c -memory from being freed and will increase memory usage. ReleaseRef can be used -to drop this reference, but DecodeMessage will always return an error -afterwards. - -2. DecodedMessageUnsafe, EncodedMessageUnsafe -The fields of these structs point to c memory. Slices are created to wrap the -native memory, with no copying performed. The underlying c memory will be freed -by a finalizer set on the creation of these objects. This means once the -original object becomes unreachable any access to the underlying memory is -undefined. Any reference created to this memory (e.g. making a copy of the -object or fields, new slices of the fields, etc) is only valid as long as the -original object is reachable. -With that said, if all usage is made through the original object, practical -usage will behave as expected. - -There is no common interface to abstract/generalize the use of these structs. -It would be fairly easy for an unsuspecting user to pass an unsafe structure to -a function that will not handle it properly. We encourage the user to be -explicit about the usage of unsafe structs. - -*/ - -type EncodedMessage struct { - Logtype []byte - Vars []byte - DictVars []byte - DictVarEndOffsets []int32 - unsafeRef *EncodedMessageUnsafe -} - -type EncodedMessageUnsafe struct { - Logtype []byte - Vars []byte - DictVars []byte - DictVarEndOffsets []int32 - cPtr unsafe.Pointer -} - -func (self *EncodedMessage) ReleaseRef() { - self.unsafeRef = nil -} - -func (self *EncodedMessageUnsafe) MakeSafe() EncodedMessage { - var em EncodedMessage - em.unsafeRef = self - em.Logtype = make([]byte, len(self.Logtype)) - em.Vars = make([]byte, len(self.Vars)) - em.DictVars = make([]byte, len(self.DictVars)) - em.DictVarEndOffsets = make([]int32, len(self.DictVarEndOffsets)) - copy(em.Logtype, self.Logtype) - copy(em.Vars, self.Vars) - copy(em.DictVars, self.DictVars) - copy(em.DictVarEndOffsets, self.DictVarEndOffsets) - return em -} - -func (self *EncodedMessage) DecodeMessage() (ffi.LogMessage, error) { - if nil == self.unsafeRef { - return ffi.LogMessage{}, NilRef - } - msg, ret := self.unsafeRef.DecodeMessage() - return msg, ret -} - -func (self *EncodedMessageUnsafe) DecodeMessage() (ffi.LogMessage, error) { - var msgClass unsafe.Pointer - var msg *C.char - var msgSize C.size_t - C.decode_message(self.cPtr, &msgClass, &msg, &msgSize) - - if nil == msgClass || nil == msg { - return ffi.LogMessage{}, DecodeError - } - - logmsg := ffi.NewLogMessage(unsafe.Pointer(msg), uint64(msgSize), msgClass) - return logmsg, nil -} - -func EncodeMessage(msg string) (EncodedMessage, int) { - em, ret := EncodeMessageUnsafe(msg) - return em.MakeSafe(), ret -} - -func EncodeMessageUnsafe(msg string) (EncodedMessageUnsafe, int) { - var logtypePtr, varsPtr, dictVarsPtr, dictVarEndOffsetsPtr unsafe.Pointer - var logtypeSize, varsSize, dictVarsSize, dictVarEndOffsetsSize uint64 - var em EncodedMessageUnsafe - em.cPtr = C.encode_message(unsafe.Pointer(&[]byte(msg)[0]), C.size_t(len(msg)), - &logtypePtr, unsafe.Pointer(&logtypeSize), - &varsPtr, unsafe.Pointer(&varsSize), - &dictVarsPtr, unsafe.Pointer(&dictVarsSize), - &dictVarEndOffsetsPtr, unsafe.Pointer(&dictVarEndOffsetsSize)) - if nil == em.cPtr { - return em, -1 - } - em.Logtype = unsafe.Slice((*byte)(logtypePtr), logtypeSize) - if nil == em.Logtype { - return em, -2 - } - if 0 != varsSize { - em.Vars = unsafe.Slice((*byte)(varsPtr), varsSize) - if nil == em.Vars { - return em, -3 - } - } - if 0 != dictVarsSize { - em.DictVars = unsafe.Slice((*byte)(dictVarsPtr), dictVarsSize) - if nil == em.DictVars { - return em, -4 - } - } - if 0 != dictVarEndOffsetsSize { - em.DictVarEndOffsets = unsafe.Slice((*int32)(dictVarEndOffsetsPtr), dictVarEndOffsetsSize) - if nil == em.DictVarEndOffsets { - return em, -5 - } - } - runtime.SetFinalizer(&em, - func(em *EncodedMessageUnsafe) { C.delete_encoded_message(em.cPtr) }) - return em, 0 -} diff --git a/message/encoding_test.go b/message/encoding_test.go deleted file mode 100644 index 68953dc..0000000 --- a/message/encoding_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package message - -import ( - "encoding/binary" - _ "fmt" - "math" - "runtime" - "testing" - - "github.com/y-scope/clp-ffi-go/test" -) - -type testLog struct { - name string - msg string - // vars []byte - // dictVars []string - // dictVarEndOffsets []int32 -} - -var testlogs []testLog = []testLog{ - {name: "static", msg: "static text static text static text"}, - {name: "int", msg: "0 1 2 3 0123"}, - {name: "float", msg: "0.0 1.1 2.2 3.3 01234.0123"}, - {name: "dict", msg: "dictVar0 dictVar1 dictVar=dictVar2"}, - {name: "combo", msg: "Static text, dictVar1, 123, 456.7, dictVar2, 987, 654.3"}, -} - -func assertDecodedMessage(t *testing.T, log testLog, err error, msg string) { - t.Helper() - if nil != err { - t.Fatalf("DecodeMessage: %v", err) - } - if log.msg != msg { - t.Fatalf("Test msg does not match LogMessage.Msg:\nwant| %v\ngot| %v", log.msg, msg) - } -} - -func assertEncodedMessage(t *testing.T, log testLog, ret int, logtype []byte, vars []byte) { - t.Helper() - if 0 != ret { - t.Fatalf("EncodeMessage: %v", ret) - } - // TODO: test other fields...? -} - -func testDecodeMessage(t *testing.T, testlog testLog) { - uem, ret := EncodeMessageUnsafe(testlog.msg) - var em EncodedMessage = uem.MakeSafe() - assertEncodedMessage(t, testlog, ret, em.Logtype, em.Vars) - - log, err := em.unsafeRef.DecodeMessage() - runtime.GC() - - // calling ReleaseRef allows uem to be collected despite em and msg being - // still reachable - em.ReleaseRef() - test.AssertFinalizers(t, test.NewFinalizer(&uem)) - runtime.GC() - - assertDecodedMessage(t, testlog, err, string(log.Msg)) - test.AssertFinalizers(t, test.NewFinalizer(&em), test.NewFinalizer(&log)) -} - -func testUnsafeDecodeMessage(t *testing.T, testlog testLog) { - em, ret := EncodeMessageUnsafe(testlog.msg) - assertEncodedMessage(t, testlog, ret, em.Logtype, em.Vars) - - log, err := em.DecodeMessage() - runtime.GC() - assertDecodedMessage(t, testlog, err, string(log.Msg)) - test.AssertFinalizers(t, test.NewFinalizer(&em), test.NewFinalizer(&log)) -} - -func TestSafeEncodeDecodeMessage(t *testing.T) { - for _, testlog := range testlogs { - test := testlog - t.Run(test.name, func(t *testing.T) { testDecodeMessage(t, test) }) - } -} - -func TestUnsafeEncodeDecodeMessage(t *testing.T) { - for _, testlog := range testlogs { - test := testlog - t.Run(test.name, func(t *testing.T) { testUnsafeDecodeMessage(t, test) }) - } -} - -func Float64frombytes(bytes []byte) float64 { - bits := binary.LittleEndian.Uint64(bytes) - float := math.Float64frombits(bits) - return float -} - -func Float64bytes(float float64) []byte { - bits := math.Float64bits(float) - bytes := make([]byte, 8) - binary.LittleEndian.PutUint64(bytes, bits) - return bytes -} diff --git a/message/msgerror.go b/message/msgerror.go deleted file mode 100644 index ef86020..0000000 --- a/message/msgerror.go +++ /dev/null @@ -1,16 +0,0 @@ -package message - -// MsgError defines errors created in the go FFI code, but may need to mirror a -// cpp type in the future. -//go:generate stringer -type=MsgError -type MsgError int - -const ( - _ MsgError = iota - DecodeError - NilRef -) - -func (self MsgError) Error() string { - return self.String() -} diff --git a/message/msgerror_string.go b/message/msgerror_string.go deleted file mode 100644 index ee35ac9..0000000 --- a/message/msgerror_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=MsgError"; DO NOT EDIT. - -package message - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[DecodeError-1] - _ = x[NilRef-2] -} - -const _MsgError_name = "DecodeErrorNilRef" - -var _MsgError_index = [...]uint8{0, 11, 17} - -func (i MsgError) String() string { - i -= 1 - if i < 0 || i >= MsgError(len(_MsgError_index)-1) { - return "MsgError(" + strconv.FormatInt(int64(i+1), 10) + ")" - } - return _MsgError_name[_MsgError_index[i]:_MsgError_index[i+1]] -} diff --git a/search/BUILD.bazel b/search/BUILD.bazel new file mode 100644 index 0000000..0c48f14 --- /dev/null +++ b/search/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "search", + srcs = [ + "wildcard_query.go", + ], + cgo = True, + cdeps = [ + "//:libclp_ffi", + ], + importpath = "github.com/y-scope/clp-ffi-go/search", + visibility = ["//visibility:public"], + deps = [ + "//ffi", + ], +) + +alias( + name = "go_default_library", + actual = ":search", + visibility = ["//visibility:public"], +) diff --git a/search/cgo_amd64.go b/search/cgo_amd64.go new file mode 100644 index 0000000..2c6891a --- /dev/null +++ b/search/cgo_amd64.go @@ -0,0 +1,10 @@ +//go:build !external && amd64 + +package search + +/* +#cgo CPPFLAGS: -I${SRCDIR}/../include/ +#cgo linux LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_linux_amd64.a -lstdc++ +#cgo darwin LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_darwin_amd64.a -lstdc++ +*/ +import "C" diff --git a/search/cgo_arm64.go b/search/cgo_arm64.go new file mode 100644 index 0000000..9c7993a --- /dev/null +++ b/search/cgo_arm64.go @@ -0,0 +1,10 @@ +//go:build !external && arm64 + +package search + +/* +#cgo CPPFLAGS: -I${SRCDIR}/../include/ +#cgo linux LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_linux_arm64.a -lstdc++ +#cgo darwin LDFLAGS: ${SRCDIR}/../lib/libclp_ffi_darwin_arm64.a -lstdc++ +*/ +import "C" diff --git a/message/cgo_external.go b/search/cgo_external.go similarity index 71% rename from message/cgo_external.go rename to search/cgo_external.go index ff49970..ee54523 100644 --- a/message/cgo_external.go +++ b/search/cgo_external.go @@ -1,10 +1,10 @@ //go:build external // When using `external` build manually set linkage with `CGO_LDFLAGS`. -package message +package search /* -#cgo CFLAGS: -I${SRCDIR}/../cpp/src/ +#cgo CPPFLAGS: -I${SRCDIR}/../include/ #cgo external LDFLAGS: */ import "C" diff --git a/search/wildcard_query.go b/search/wildcard_query.go new file mode 100644 index 0000000..a5d7e42 --- /dev/null +++ b/search/wildcard_query.go @@ -0,0 +1,89 @@ +package search + +/* +#include +#include +*/ +import "C" + +import ( + "strings" + "unsafe" + + "github.com/y-scope/clp-ffi-go/ffi" +) + +// A CLP wildcard query containing a query string and a bool for whether the +// query is case sensitive or not. The fields must be accessed through getters +// to ensure that the query string remains clean/safe after creation by +// NewWildcardQuery. +// (Copied from clp/components/core/src/string_utils.hpp) +// Two wildcards are currently supported: '*' to match 0 or more characters, and +// '?' to match any single character. Each can be escaped using a preceding '\'. +// Other characters which are escaped are treated as normal characters. +type WildcardQuery struct { + query string + caseSensitive bool +} + +// Create a new WildcardQuery that is cleaned to contain a safe wildcard query +// string. A wildcard query string must follow 2 rules: +// 1. The wildcard string should not contain consecutive '*'. +// 2. The wildcard string should not contain an escape character without a +// character following it. +// +// NewWildcardQuery will sanitize the provided query and store the safe version. +func NewWildcardQuery(query string, caseSensitive bool) WildcardQuery { + var cptr unsafe.Pointer + cleanQuery := C.wildcard_query_clean( + C.StringView{ + (*C.char)(unsafe.Pointer(unsafe.StringData(query))), + C.size_t(len(query)), + }, + &cptr, + ) + defer C.wildcard_query_delete(cptr) + return WildcardQuery{ + strings.Clone(unsafe.String( + (*byte)((unsafe.Pointer)(cleanQuery.m_data)), + cleanQuery.m_size, + )), + caseSensitive, + } +} + +func (self WildcardQuery) Query() string { return self.query } +func (self WildcardQuery) CaseSensitive() bool { return self.caseSensitive } + +// A MergedWildcardQuery represents the union of multiple wildcard queries +// (multiple WildcardQuery instances each with their own query string and case +// sensitivity). +type MergedWildcardQuery struct { + queries string + endOffsets []int + caseSensitivity []bool +} + +func (self MergedWildcardQuery) Queries() string { return self.queries } +func (self MergedWildcardQuery) EndOffsets() []int { return self.endOffsets } +func (self MergedWildcardQuery) CaseSensitivity() []bool { return self.caseSensitivity } + +// Merge multiple WildcardQuery objects together by concatenating their query +// strings, storing their end/length offsets, and recording their case +// sensitivity. +func MergeWildcardQueries(queries []WildcardQuery) MergedWildcardQuery { + var sb strings.Builder + offsets := make([]int, len(queries)) + caseSensitivity := make([]bool, len(queries)) + for i, q := range queries { + offsets[i], _ = sb.WriteString(q.query) // err always nil + caseSensitivity[i] = queries[i].caseSensitive + } + return MergedWildcardQuery{sb.String(), offsets, caseSensitivity} +} + +// A timestamp interval of [m_lower, m_upper). +type TimestampInterval struct { + Lower ffi.EpochTimeMs + Upper ffi.EpochTimeMs +} diff --git a/test/BUILD.bazel b/test/BUILD.bazel deleted file mode 100644 index a4a5f5b..0000000 --- a/test/BUILD.bazel +++ /dev/null @@ -1,14 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "test", - srcs = ["finalizers.go"], - importpath = "github.com/y-scope/clp-ffi-go/test", - visibility = ["//visibility:public"], -) - -alias( - name = "go_default_library", - actual = ":test", - visibility = ["//visibility:public"], -) diff --git a/test/finalizers.go b/test/finalizers.go deleted file mode 100644 index 9ca02a8..0000000 --- a/test/finalizers.go +++ /dev/null @@ -1,44 +0,0 @@ -package test - -import ( - "fmt" - "runtime" - "testing" - "time" -) - -type Finalizer struct { - ch chan bool - msg string -} - -// We must split NewFinalizers and AssertFinalizers into two functions to -// ensure that the GC call in assertFinalizers will find our pointers -// unreachable. If we combine these functions the runtime will assume the -// pointers in ptrs are still reachable in the caller (even if the caller -// returns immediately after the function call) and will not GC them (so the -// finalizers will not run). -func NewFinalizer[T any](ptr *T) Finalizer { - fin := Finalizer{ - make(chan bool, 1), - fmt.Sprintf("%T", ptr), - } - // must capture fin.ch for SetFinalizer lambda - ch := fin.ch - runtime.SetFinalizer(ptr, func(_ any) { ch <- true }) - return fin -} - -// AssertFinalizers checks that each Finalizer channel has been signalled after -// running a GC cycle. -func AssertFinalizers(t *testing.T, fins ...Finalizer) { - t.Helper() - runtime.GC() - for _, fin := range fins { - select { - case <-fin.ch: - case <-time.After(4 * time.Second): - t.Fatalf("finalizer did not run for: %s", fin.msg) - } - } -} From 9253255296d2b733344d72d7e87d4a7a0013a86b Mon Sep 17 00:00:00 2001 From: davidlion Date: Wed, 10 Apr 2024 16:09:12 -0400 Subject: [PATCH 02/27] Reintroduce __cplusplus ifdefs to headers so the correct names are in the header when used with a c++ compiler. --- cpp/.clang-format | 2 +- cpp/src/ffi_go/defs.h | 110 +++++----- cpp/src/ffi_go/ir/decoder.h | 142 +++++++------ cpp/src/ffi_go/ir/deserializer.h | 280 +++++++++++++------------ cpp/src/ffi_go/ir/encoder.h | 158 +++++++------- cpp/src/ffi_go/search/wildcard_query.h | 106 +++++----- include/ffi_go/defs.h | 110 +++++----- include/ffi_go/ir/decoder.h | 142 +++++++------ include/ffi_go/ir/deserializer.h | 280 +++++++++++++------------ include/ffi_go/ir/encoder.h | 158 +++++++------- include/ffi_go/search/wildcard_query.h | 106 +++++----- 11 files changed, 837 insertions(+), 757 deletions(-) diff --git a/cpp/.clang-format b/cpp/.clang-format index f3fdee1..08627a5 100644 --- a/cpp/.clang-format +++ b/cpp/.clang-format @@ -84,7 +84,7 @@ IndentCaseBlocks: false IndentCaseLabels: true IndentExternBlock: Indent IndentGotoLabels: false -IndentPPDirectives: BeforeHash +IndentPPDirectives: None IndentRequiresClause: false IndentWrappedFunctionNames: false InsertBraces: true diff --git a/cpp/src/ffi_go/defs.h b/cpp/src/ffi_go/defs.h index 563c89a..17f2a4f 100644 --- a/cpp/src/ffi_go/defs.h +++ b/cpp/src/ffi_go/defs.h @@ -4,68 +4,76 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include -// TODO: replace with clp c-compatible header once it exists -typedef int64_t epoch_time_ms_t; + // TODO: replace with clp c-compatible header once it exists + typedef int64_t epoch_time_ms_t; + + /** + * A span of a bool array passed down through Cgo. + */ + typedef struct { + bool* m_data; + size_t m_size; + } BoolSpan; -/** - * A span of a bool array passed down through Cgo. - */ -typedef struct { - bool* m_data; - size_t m_size; -} BoolSpan; + /** + * A span of a byte array passed down through Cgo. + */ + typedef struct { + void* m_data; + size_t m_size; + } ByteSpan; -/** - * A span of a byte array passed down through Cgo. - */ -typedef struct { - void* m_data; - size_t m_size; -} ByteSpan; + /** + * A span of a Go int32 array passed down through Cgo. + */ + typedef struct { + int32_t* m_data; + size_t m_size; + } Int32tSpan; -/** - * A span of a Go int32 array passed down through Cgo. - */ -typedef struct { - int32_t* m_data; - size_t m_size; -} Int32tSpan; + /** + * A span of a Go int64 array passed down through Cgo. + */ + typedef struct { + int64_t* m_data; + size_t m_size; + } Int64tSpan; -/** - * A span of a Go int64 array passed down through Cgo. - */ -typedef struct { - int64_t* m_data; - size_t m_size; -} Int64tSpan; + /** + * A span of a Go int/C.size_t array passed down through Cgo. + */ + typedef struct { + size_t* m_data; + size_t m_size; + } SizetSpan; -/** - * A span of a Go int/C.size_t array passed down through Cgo. - */ -typedef struct { - size_t* m_data; - size_t m_size; -} SizetSpan; + /** + * A view of a Go string passed down through Cgo. + */ + typedef struct { + char const* m_data; + size_t m_size; + } StringView; -/** - * A view of a Go string passed down through Cgo. - */ -typedef struct { - char const* m_data; - size_t m_size; -} StringView; + /** + * A view of a Go ffi.LogEvent passed down through Cgo. + */ + typedef struct { + StringView m_log_message; + epoch_time_ms_t m_timestamp; + } LogEventView; -/** - * A view of a Go ffi.LogEvent passed down through Cgo. - */ -typedef struct { - StringView m_log_message; - epoch_time_ms_t m_timestamp; -} LogEventView; +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-deprecated-headers) diff --git a/cpp/src/ffi_go/ir/decoder.h b/cpp/src/ffi_go/ir/decoder.h index f1efef7..3c59ea1 100644 --- a/cpp/src/ffi_go/ir/decoder.h +++ b/cpp/src/ffi_go/ir/decoder.h @@ -5,81 +5,89 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include -/** - * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. - * @return New ir::Decoder's address - */ -void* ir_decoder_new(); + /** + * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. + * @return New ir::Decoder's address + */ + void* ir_decoder_new(); + + /** + * Clean up the underlying ir::Decoder of a Go ir.Decoder. + * @param[in] ir_encoder Address of a ir::Decoder created and returned by + * ir_decoder_new + */ + void ir_decoder_close(void* decoder); -/** - * Clean up the underlying ir::Decoder of a Go ir.Decoder. - * @param[in] ir_encoder Address of a ir::Decoder created and returned by - * ir_decoder_new - */ -void ir_decoder_close(void* decoder); + /** + * Given the fields of a CLP IR encoded log message with eight byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message + ); -/** - * Given the fields of a CLP IR encoded log message with eight byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_decoder_decode_eight_byte_log_message( - StringView logtype, - Int64tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message -); + /** + * Given the fields of a CLP IR encoded log message with four byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message + ); -/** - * Given the fields of a CLP IR encoded log message with four byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_decoder_decode_four_byte_log_message( - StringView logtype, - Int32tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message -); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/cpp/src/ffi_go/ir/deserializer.h b/cpp/src/ffi_go/ir/deserializer.h index 572c056..7ad65f7 100644 --- a/cpp/src/ffi_go/ir/deserializer.h +++ b/cpp/src/ffi_go/ir/deserializer.h @@ -4,153 +4,161 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-trailing-return-type) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include #include -/** - * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. - * @param[in] ir_deserializer The address of a ir::Deserializer created and - * returned by ir_deserializer_deserialize_preamble - */ -void ir_deserializer_close(void* ir_deserializer); + /** + * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. + * @param[in] ir_deserializer The address of a ir::Deserializer created and + * returned by ir_deserializer_deserialize_preamble + */ + void ir_deserializer_close(void* ir_deserializer); + + /** + * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and + * extract its information. An ir::Deserializer will be allocated to use as the + * backing storage for a Go ir.Deserializer (i.e. subsequent calls to + * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read + * the metadata based on the returned type. All pointer parameters must be + * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[out] ir_pos Position in ir_view read to + * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) + * @param[out] metadata_type Type of metadata in preamble (e.g. json) + * @param[out] metadata_pos Position in ir_view where the metadata begins + * @param[out] metadata_size Size of the metadata (in bytes) + * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer + * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer + * to be filled in by Go using the metadata contents + * @return ffi::ir_stream::IRErrorCode forwarded from either + * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble + */ + int ir_deserializer_deserialize_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr + ); -/** - * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and - * extract its information. An ir::Deserializer will be allocated to use as the - * backing storage for a Go ir.Deserializer (i.e. subsequent calls to - * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read - * the metadata based on the returned type. All pointer parameters must be - * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[out] ir_pos Position in ir_view read to - * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) - * @param[out] metadata_type Type of metadata in preamble (e.g. json) - * @param[out] metadata_pos Position in ir_view where the metadata begins - * @param[out] metadata_size Size of the metadata (in bytes) - * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer - * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer - * to be filled in by Go using the metadata contents - * @return ffi::ir_stream::IRErrorCode forwarded from either - * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble - */ -int ir_deserializer_deserialize_preamble( - ByteSpan ir_view, - size_t* ir_pos, - int8_t* ir_encoding, - int8_t* metadata_type, - size_t* metadata_pos, - uint16_t* metadata_size, - void** ir_deserializer_ptr, - void** timestamp_ptr -); + /** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::decode_next_message + */ + int ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event + ); -/** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::decode_next_message - */ -int ir_deserializer_deserialize_eight_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event -); + /** + * Given a CLP IR buffer with four byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + */ + int ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event + ); -/** - * Given a CLP IR buffer with four byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - */ -int ir_deserializer_deserialize_four_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event -); + /** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event until finding an event that is both within the time interval and + * matches any query. If queries is empty, the first log event within the time + * interval is treated as a match. Returns the components of the found log event + * and the buffer position it ends at. All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the first matching query or + * 0 if queries is empty + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ + int ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query + ); -/** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event until finding an event that is both within the time interval and - * matches any query. If queries is empty, the first log event within the time - * interval is treated as a match. Returns the components of the found log event - * and the buffer position it ends at. All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the first matching query or - * 0 if queries is empty - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ -int ir_deserializer_deserialize_eight_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query -); + /** + * Given a CLP IR buffer with four byte encoding, deserialize the next log event + * until finding an event that is both within the time interval and matches any + * query. If queries is empty, the first log event within the time interval is + * treated as a match. Returns the components of the found log event and the + * buffer position it ends at. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the matching query + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ + int ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query + ); -/** - * Given a CLP IR buffer with four byte encoding, deserialize the next log event - * until finding an event that is both within the time interval and matches any - * query. If queries is empty, the first log event within the time interval is - * treated as a match. Returns the components of the found log event and the - * buffer position it ends at. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the matching query - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ -int ir_deserializer_deserialize_four_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query -); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/cpp/src/ffi_go/ir/encoder.h b/cpp/src/ffi_go/ir/encoder.h index aafa2a4..b8d78e3 100644 --- a/cpp/src/ffi_go/ir/encoder.h +++ b/cpp/src/ffi_go/ir/encoder.h @@ -5,91 +5,99 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include -/** - * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. - * @return New ir::Encoder's address - */ -void* ir_encoder_eight_byte_new(); + /** + * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. + * @return New ir::Encoder's address + */ + void* ir_encoder_eight_byte_new(); + + /** + * @copydoc ir_encoder_eight_byte_new() + */ + void* ir_encoder_four_byte_new(); -/** - * @copydoc ir_encoder_eight_byte_new() - */ -void* ir_encoder_four_byte_new(); + /** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_eight_byte_new + */ + void ir_encoder_eight_byte_close(void* ir_encoder); -/** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_eight_byte_new - */ -void ir_encoder_eight_byte_close(void* ir_encoder); + /** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_four_byte_new + */ + void ir_encoder_four_byte_close(void* ir_encoder); -/** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_four_byte_new - */ -void ir_encoder_four_byte_close(void* ir_encoder); + /** + * Given a log message, encode it into a CLP IR object with eight byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets + ); -/** - * Given a log message, encode it into a CLP IR object with eight byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_encoder_encode_eight_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int64tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets -); + /** + * Given a log message, encode it into a CLP IR object with four byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets + ); -/** - * Given a log message, encode it into a CLP IR object with four byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_encoder_encode_four_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int32tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets -); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/cpp/src/ffi_go/search/wildcard_query.h b/cpp/src/ffi_go/search/wildcard_query.h index f995e29..36c1aac 100644 --- a/cpp/src/ffi_go/search/wildcard_query.h +++ b/cpp/src/ffi_go/search/wildcard_query.h @@ -5,64 +5,72 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include -/** - * A timestamp interval of [m_lower, m_upper). - */ -typedef struct { - epoch_time_ms_t m_lower; - epoch_time_ms_t m_upper; -} TimestampInterval; + /** + * A timestamp interval of [m_lower, m_upper). + */ + typedef struct { + epoch_time_ms_t m_lower; + epoch_time_ms_t m_upper; + } TimestampInterval; + + /** + * A view of a wildcard query passed down from Go. The query string is assumed + * to have been cleaned using the CLP function `clean_up_wildcard_search_string` + * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case + * insensitive). + */ + typedef struct { + StringView m_query; + bool m_case_sensitive; + } WildcardQueryView; -/** - * A view of a wildcard query passed down from Go. The query string is assumed - * to have been cleaned using the CLP function `clean_up_wildcard_search_string` - * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case - * insensitive). - */ -typedef struct { - StringView m_query; - bool m_case_sensitive; -} WildcardQueryView; + /** + * A view of a Go search.MergedWildcardQuery passed down through Cgo. The + * string is a concatenation of all wildcard queries, while m_end_offsets stores + * the size of each query. + */ + typedef struct { + StringView m_queries; + SizetSpan m_end_offsets; + BoolSpan m_case_sensitivity; + } MergedWildcardQueryView; -/** - * A view of a Go search.MergedWildcardQuery passed down through Cgo. The - * string is a concatenation of all wildcard queries, while m_end_offsets stores - * the size of each query. - */ -typedef struct { - StringView m_queries; - SizetSpan m_end_offsets; - BoolSpan m_case_sensitivity; -} MergedWildcardQueryView; + /** + * Delete a std::string holding a wildcard query. + * @param[in] str Address of a std::string created and returned by + * clean_wildcard_query + */ + void wildcard_query_delete(void* str); -/** - * Delete a std::string holding a wildcard query. - * @param[in] str Address of a std::string created and returned by - * clean_wildcard_query - */ -void wildcard_query_delete(void* str); + /** + * Given a query string, clean it to be safe for matching. See + * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ + StringView wildcard_query_clean(StringView query, void** ptr); -/** - * Given a query string, clean it to be safe for matching. See - * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. - * @param[in] query Query string to clean - * @param[in] ptr Address of a new std::string - * @return New string holding cleaned query - */ -StringView wildcard_query_clean(StringView query, void** ptr); + /** + * Given a target string perform CLP wildcard matching using query. See + * `wildcard_match_unsafe` in CLP src/string_utils.hpp. + * @param[in] target String to perform matching on + * @param[in] query Query to use for matching + * @return 1 if query matches target, 0 otherwise + */ + int wildcard_query_match(StringView target, WildcardQueryView query); -/** - * Given a target string perform CLP wildcard matching using query. See - * `wildcard_match_unsafe` in CLP src/string_utils.hpp. - * @param[in] target String to perform matching on - * @param[in] query Query to use for matching - * @return 1 if query matches target, 0 otherwise - */ -int wildcard_query_match(StringView target, WildcardQueryView query); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/include/ffi_go/defs.h b/include/ffi_go/defs.h index 563c89a..17f2a4f 100644 --- a/include/ffi_go/defs.h +++ b/include/ffi_go/defs.h @@ -4,68 +4,76 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include -// TODO: replace with clp c-compatible header once it exists -typedef int64_t epoch_time_ms_t; + // TODO: replace with clp c-compatible header once it exists + typedef int64_t epoch_time_ms_t; + + /** + * A span of a bool array passed down through Cgo. + */ + typedef struct { + bool* m_data; + size_t m_size; + } BoolSpan; -/** - * A span of a bool array passed down through Cgo. - */ -typedef struct { - bool* m_data; - size_t m_size; -} BoolSpan; + /** + * A span of a byte array passed down through Cgo. + */ + typedef struct { + void* m_data; + size_t m_size; + } ByteSpan; -/** - * A span of a byte array passed down through Cgo. - */ -typedef struct { - void* m_data; - size_t m_size; -} ByteSpan; + /** + * A span of a Go int32 array passed down through Cgo. + */ + typedef struct { + int32_t* m_data; + size_t m_size; + } Int32tSpan; -/** - * A span of a Go int32 array passed down through Cgo. - */ -typedef struct { - int32_t* m_data; - size_t m_size; -} Int32tSpan; + /** + * A span of a Go int64 array passed down through Cgo. + */ + typedef struct { + int64_t* m_data; + size_t m_size; + } Int64tSpan; -/** - * A span of a Go int64 array passed down through Cgo. - */ -typedef struct { - int64_t* m_data; - size_t m_size; -} Int64tSpan; + /** + * A span of a Go int/C.size_t array passed down through Cgo. + */ + typedef struct { + size_t* m_data; + size_t m_size; + } SizetSpan; -/** - * A span of a Go int/C.size_t array passed down through Cgo. - */ -typedef struct { - size_t* m_data; - size_t m_size; -} SizetSpan; + /** + * A view of a Go string passed down through Cgo. + */ + typedef struct { + char const* m_data; + size_t m_size; + } StringView; -/** - * A view of a Go string passed down through Cgo. - */ -typedef struct { - char const* m_data; - size_t m_size; -} StringView; + /** + * A view of a Go ffi.LogEvent passed down through Cgo. + */ + typedef struct { + StringView m_log_message; + epoch_time_ms_t m_timestamp; + } LogEventView; -/** - * A view of a Go ffi.LogEvent passed down through Cgo. - */ -typedef struct { - StringView m_log_message; - epoch_time_ms_t m_timestamp; -} LogEventView; +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-deprecated-headers) diff --git a/include/ffi_go/ir/decoder.h b/include/ffi_go/ir/decoder.h index f1efef7..3c59ea1 100644 --- a/include/ffi_go/ir/decoder.h +++ b/include/ffi_go/ir/decoder.h @@ -5,81 +5,89 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include -/** - * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. - * @return New ir::Decoder's address - */ -void* ir_decoder_new(); + /** + * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. + * @return New ir::Decoder's address + */ + void* ir_decoder_new(); + + /** + * Clean up the underlying ir::Decoder of a Go ir.Decoder. + * @param[in] ir_encoder Address of a ir::Decoder created and returned by + * ir_decoder_new + */ + void ir_decoder_close(void* decoder); -/** - * Clean up the underlying ir::Decoder of a Go ir.Decoder. - * @param[in] ir_encoder Address of a ir::Decoder created and returned by - * ir_decoder_new - */ -void ir_decoder_close(void* decoder); + /** + * Given the fields of a CLP IR encoded log message with eight byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message + ); -/** - * Given the fields of a CLP IR encoded log message with eight byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_decoder_decode_eight_byte_log_message( - StringView logtype, - Int64tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message -); + /** + * Given the fields of a CLP IR encoded log message with four byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message + ); -/** - * Given the fields of a CLP IR encoded log message with four byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_decoder_decode_four_byte_log_message( - StringView logtype, - Int32tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message -); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/include/ffi_go/ir/deserializer.h b/include/ffi_go/ir/deserializer.h index 572c056..7ad65f7 100644 --- a/include/ffi_go/ir/deserializer.h +++ b/include/ffi_go/ir/deserializer.h @@ -4,153 +4,161 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-trailing-return-type) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include #include -/** - * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. - * @param[in] ir_deserializer The address of a ir::Deserializer created and - * returned by ir_deserializer_deserialize_preamble - */ -void ir_deserializer_close(void* ir_deserializer); + /** + * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. + * @param[in] ir_deserializer The address of a ir::Deserializer created and + * returned by ir_deserializer_deserialize_preamble + */ + void ir_deserializer_close(void* ir_deserializer); + + /** + * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and + * extract its information. An ir::Deserializer will be allocated to use as the + * backing storage for a Go ir.Deserializer (i.e. subsequent calls to + * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read + * the metadata based on the returned type. All pointer parameters must be + * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[out] ir_pos Position in ir_view read to + * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) + * @param[out] metadata_type Type of metadata in preamble (e.g. json) + * @param[out] metadata_pos Position in ir_view where the metadata begins + * @param[out] metadata_size Size of the metadata (in bytes) + * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer + * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer + * to be filled in by Go using the metadata contents + * @return ffi::ir_stream::IRErrorCode forwarded from either + * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble + */ + int ir_deserializer_deserialize_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr + ); -/** - * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and - * extract its information. An ir::Deserializer will be allocated to use as the - * backing storage for a Go ir.Deserializer (i.e. subsequent calls to - * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read - * the metadata based on the returned type. All pointer parameters must be - * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[out] ir_pos Position in ir_view read to - * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) - * @param[out] metadata_type Type of metadata in preamble (e.g. json) - * @param[out] metadata_pos Position in ir_view where the metadata begins - * @param[out] metadata_size Size of the metadata (in bytes) - * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer - * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer - * to be filled in by Go using the metadata contents - * @return ffi::ir_stream::IRErrorCode forwarded from either - * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble - */ -int ir_deserializer_deserialize_preamble( - ByteSpan ir_view, - size_t* ir_pos, - int8_t* ir_encoding, - int8_t* metadata_type, - size_t* metadata_pos, - uint16_t* metadata_size, - void** ir_deserializer_ptr, - void** timestamp_ptr -); + /** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::decode_next_message + */ + int ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event + ); -/** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::decode_next_message - */ -int ir_deserializer_deserialize_eight_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event -); + /** + * Given a CLP IR buffer with four byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + */ + int ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event + ); -/** - * Given a CLP IR buffer with four byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - */ -int ir_deserializer_deserialize_four_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event -); + /** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event until finding an event that is both within the time interval and + * matches any query. If queries is empty, the first log event within the time + * interval is treated as a match. Returns the components of the found log event + * and the buffer position it ends at. All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the first matching query or + * 0 if queries is empty + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ + int ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query + ); -/** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event until finding an event that is both within the time interval and - * matches any query. If queries is empty, the first log event within the time - * interval is treated as a match. Returns the components of the found log event - * and the buffer position it ends at. All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the first matching query or - * 0 if queries is empty - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ -int ir_deserializer_deserialize_eight_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query -); + /** + * Given a CLP IR buffer with four byte encoding, deserialize the next log event + * until finding an event that is both within the time interval and matches any + * query. If queries is empty, the first log event within the time interval is + * treated as a match. Returns the components of the found log event and the + * buffer position it ends at. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the matching query + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ + int ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query + ); -/** - * Given a CLP IR buffer with four byte encoding, deserialize the next log event - * until finding an event that is both within the time interval and matches any - * query. If queries is empty, the first log event within the time interval is - * treated as a match. Returns the components of the found log event and the - * buffer position it ends at. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the matching query - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ -int ir_deserializer_deserialize_four_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query -); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/include/ffi_go/ir/encoder.h b/include/ffi_go/ir/encoder.h index aafa2a4..b8d78e3 100644 --- a/include/ffi_go/ir/encoder.h +++ b/include/ffi_go/ir/encoder.h @@ -5,91 +5,99 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include #include -/** - * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. - * @return New ir::Encoder's address - */ -void* ir_encoder_eight_byte_new(); + /** + * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. + * @return New ir::Encoder's address + */ + void* ir_encoder_eight_byte_new(); + + /** + * @copydoc ir_encoder_eight_byte_new() + */ + void* ir_encoder_four_byte_new(); -/** - * @copydoc ir_encoder_eight_byte_new() - */ -void* ir_encoder_four_byte_new(); + /** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_eight_byte_new + */ + void ir_encoder_eight_byte_close(void* ir_encoder); -/** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_eight_byte_new - */ -void ir_encoder_eight_byte_close(void* ir_encoder); + /** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_four_byte_new + */ + void ir_encoder_four_byte_close(void* ir_encoder); -/** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_four_byte_new - */ -void ir_encoder_four_byte_close(void* ir_encoder); + /** + * Given a log message, encode it into a CLP IR object with eight byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets + ); -/** - * Given a log message, encode it into a CLP IR object with eight byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_encoder_encode_eight_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int64tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets -); + /** + * Given a log message, encode it into a CLP IR object with four byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ + int ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets + ); -/** - * Given a log message, encode it into a CLP IR object with four byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ -int ir_encoder_encode_four_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int32tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets -); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/include/ffi_go/search/wildcard_query.h b/include/ffi_go/search/wildcard_query.h index f995e29..36c1aac 100644 --- a/include/ffi_go/search/wildcard_query.h +++ b/include/ffi_go/search/wildcard_query.h @@ -5,64 +5,72 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) +#ifdef __cplusplus +extern "C" { +#endif + #include #include -/** - * A timestamp interval of [m_lower, m_upper). - */ -typedef struct { - epoch_time_ms_t m_lower; - epoch_time_ms_t m_upper; -} TimestampInterval; + /** + * A timestamp interval of [m_lower, m_upper). + */ + typedef struct { + epoch_time_ms_t m_lower; + epoch_time_ms_t m_upper; + } TimestampInterval; + + /** + * A view of a wildcard query passed down from Go. The query string is assumed + * to have been cleaned using the CLP function `clean_up_wildcard_search_string` + * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case + * insensitive). + */ + typedef struct { + StringView m_query; + bool m_case_sensitive; + } WildcardQueryView; -/** - * A view of a wildcard query passed down from Go. The query string is assumed - * to have been cleaned using the CLP function `clean_up_wildcard_search_string` - * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case - * insensitive). - */ -typedef struct { - StringView m_query; - bool m_case_sensitive; -} WildcardQueryView; + /** + * A view of a Go search.MergedWildcardQuery passed down through Cgo. The + * string is a concatenation of all wildcard queries, while m_end_offsets stores + * the size of each query. + */ + typedef struct { + StringView m_queries; + SizetSpan m_end_offsets; + BoolSpan m_case_sensitivity; + } MergedWildcardQueryView; -/** - * A view of a Go search.MergedWildcardQuery passed down through Cgo. The - * string is a concatenation of all wildcard queries, while m_end_offsets stores - * the size of each query. - */ -typedef struct { - StringView m_queries; - SizetSpan m_end_offsets; - BoolSpan m_case_sensitivity; -} MergedWildcardQueryView; + /** + * Delete a std::string holding a wildcard query. + * @param[in] str Address of a std::string created and returned by + * clean_wildcard_query + */ + void wildcard_query_delete(void* str); -/** - * Delete a std::string holding a wildcard query. - * @param[in] str Address of a std::string created and returned by - * clean_wildcard_query - */ -void wildcard_query_delete(void* str); + /** + * Given a query string, clean it to be safe for matching. See + * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ + StringView wildcard_query_clean(StringView query, void** ptr); -/** - * Given a query string, clean it to be safe for matching. See - * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. - * @param[in] query Query string to clean - * @param[in] ptr Address of a new std::string - * @return New string holding cleaned query - */ -StringView wildcard_query_clean(StringView query, void** ptr); + /** + * Given a target string perform CLP wildcard matching using query. See + * `wildcard_match_unsafe` in CLP src/string_utils.hpp. + * @param[in] target String to perform matching on + * @param[in] query Query to use for matching + * @return 1 if query matches target, 0 otherwise + */ + int wildcard_query_match(StringView target, WildcardQueryView query); -/** - * Given a target string perform CLP wildcard matching using query. See - * `wildcard_match_unsafe` in CLP src/string_utils.hpp. - * @param[in] target String to perform matching on - * @param[in] query Query to use for matching - * @return 1 if query matches target, 0 otherwise - */ -int wildcard_query_match(StringView target, WildcardQueryView query); +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) From 33647d0485119820e8382f7367dfdc0069e3b7ac Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 12:53:00 -0400 Subject: [PATCH 03/27] Apply suggestions from code review - fix typos - update NamespaceIndentation to None - add [[nodiscard]] - typedef -> using - add const - use fancy auto syntax Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> --- README.rst | 2 +- cpp/.clang-format | 2 +- cpp/src/ffi_go/ir/decoder.cpp | 8 +++--- cpp/src/ffi_go/ir/decoder.h | 2 +- cpp/src/ffi_go/ir/deserializer.cpp | 36 +++++++++++++----------- cpp/src/ffi_go/ir/deserializer.h | 2 +- cpp/src/ffi_go/ir/encoder.cpp | 18 ++++++------ cpp/src/ffi_go/ir/encoder.h | 4 +-- cpp/src/ffi_go/ir/serializer.h | 12 ++++---- cpp/src/ffi_go/search/wildcard_query.cpp | 5 ++-- ffi/ffi.go | 10 +++---- 11 files changed, 53 insertions(+), 48 deletions(-) diff --git a/README.rst b/README.rst index a12cc12..1334ac8 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,7 @@ Testing ------- To run all unit tests run: ``go_test_ir="/path/to/my-ir.clp.zst" go test ./...`` -- Some of the ``ir`` package's tests currently requries an existing CLP IR file +- Some of the ``ir`` package's tests currently require an existing CLP IR file compressed with zstd. This file's path is taken as an environment variable named ``go_test_ir``. It can be an absolute path or a path relative to the ``ir`` directory. diff --git a/cpp/.clang-format b/cpp/.clang-format index 08627a5..1e8df9a 100644 --- a/cpp/.clang-format +++ b/cpp/.clang-format @@ -100,7 +100,7 @@ KeepEmptyLinesAtTheStartOfBlocks: false LambdaBodyIndentation: Signature LineEnding: LF MaxEmptyLinesToKeep: 1 -NamespaceIndentation: Inner +NamespaceIndentation: None PPIndentWidth: -1 PackConstructorInitializers: CurrentLine PenaltyBreakAssignment: 50 diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index f5fd268..49c7c14 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -21,7 +21,7 @@ namespace { * Generic helper for ir_decoder_decode_*_log_message */ template - auto decode_log_message( + [[nodiscard]] auto decode_log_message( StringView logtype, encoded_var_view_t vars, StringView dict_vars, @@ -29,10 +29,10 @@ namespace { void* ir_decoder, StringView* log_msg_view ) -> int { - typedef typename std::conditional< + using encoded_var_t = std::conditional< std::is_same_v, ffi::eight_byte_encoded_variable_t, - ffi::four_byte_encoded_variable_t>::type encoded_var_t; + ffi::four_byte_encoded_variable_t>::type; Decoder* decoder{static_cast(ir_decoder)}; ffi_go::LogMessage& log_msg = decoder->m_log_message; log_msg.reserve(logtype.m_size + dict_vars.m_size); @@ -47,7 +47,7 @@ namespace { dict_var_end_offsets.m_data, dict_var_end_offsets.m_size ); - } catch (ffi::EncodingException& e) { + } catch (ffi::EncodingException const& e) { err = IRErrorCode_Decode_Error; } diff --git a/cpp/src/ffi_go/ir/decoder.h b/cpp/src/ffi_go/ir/decoder.h index 3c59ea1..52a58d3 100644 --- a/cpp/src/ffi_go/ir/decoder.h +++ b/cpp/src/ffi_go/ir/decoder.h @@ -38,7 +38,7 @@ extern "C" { * @param[in] vars Array of encoded variables * @param[in] dict_vars String containing all dictionary variables concatenated * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log * message diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index b5da8b8..d9e4610 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -30,7 +30,7 @@ namespace { * Generic helper for ir_deserializer_deserialize_*_log_event */ template - auto deserialize_log_event( + [[nodiscard]] auto deserialize_log_event( ByteSpan ir_view, void* ir_deserializer, size_t* ir_pos, @@ -41,7 +41,7 @@ namespace { * Generic helper for ir_deserializer_deserialize_*_wildcard_match */ template - auto deserialize_wildcard_match( + [[nodiscard]] auto deserialize_wildcard_match( ByteSpan ir_view, void* ir_deserializer, TimestampInterval time_interval, @@ -121,8 +121,9 @@ namespace { std::vector> queries(merged_query.m_end_offsets.m_size); size_t pos{0}; for (size_t i{0}; i < merged_query.m_end_offsets.m_size; i++) { - queries[i].first = query_view.substr(pos, end_offsets[i]); - queries[i].second = case_sensitivity[i]; + auto& [query_str_view, is_case_sensitive]{queries[i]}; + query_str_view = query_view.substr(pos, end_offsets[i]); + is_case_sensitive = case_sensitivity[i]; pos += end_offsets[i]; } @@ -178,19 +179,22 @@ namespace { if (time_interval.m_lower > deserializer->m_timestamp) { continue; } - std::pair const match{query_fn(deserializer->m_log_event.m_log_message)}; - if (match.first) { - size_t pos{0}; - if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { - return static_cast(IRErrorCode_Decode_Error); - } - *ir_pos = pos; - log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); - log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); - log_event->m_timestamp = deserializer->m_timestamp; - *matching_query = match.second; - return static_cast(IRErrorCode_Success); + auto const [has_matching_query, matching_query_idx]{ + query_fn(deserializer->m_log_event.m_log_message) + }; + if (false == has_matching_query) { + continue; + } + size_t pos{0}; + if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { + return static_cast(IRErrorCode_Decode_Error); } + *ir_pos = pos; + log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); + log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); + log_event->m_timestamp = deserializer->m_timestamp; + *matching_query = matching_query_idx; + return static_cast(IRErrorCode_Success); } } } // namespace diff --git a/cpp/src/ffi_go/ir/deserializer.h b/cpp/src/ffi_go/ir/deserializer.h index 7ad65f7..967d4da 100644 --- a/cpp/src/ffi_go/ir/deserializer.h +++ b/cpp/src/ffi_go/ir/deserializer.h @@ -22,7 +22,7 @@ extern "C" { void ir_deserializer_close(void* ir_deserializer); /** - * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and + * Given a CLP IR buffer (any encoding), attempt to deserialize a preamble and * extract its information. An ir::Deserializer will be allocated to use as the * backing storage for a Go ir.Deserializer (i.e. subsequent calls to * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index 54ae633..6be86ae 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -30,12 +30,12 @@ namespace { StringView* dict_vars, Int32tSpan* dict_var_end_offsets ) -> int { - typedef typename std::conditional< + using encoded_var_t std::conditional< std::is_same_v, ffi::eight_byte_encoded_variable_t, - ffi::four_byte_encoded_variable_t>::type encoded_var_t; + ffi::four_byte_encoded_variable_t>::type; Encoder* encoder{static_cast*>(ir_encoder)}; - LogMessage& ir_log_msg = encoder->m_log_message; + auto& ir_log_msg{encoder->m_log_message}; ir_log_msg.reserve(log_message.m_size); std::string_view const log_msg_view{log_message.m_data, log_message.m_size}; @@ -53,14 +53,14 @@ namespace { // dict_var_offsets contains begin_pos followed by end_pos of each // dictionary variable in the message - int32_t prev_end_off = 0; + int32_t prev_end_off{0}; for (size_t i = 0; i < dict_var_offsets.size(); i += 2) { - int32_t const begin_pos = dict_var_offsets[i]; - int32_t const end_pos = dict_var_offsets[i + 1]; + int32_t const begin_pos{dict_var_offsets[i]}; + int32_t const end_pos{dict_var_offsets[i + 1]}; ir_log_msg.m_dict_vars.insert( - ir_log_msg.m_dict_vars.begin() + prev_end_off, - log_msg_view.begin() + begin_pos, - log_msg_view.begin() + end_pos + ir_log_msg.m_dict_vars.cbegin() + prev_end_off, + log_msg_view.cbegin() + begin_pos, + log_msg_view.cbegin() + end_pos ); prev_end_off = prev_end_off + (end_pos - begin_pos); ir_log_msg.m_dict_var_end_offsets.push_back(prev_end_off); diff --git a/cpp/src/ffi_go/ir/encoder.h b/cpp/src/ffi_go/ir/encoder.h index b8d78e3..c011c11 100644 --- a/cpp/src/ffi_go/ir/encoder.h +++ b/cpp/src/ffi_go/ir/encoder.h @@ -52,7 +52,7 @@ extern "C" { * @param[out] vars Array of encoded variables * @param[out] dict_vars String containing all dictionary variables concatenated * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message * returns false @@ -80,7 +80,7 @@ extern "C" { * @param[out] vars Array of encoded variables * @param[out] dict_vars String containing all dictionary variables concatenated * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message * returns false diff --git a/cpp/src/ffi_go/ir/serializer.h b/cpp/src/ffi_go/ir/serializer.h index 0e200a8..576cbe1 100644 --- a/cpp/src/ffi_go/ir/serializer.h +++ b/cpp/src/ffi_go/ir/serializer.h @@ -17,7 +17,7 @@ void ir_serializer_close(void* ir_serializer); /** - * Given the fields of a CLP IR premable, serialize them into an IR byte stream + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream * with eight byte encoding. An ir::Serializer will be allocated to use as the * backing storage for a Go ir.Serializer (i.e. subsequent calls to * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null @@ -29,7 +29,7 @@ void ir_serializer_close(void* ir_serializer); * @param[in] time_zone_id TZID timezone of the timestamps in the IR * @param[out] ir_serializer_ptr Address of a new ir::Serializer * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwared from + * @return ffi::ir_stream::IRErrorCode forwarded from * ffi::ir_stream::eight_byte_encoding::encode_preamble */ int ir_serializer_serialize_eight_byte_preamble( @@ -41,7 +41,7 @@ int ir_serializer_serialize_eight_byte_preamble( ); /** - * Given the fields of a CLP IR premable, serialize them into an IR byte stream + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream * with four byte encoding. An ir::Serializer will be allocated to use as the * backing storage for a Go ir.Serializer (i.e. subsequent calls to * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null @@ -53,7 +53,7 @@ int ir_serializer_serialize_eight_byte_preamble( * @param[in] time_zone_id TZID timezone of the timestamps in the IR * @param[out] ir_serializer_ptr Address of a new ir::Serializer * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwared from + * @return ffi::ir_stream::IRErrorCode forwarded from * ffi::ir_stream::four_byte_encoding::encode_preamble */ int ir_serializer_serialize_four_byte_preamble( @@ -74,7 +74,7 @@ int ir_serializer_serialize_four_byte_preamble( * @param[in] timestamp Timestamp of the log event to serialize * @param[in] ir_serializer ir::Serializer object to be used as storage * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwared from + * @return ffi::ir_stream::IRErrorCode forwarded from * ffi::ir_stream::eight_byte_encoding::encode_message */ int ir_serializer_serialize_eight_byte_log_event( @@ -94,7 +94,7 @@ int ir_serializer_serialize_eight_byte_log_event( * IR stream * @param[in] ir_serializer ir::Serializer object to be used as storage * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwared from + * @return ffi::ir_stream::IRErrorCode forwarded from * ffi::ir_stream::four_byte_encoding::encode_message */ int ir_serializer_serialize_four_byte_log_event( diff --git a/cpp/src/ffi_go/search/wildcard_query.cpp b/cpp/src/ffi_go/search/wildcard_query.cpp index f3f4380..9ea2360 100644 --- a/cpp/src/ffi_go/search/wildcard_query.cpp +++ b/cpp/src/ffi_go/search/wildcard_query.cpp @@ -14,8 +14,9 @@ extern "C" auto wildcard_query_delete(void* str) -> void { } extern "C" auto wildcard_query_clean(StringView query, void** ptr) -> StringView { - auto* clean = new std::string{ - clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size})}; + auto* clean{new std::string{ + clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size}) + }}; *ptr = clean; return {clean->data(), clean->size()}; } diff --git a/ffi/ffi.go b/ffi/ffi.go index 1d6c738..b0d3ef7 100644 --- a/ffi/ffi.go +++ b/ffi/ffi.go @@ -10,7 +10,7 @@ type EpochTimeMs int64 // A LogMessageView is a LogMessage that is backed by C++ allocated memory // rather than the Go heap. A LogMessageView, x, is valid when returned and will // remain valid until a new LogMessageView is returned by the same object (e.g. -// an ir.Deserializer) that retuend x. +// an ir.Deserializer) that returns x. type ( LogMessageView = string LogMessage = string @@ -23,10 +23,10 @@ type LogEvent struct { Timestamp EpochTimeMs } -// The underlying memory of LogEventView is C-allocated and owned by the object -// (e.g. reader, desializer, etc) that returned it. Using an existing -// LogEventView after a new view has been returned by the same producing object -// is undefined (different producing objects own their own memory for views). +// LogEventView memory is allocated and owned by the C++ object (e.g., reader, +// deserializer) that returns it. Reusing a LogEventView after the same object +// has issued a new view leads to undefined behavior, as different objects +// manage their own memory independently. type LogEventView struct { LogMessageView Timestamp EpochTimeMs From 2fd3f1545c3a674335f3114621d131ac9d1ca1b6 Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 12:55:53 -0400 Subject: [PATCH 04/27] Apply suggestions from code review - drop static cast Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> --- cpp/src/ffi_go/search/wildcard_query.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cpp/src/ffi_go/search/wildcard_query.cpp b/cpp/src/ffi_go/search/wildcard_query.cpp index 9ea2360..b21faa1 100644 --- a/cpp/src/ffi_go/search/wildcard_query.cpp +++ b/cpp/src/ffi_go/search/wildcard_query.cpp @@ -23,9 +23,9 @@ extern "C" auto wildcard_query_clean(StringView query, void** ptr) -> StringView extern "C" auto wildcard_query_match(StringView target, WildcardQueryView query) -> int { return static_cast(wildcard_match_unsafe( - std::string_view{target.m_data, target.m_size}, - std::string_view{query.m_query.m_data, query.m_query.m_size}, - static_cast(query.m_case_sensitive) + {target.m_data, target.m_size}, + {query.m_query.m_data, query.m_query.m_size}, + query.m_case_sensitive )); } } // namespace ffi_go::search From c4abd243b5562e897d1476945c2ee5b61178f299 Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 13:02:17 -0400 Subject: [PATCH 05/27] Improve writing --- README.rst | 14 +++++++------- ffi/ffi.go | 5 ++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 1334ac8..1db71d0 100644 --- a/README.rst +++ b/README.rst @@ -74,6 +74,13 @@ arguments or modifications. __ https://github.com/bazelbuild/rules_go/blob/master/docs/go/core/rules.md#go_library-deps +Why not build with cgo? +''''''''''''''''''''''' +The primary reason we choose to build with CMake rather than directly with cgo, +is to ease code maintenance by maximizing the reuse of CLP's code with no +modifications. If a platform you use is not supported by the pre-built +libraries, please open an issue and we can integrate it into our build process. + Testing ------- To run all unit tests run: ``go_test_ir="/path/to/my-ir.clp.zst" go test ./...`` @@ -83,13 +90,6 @@ To run all unit tests run: ``go_test_ir="/path/to/my-ir.clp.zst" go test ./...`` named ``go_test_ir``. It can be an absolute path or a path relative to the ``ir`` directory. -Why not build with cgo? -''''''''''''''''''''''' -The primary reason we choose to build with CMake rather than directly with cgo, -is to ease code maintenance by maximizing the reuse of CLP's code with no -modifications. If a platform you use is not supported by the pre-built -libraries, please open an issue and we can integrate it into our build process. - Using an external C++ library ----------------------------- Use the ``external`` build tag to link with different CLP FFI library instead diff --git a/ffi/ffi.go b/ffi/ffi.go index b0d3ef7..7d19294 100644 --- a/ffi/ffi.go +++ b/ffi/ffi.go @@ -1,6 +1,5 @@ -// The ffi package contains the general types representing log events created -// and used by logging functions or libraries. In other words log events with no -// sort of CLP encoding or serializing. +// The ffi package defines types for log events used in logging functions and +// libraries, without CLP encoding or serialization. package ffi // Mirrors cpp type epoch_time_ms_t From 50bec0875405330b1338ec4ff27c8eae5c2b5eb3 Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 13:03:37 -0400 Subject: [PATCH 06/27] Fix extern C header ifdef to not wrap include statements. --- cpp/src/ffi_go/ir/decoder.h | 8 ++++---- cpp/src/ffi_go/ir/deserializer.h | 8 ++++---- cpp/src/ffi_go/ir/encoder.h | 8 ++++---- cpp/src/ffi_go/ir/serializer.h | 8 ++++++++ cpp/src/ffi_go/search/wildcard_query.h | 8 ++++---- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/cpp/src/ffi_go/ir/decoder.h b/cpp/src/ffi_go/ir/decoder.h index 52a58d3..32e5b51 100644 --- a/cpp/src/ffi_go/ir/decoder.h +++ b/cpp/src/ffi_go/ir/decoder.h @@ -5,15 +5,15 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. * @return New ir::Decoder's address diff --git a/cpp/src/ffi_go/ir/deserializer.h b/cpp/src/ffi_go/ir/deserializer.h index 967d4da..3298544 100644 --- a/cpp/src/ffi_go/ir/deserializer.h +++ b/cpp/src/ffi_go/ir/deserializer.h @@ -4,16 +4,16 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-trailing-return-type) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. * @param[in] ir_deserializer The address of a ir::Deserializer created and diff --git a/cpp/src/ffi_go/ir/encoder.h b/cpp/src/ffi_go/ir/encoder.h index c011c11..1737ada 100644 --- a/cpp/src/ffi_go/ir/encoder.h +++ b/cpp/src/ffi_go/ir/encoder.h @@ -5,15 +5,15 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. * @return New ir::Encoder's address diff --git a/cpp/src/ffi_go/ir/serializer.h b/cpp/src/ffi_go/ir/serializer.h index 576cbe1..5e8aa46 100644 --- a/cpp/src/ffi_go/ir/serializer.h +++ b/cpp/src/ffi_go/ir/serializer.h @@ -9,6 +9,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Clean up the underlying ir::Serializer of a Go ir.Serializer. * @param[in] ir_serializer Address of a ir::Serializer created and returned by @@ -104,6 +108,10 @@ int ir_serializer_serialize_four_byte_log_event( ByteSpan* ir_view ); +#ifdef __cplusplus +} +#endif + // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) #endif // FFI_GO_IR_SERIALIZER_H diff --git a/cpp/src/ffi_go/search/wildcard_query.h b/cpp/src/ffi_go/search/wildcard_query.h index 36c1aac..3aea7bf 100644 --- a/cpp/src/ffi_go/search/wildcard_query.h +++ b/cpp/src/ffi_go/search/wildcard_query.h @@ -5,14 +5,14 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * A timestamp interval of [m_lower, m_upper). */ From 98176cb7c52dd460fb3548ac2c96ff255784756c Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 13:22:26 -0400 Subject: [PATCH 07/27] Add macos back in workflow --- .github/workflows/build.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70848ac..19625c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,14 @@ on: push: workflow_call: +# TODO: add separate jobs for: +# 1. building, linting, testing c++ +# 2. building and testing go using a fresh built c++ library jobs: prebuilt-test: strategy: matrix: - # os: [macos-latest, ubuntu-latest] - os: [ubuntu-latest] + os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -26,8 +28,7 @@ jobs: build-lint-test: strategy: matrix: - # os: [macos-latest, ubuntu-latest] - os: [ubuntu-latest] + os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 From 439f9482a859c97e04643768fa253d66275c337e Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 13:25:02 -0400 Subject: [PATCH 08/27] LogTypes.hpp -> types.hpp --- cpp/CMakeLists.txt | 4 ++-- cpp/src/ffi_go/ir/decoder.cpp | 4 ++-- cpp/src/ffi_go/ir/deserializer.cpp | 4 ++-- cpp/src/ffi_go/ir/encoder.cpp | 4 ++-- cpp/src/ffi_go/ir/serializer.cpp | 4 ++-- cpp/src/ffi_go/ir/{LogTypes.hpp => types.hpp} | 2 +- cpp/src/ffi_go/{LogTypes.hpp => types.hpp} | 0 7 files changed, 11 insertions(+), 11 deletions(-) rename cpp/src/ffi_go/ir/{LogTypes.hpp => types.hpp} (98%) rename cpp/src/ffi_go/{LogTypes.hpp => types.hpp} (100%) diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 7c45667..9c3c56e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -110,11 +110,11 @@ target_sources(${LIB_NAME} PUBLIC ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/decoding_methods.hpp ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/decoding_methods.inc ${CLP_SRC_DIR}/components/core/src/ffi/ir_stream/protocol_constants.hpp - src/ffi_go/LogTypes.hpp + src/ffi_go/types.hpp src/ffi_go/ir/decoder.cpp src/ffi_go/ir/deserializer.cpp src/ffi_go/ir/encoder.cpp - src/ffi_go/ir/LogTypes.hpp + src/ffi_go/ir/types.hpp src/ffi_go/ir/serializer.cpp src/ffi_go/search/wildcard_query.cpp ) diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index 49c7c14..7bf5fa6 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -10,8 +10,8 @@ #include #include -#include -#include +#include +#include namespace ffi_go::ir { using namespace ffi::ir_stream; diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index d9e4610..734ae99 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -17,8 +17,8 @@ #include #include -#include -#include +#include +#include #include namespace ffi_go::ir { diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index 6be86ae..91975bc 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -11,8 +11,8 @@ #include #include -#include -#include +#include +#include namespace ffi_go::ir { using namespace ffi::ir_stream; diff --git a/cpp/src/ffi_go/ir/serializer.cpp b/cpp/src/ffi_go/ir/serializer.cpp index bf0ffaf..27506a5 100644 --- a/cpp/src/ffi_go/ir/serializer.cpp +++ b/cpp/src/ffi_go/ir/serializer.cpp @@ -11,8 +11,8 @@ #include #include -#include -#include +#include +#include namespace ffi_go::ir { using namespace ffi; diff --git a/cpp/src/ffi_go/ir/LogTypes.hpp b/cpp/src/ffi_go/ir/types.hpp similarity index 98% rename from cpp/src/ffi_go/ir/LogTypes.hpp rename to cpp/src/ffi_go/ir/types.hpp index 471ec7a..366f519 100644 --- a/cpp/src/ffi_go/ir/LogTypes.hpp +++ b/cpp/src/ffi_go/ir/types.hpp @@ -7,7 +7,7 @@ #include #include -#include +#include namespace ffi_go::ir { diff --git a/cpp/src/ffi_go/LogTypes.hpp b/cpp/src/ffi_go/types.hpp similarity index 100% rename from cpp/src/ffi_go/LogTypes.hpp rename to cpp/src/ffi_go/types.hpp From a4391331811839b9e480ad079bf3b7dd9e31034e Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 13:26:07 -0400 Subject: [PATCH 09/27] Update cpp/src/ffi_go/ir/decoder.cpp Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> --- cpp/src/ffi_go/ir/decoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index 7bf5fa6..5251e7f 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -34,7 +34,7 @@ namespace { ffi::eight_byte_encoded_variable_t, ffi::four_byte_encoded_variable_t>::type; Decoder* decoder{static_cast(ir_decoder)}; - ffi_go::LogMessage& log_msg = decoder->m_log_message; + auto& log_msg{decoder->m_log_message}; log_msg.reserve(logtype.m_size + dict_vars.m_size); IRErrorCode err{IRErrorCode_Success}; From 67003c27d363200b103a427cc0fffdbb350dd37d Mon Sep 17 00:00:00 2001 From: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:08:20 -0400 Subject: [PATCH 10/27] Update cpp/src/ffi_go/ir/encoder.cpp --- cpp/src/ffi_go/ir/encoder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index 91975bc..636d277 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -30,7 +30,7 @@ namespace { StringView* dict_vars, Int32tSpan* dict_var_end_offsets ) -> int { - using encoded_var_t std::conditional< + using encoded_var_t = std::conditional< std::is_same_v, ffi::eight_byte_encoded_variable_t, ffi::four_byte_encoded_variable_t>::type; From 2d570a8faaea4bc07da413086c361639806f350c Mon Sep 17 00:00:00 2001 From: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:23:00 -0400 Subject: [PATCH 11/27] Adding `typename` for dependent names for disambiguration purpose. --- cpp/src/ffi_go/ir/decoder.cpp | 2 +- cpp/src/ffi_go/ir/encoder.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index 5251e7f..c891e59 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -29,7 +29,7 @@ namespace { void* ir_decoder, StringView* log_msg_view ) -> int { - using encoded_var_t = std::conditional< + using encoded_var_t = typename std::conditional< std::is_same_v, ffi::eight_byte_encoded_variable_t, ffi::four_byte_encoded_variable_t>::type; diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index 636d277..fc509c9 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -30,7 +30,7 @@ namespace { StringView* dict_vars, Int32tSpan* dict_var_end_offsets ) -> int { - using encoded_var_t = std::conditional< + using encoded_var_t = typename std::conditional< std::is_same_v, ffi::eight_byte_encoded_variable_t, ffi::four_byte_encoded_variable_t>::type; From edeab0decfce9479f8c7eed0f5d2139260fe28c5 Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 18:52:26 -0400 Subject: [PATCH 12/27] Refactor c++ and add missing null checks. --- cpp/src/ffi_go/ir/decoder.cpp | 75 +++--- cpp/src/ffi_go/ir/deserializer.cpp | 283 ++++++++++++----------- cpp/src/ffi_go/ir/deserializer.h | 4 +- cpp/src/ffi_go/ir/encoder.cpp | 115 ++++----- cpp/src/ffi_go/ir/serializer.cpp | 185 +++++++++------ cpp/src/ffi_go/ir/serializer.h | 180 +++++++------- cpp/src/ffi_go/search/wildcard_query.cpp | 15 +- cpp/src/ffi_go/search/wildcard_query.h | 18 +- include/ffi_go/ir/decoder.h | 10 +- include/ffi_go/ir/deserializer.h | 14 +- include/ffi_go/ir/encoder.h | 12 +- include/ffi_go/ir/serializer.h | 188 ++++++++------- include/ffi_go/search/wildcard_query.h | 26 +-- ir/deserializer.go | 2 +- ir/serializer.go | 4 +- lib/libclp_ffi_linux_amd64.a | Bin 326866 -> 328798 bytes search/wildcard_query.go | 2 +- 17 files changed, 602 insertions(+), 531 deletions(-) diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index c891e59..fd2a281 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -17,44 +17,47 @@ namespace ffi_go::ir { using namespace ffi::ir_stream; namespace { - /** - * Generic helper for ir_decoder_decode_*_log_message - */ - template - [[nodiscard]] auto decode_log_message( - StringView logtype, - encoded_var_view_t vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_msg_view - ) -> int { - using encoded_var_t = typename std::conditional< - std::is_same_v, - ffi::eight_byte_encoded_variable_t, - ffi::four_byte_encoded_variable_t>::type; - Decoder* decoder{static_cast(ir_decoder)}; - auto& log_msg{decoder->m_log_message}; - log_msg.reserve(logtype.m_size + dict_vars.m_size); - - IRErrorCode err{IRErrorCode_Success}; - try { - log_msg = ffi::decode_message( - std::string_view(logtype.m_data, logtype.m_size), - vars.m_data, - vars.m_size, - std::string_view(dict_vars.m_data, dict_vars.m_size), - dict_var_end_offsets.m_data, - dict_var_end_offsets.m_size - ); - } catch (ffi::EncodingException const& e) { - err = IRErrorCode_Decode_Error; - } +/** + * Generic helper for ir_decoder_decode_*_log_message + */ +template +[[nodiscard]] auto decode_log_message( + StringView logtype, + encoded_var_view_t vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_msg_view +) -> int { + using encoded_var_t = typename std::conditional< + std::is_same_v, + ffi::eight_byte_encoded_variable_t, + ffi::four_byte_encoded_variable_t>::type; + if (nullptr == ir_decoder || nullptr == log_msg_view) { + return static_cast(IRErrorCode_Corrupted_IR); + } + Decoder* decoder{static_cast(ir_decoder)}; + auto& log_msg{decoder->m_log_message}; + log_msg.reserve(logtype.m_size + dict_vars.m_size); - log_msg_view->m_data = log_msg.data(); - log_msg_view->m_size = log_msg.size(); - return static_cast(err); + IRErrorCode err{IRErrorCode_Success}; + try { + log_msg = ffi::decode_message( + std::string_view(logtype.m_data, logtype.m_size), + vars.m_data, + vars.m_size, + std::string_view(dict_vars.m_data, dict_vars.m_size), + dict_var_end_offsets.m_data, + dict_var_end_offsets.m_size + ); + } catch (ffi::EncodingException const& e) { + err = IRErrorCode_Decode_Error; } + + log_msg_view->m_data = log_msg.data(); + log_msg_view->m_size = log_msg.size(); + return static_cast(err); +} } // namespace extern "C" auto ir_decoder_new() -> void* { diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index 734ae99..b24cd86 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -18,50 +18,139 @@ #include #include -#include #include +#include namespace ffi_go::ir { using namespace ffi; using namespace ffi::ir_stream; namespace { - /** - * Generic helper for ir_deserializer_deserialize_*_log_event - */ - template - [[nodiscard]] auto deserialize_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event - ) -> int; +/** + * Generic helper for ir_deserializer_deserialize_*_log_event + */ +template +[[nodiscard]] auto deserialize_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +) -> int; + +/** + * Generic helper for ir_deserializer_deserialize_*_wildcard_match + */ +template +[[nodiscard]] auto deserialize_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + WildcardQueryView queries, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +) -> int; + +template +auto deserialize_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +) -> int { + if (nullptr == ir_deserializer || nullptr == ir_pos || nullptr == log_event) { + return static_cast(IRErrorCode_Corrupted_IR); + } + BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; + Deserializer* deserializer{static_cast(ir_deserializer)}; + + IRErrorCode err{}; + epoch_time_ms_t timestamp{}; + if constexpr (std::is_same_v) { + err = eight_byte_encoding::decode_next_message( + ir_buf, + deserializer->m_log_event.m_log_message, + timestamp + ); + } else if constexpr (std::is_same_v) { + epoch_time_ms_t timestamp_delta{}; + err = four_byte_encoding::decode_next_message( + ir_buf, + deserializer->m_log_event.m_log_message, + timestamp_delta + ); + timestamp = deserializer->m_timestamp + timestamp_delta; + } else { + static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); + } + if (IRErrorCode_Success != err) { + return static_cast(err); + } + deserializer->m_timestamp = timestamp; + + size_t pos{0}; + if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { + return static_cast(IRErrorCode_Decode_Error); + } + *ir_pos = pos; + log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); + log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); + log_event->m_timestamp = deserializer->m_timestamp; + return static_cast(IRErrorCode_Success); +} - /** - * Generic helper for ir_deserializer_deserialize_*_wildcard_match - */ - template - [[nodiscard]] auto deserialize_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - WildcardQueryView queries, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query - ) -> int; +template +auto deserialize_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +) -> int { + if (nullptr == ir_deserializer || nullptr == ir_pos || nullptr == log_event + || nullptr == matching_query) + { + return static_cast(IRErrorCode_Corrupted_IR); + } + BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; + Deserializer* deserializer{static_cast(ir_deserializer)}; + std::string_view const query_view{merged_query.m_queries.m_data, merged_query.m_queries.m_size}; + std::span const end_offsets{ + merged_query.m_end_offsets.m_data, + merged_query.m_end_offsets.m_size}; + std::span const case_sensitivity{ + merged_query.m_case_sensitivity.m_data, + merged_query.m_case_sensitivity.m_size}; + + std::vector> queries(merged_query.m_end_offsets.m_size); + size_t pos{0}; + for (size_t i{0}; i < merged_query.m_end_offsets.m_size; i++) { + auto& [query_str_view, is_case_sensitive]{queries[i]}; + query_str_view = query_view.substr(pos, end_offsets[i]); + is_case_sensitive = case_sensitivity[i]; + pos += end_offsets[i]; + } - template - auto deserialize_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event - ) -> int { - BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; - Deserializer* deserializer{static_cast(ir_deserializer)}; + std::function(ffi_go::LogMessage const&)> query_fn; + if (false == queries.empty()) { + query_fn = [&](ffi_go::LogMessage const& log_message) -> std::pair { + auto const found_query = std::find_if( + queries.cbegin(), + queries.cend(), + [&](std::pair const& query) -> bool { + return wildcard_match_unsafe(log_message, query.first, query.second); + } + ); + return {queries.cend() != found_query, found_query - queries.cbegin()}; + }; + } else { + query_fn = [](ffi_go::LogMessage const&) -> std::pair { return {true, 0}; }; + } - IRErrorCode err{}; + IRErrorCode err{}; + while (true) { epoch_time_ms_t timestamp{}; if constexpr (std::is_same_v) { err = eight_byte_encoding::decode_next_message( @@ -85,6 +174,20 @@ namespace { } deserializer->m_timestamp = timestamp; + if (time_interval.m_upper <= deserializer->m_timestamp) { + // TODO this is an extremely fragile hack until the CLP ffi ir + // code is refactored and IRErrorCode includes things beyond + // decoding. + return static_cast(IRErrorCode_Incomplete_IR + 1); + } + if (time_interval.m_lower > deserializer->m_timestamp) { + continue; + } + auto const [has_matching_query, matching_query_idx]{ + query_fn(deserializer->m_log_event.m_log_message)}; + if (false == has_matching_query) { + continue; + } size_t pos{0}; if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { return static_cast(IRErrorCode_Decode_Error); @@ -93,110 +196,10 @@ namespace { log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); log_event->m_timestamp = deserializer->m_timestamp; + *matching_query = matching_query_idx; return static_cast(IRErrorCode_Success); } - - template - auto deserialize_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query - ) -> int { - BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; - Deserializer* deserializer{static_cast(ir_deserializer)}; - std::string_view const query_view{ - merged_query.m_queries.m_data, - merged_query.m_queries.m_size}; - std::span const end_offsets{ - merged_query.m_end_offsets.m_data, - merged_query.m_end_offsets.m_size}; - std::span const case_sensitivity{ - merged_query.m_case_sensitivity.m_data, - merged_query.m_case_sensitivity.m_size}; - - std::vector> queries(merged_query.m_end_offsets.m_size); - size_t pos{0}; - for (size_t i{0}; i < merged_query.m_end_offsets.m_size; i++) { - auto& [query_str_view, is_case_sensitive]{queries[i]}; - query_str_view = query_view.substr(pos, end_offsets[i]); - is_case_sensitive = case_sensitivity[i]; - pos += end_offsets[i]; - } - - std::function(ffi_go::LogMessage const&)> query_fn; - if (false == queries.empty()) { - query_fn = [&](ffi_go::LogMessage const& log_message) -> std::pair { - auto const found_query = std::find_if( - queries.cbegin(), - queries.cend(), - [&](std::pair const& query) -> bool { - return wildcard_match_unsafe(log_message, query.first, query.second); - } - ); - return {queries.cend() != found_query, found_query - queries.cbegin()}; - }; - } else { - query_fn = [](ffi_go::LogMessage const&) -> std::pair { - return {true, 0}; - }; - } - - IRErrorCode err{}; - while (true) { - epoch_time_ms_t timestamp{}; - if constexpr (std::is_same_v) { - err = eight_byte_encoding::decode_next_message( - ir_buf, - deserializer->m_log_event.m_log_message, - timestamp - ); - } else if constexpr (std::is_same_v) { - epoch_time_ms_t timestamp_delta{}; - err = four_byte_encoding::decode_next_message( - ir_buf, - deserializer->m_log_event.m_log_message, - timestamp_delta - ); - timestamp = deserializer->m_timestamp + timestamp_delta; - } else { - static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); - } - if (IRErrorCode_Success != err) { - return static_cast(err); - } - deserializer->m_timestamp = timestamp; - - if (time_interval.m_upper <= deserializer->m_timestamp) { - // TODO this is an extremely fragile hack until the CLP ffi ir - // code is refactored and IRErrorCode includes things beyond - // decoding. - return static_cast(IRErrorCode_Incomplete_IR + 1); - } - if (time_interval.m_lower > deserializer->m_timestamp) { - continue; - } - auto const [has_matching_query, matching_query_idx]{ - query_fn(deserializer->m_log_event.m_log_message) - }; - if (false == has_matching_query) { - continue; - } - size_t pos{0}; - if (ErrorCode_Success != ir_buf.try_get_pos(pos)) { - return static_cast(IRErrorCode_Decode_Error); - } - *ir_pos = pos; - log_event->m_log_message.m_data = deserializer->m_log_event.m_log_message.data(); - log_event->m_log_message.m_size = deserializer->m_log_event.m_log_message.size(); - log_event->m_timestamp = deserializer->m_timestamp; - *matching_query = matching_query_idx; - return static_cast(IRErrorCode_Success); - } - } +} } // namespace extern "C" auto ir_deserializer_close(void* ir_deserializer) -> void { @@ -204,7 +207,7 @@ extern "C" auto ir_deserializer_close(void* ir_deserializer) -> void { delete static_cast(ir_deserializer); } -extern "C" auto ir_deserializer_deserialize_preamble( +extern "C" auto ir_deserializer_new_deserializer_with_preamble( ByteSpan ir_view, size_t* ir_pos, int8_t* ir_encoding, @@ -214,6 +217,12 @@ extern "C" auto ir_deserializer_deserialize_preamble( void** ir_deserializer_ptr, void** timestamp_ptr ) -> int { + if (nullptr == ir_pos || nullptr == ir_encoding || nullptr == metadata_type + || nullptr == metadata_pos || nullptr == metadata_size || nullptr == ir_deserializer_ptr + || nullptr == timestamp_ptr) + { + return static_cast(IRErrorCode_Corrupted_IR); + } BufferReader ir_buf{static_cast(ir_view.m_data), ir_view.m_size}; bool four_byte_encoding{}; diff --git a/cpp/src/ffi_go/ir/deserializer.h b/cpp/src/ffi_go/ir/deserializer.h index 3298544..ba8fcc9 100644 --- a/cpp/src/ffi_go/ir/deserializer.h +++ b/cpp/src/ffi_go/ir/deserializer.h @@ -17,7 +17,7 @@ extern "C" { /** * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. * @param[in] ir_deserializer The address of a ir::Deserializer created and - * returned by ir_deserializer_deserialize_preamble + * returned by ir_deserializer_new_deserializer_with_preamble */ void ir_deserializer_close(void* ir_deserializer); @@ -40,7 +40,7 @@ extern "C" { * @return ffi::ir_stream::IRErrorCode forwarded from either * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble */ - int ir_deserializer_deserialize_preamble( + int ir_deserializer_new_deserializer_with_preamble( ByteSpan ir_view, size_t* ir_pos, int8_t* ir_encoding, diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index fc509c9..fa722d1 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -18,64 +18,69 @@ namespace ffi_go::ir { using namespace ffi::ir_stream; namespace { - /** - * Generic helper for ir_encoder_encode_*_log_message - */ - template - auto encode_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - encoded_var_view_t* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets - ) -> int { - using encoded_var_t = typename std::conditional< - std::is_same_v, - ffi::eight_byte_encoded_variable_t, - ffi::four_byte_encoded_variable_t>::type; - Encoder* encoder{static_cast*>(ir_encoder)}; - auto& ir_log_msg{encoder->m_log_message}; - ir_log_msg.reserve(log_message.m_size); - - std::string_view const log_msg_view{log_message.m_data, log_message.m_size}; - std::vector dict_var_offsets; - if (false - == ffi::encode_message( - log_msg_view, - ir_log_msg.m_logtype, - ir_log_msg.m_vars, - dict_var_offsets - )) - { - return static_cast(IRErrorCode_Corrupted_IR); - } +/** + * Generic helper for ir_encoder_encode_*_log_message + */ +template +auto encode_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + encoded_var_view_t* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +) -> int { + using encoded_var_t = typename std::conditional< + std::is_same_v, + ffi::eight_byte_encoded_variable_t, + ffi::four_byte_encoded_variable_t>::type; + if (nullptr == ir_encoder || nullptr == logtype || nullptr == vars || nullptr == dict_vars + || nullptr == dict_var_end_offsets) + { + return static_cast(IRErrorCode_Corrupted_IR); + } + Encoder* encoder{static_cast*>(ir_encoder)}; + auto& ir_log_msg{encoder->m_log_message}; + ir_log_msg.reserve(log_message.m_size); - // dict_var_offsets contains begin_pos followed by end_pos of each - // dictionary variable in the message - int32_t prev_end_off{0}; - for (size_t i = 0; i < dict_var_offsets.size(); i += 2) { - int32_t const begin_pos{dict_var_offsets[i]}; - int32_t const end_pos{dict_var_offsets[i + 1]}; - ir_log_msg.m_dict_vars.insert( - ir_log_msg.m_dict_vars.cbegin() + prev_end_off, - log_msg_view.cbegin() + begin_pos, - log_msg_view.cbegin() + end_pos - ); - prev_end_off = prev_end_off + (end_pos - begin_pos); - ir_log_msg.m_dict_var_end_offsets.push_back(prev_end_off); - } + std::string_view const log_msg_view{log_message.m_data, log_message.m_size}; + std::vector dict_var_offsets; + if (false + == ffi::encode_message( + log_msg_view, + ir_log_msg.m_logtype, + ir_log_msg.m_vars, + dict_var_offsets + )) + { + return static_cast(IRErrorCode_Corrupted_IR); + } - logtype->m_data = ir_log_msg.m_logtype.data(); - logtype->m_size = ir_log_msg.m_logtype.size(); - vars->m_data = ir_log_msg.m_vars.data(); - vars->m_size = ir_log_msg.m_vars.size(); - dict_vars->m_data = ir_log_msg.m_dict_vars.data(); - dict_vars->m_size = ir_log_msg.m_dict_vars.size(); - dict_var_end_offsets->m_data = ir_log_msg.m_dict_var_end_offsets.data(); - dict_var_end_offsets->m_size = ir_log_msg.m_dict_var_end_offsets.size(); - return static_cast(IRErrorCode_Success); + // dict_var_offsets contains begin_pos followed by end_pos of each + // dictionary variable in the message + int32_t prev_end_off{0}; + for (size_t i = 0; i < dict_var_offsets.size(); i += 2) { + int32_t const begin_pos{dict_var_offsets[i]}; + int32_t const end_pos{dict_var_offsets[i + 1]}; + ir_log_msg.m_dict_vars.insert( + ir_log_msg.m_dict_vars.cbegin() + prev_end_off, + log_msg_view.cbegin() + begin_pos, + log_msg_view.cbegin() + end_pos + ); + prev_end_off = prev_end_off + (end_pos - begin_pos); + ir_log_msg.m_dict_var_end_offsets.push_back(prev_end_off); } + + logtype->m_data = ir_log_msg.m_logtype.data(); + logtype->m_size = ir_log_msg.m_logtype.size(); + vars->m_data = ir_log_msg.m_vars.data(); + vars->m_size = ir_log_msg.m_vars.size(); + dict_vars->m_data = ir_log_msg.m_dict_vars.data(); + dict_vars->m_size = ir_log_msg.m_dict_vars.size(); + dict_var_end_offsets->m_data = ir_log_msg.m_dict_var_end_offsets.data(); + dict_var_end_offsets->m_size = ir_log_msg.m_dict_var_end_offsets.size(); + return static_cast(IRErrorCode_Success); +} } // namespace extern "C" auto ir_encoder_eight_byte_new() -> void* { diff --git a/cpp/src/ffi_go/ir/serializer.cpp b/cpp/src/ffi_go/ir/serializer.cpp index 27506a5..81d97d6 100644 --- a/cpp/src/ffi_go/ir/serializer.cpp +++ b/cpp/src/ffi_go/ir/serializer.cpp @@ -19,69 +19,68 @@ using namespace ffi; using namespace ffi::ir_stream; namespace { - /** - * Generic helper for ir_serializer_serialize_*_log_event - */ - template - auto serialize_log_event( - StringView log_message, - epoch_time_ms_t timestamp_or_delta, - void* ir_serializer, - ByteSpan* ir_view - ) -> int { - Serializer* serializer{static_cast(ir_serializer)}; - serializer->m_ir_buf.clear(); - - bool success{false}; - if constexpr (std::is_same_v) { - success = eight_byte_encoding::encode_message( - timestamp_or_delta, - std::string_view{log_message.m_data, log_message.m_size}, - serializer->m_logtype, - serializer->m_ir_buf - ); - } else if constexpr (std::is_same_v) { - success = four_byte_encoding::encode_message( - timestamp_or_delta, - std::string_view{log_message.m_data, log_message.m_size}, - serializer->m_logtype, - serializer->m_ir_buf - ); - } else { - static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); - } - if (false == success) { - return static_cast(IRErrorCode_Corrupted_IR); - } - - ir_view->m_data = serializer->m_ir_buf.data(); - ir_view->m_size = serializer->m_ir_buf.size(); - return static_cast(IRErrorCode_Success); - } -} // namespace +/** + * Generic helper for ir_serializer_new_*_serializer_with_preamble functions. + */ +template +[[nodiscard]] auto new_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + [[maybe_unused]] epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +) -> int; -extern "C" auto ir_serializer_close(void* ir_serializer) -> void { - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - delete static_cast(ir_serializer); -} +/** + * Generic helper for ir_serializer_serialize_*_log_event functions. + */ +template +[[nodiscard]] auto serialize_log_event( + StringView log_message, + epoch_time_ms_t timestamp_or_delta, + void* ir_serializer, + ByteSpan* ir_view +) -> int; -extern "C" auto ir_serializer_serialize_eight_byte_preamble( +template +auto new_serializer_with_preamble( StringView ts_pattern, StringView ts_pattern_syntax, StringView time_zone_id, + [[maybe_unused]] epoch_time_ms_t reference_ts, void** ir_serializer_ptr, ByteSpan* ir_view ) -> int { + if (nullptr == ir_serializer_ptr || nullptr == ir_view) { + return static_cast(IRErrorCode_Corrupted_IR); + } Serializer* serializer{new Serializer{}}; + if (nullptr == serializer) { + return static_cast(IRErrorCode_Corrupted_IR); + } *ir_serializer_ptr = serializer; - if (false - == eight_byte_encoding::encode_preamble( + + bool success{false}; + if constexpr (std::is_same_v) { + success = eight_byte_encoding::encode_preamble( std::string_view{ts_pattern.m_data, ts_pattern.m_size}, std::string_view{ts_pattern_syntax.m_data, ts_pattern_syntax.m_size}, std::string_view{time_zone_id.m_data, time_zone_id.m_size}, serializer->m_ir_buf - )) - { + ); + } else if constexpr (std::is_same_v) { + success = four_byte_encoding::encode_preamble( + std::string_view{ts_pattern.m_data, ts_pattern.m_size}, + std::string_view{ts_pattern_syntax.m_data, ts_pattern_syntax.m_size}, + std::string_view{time_zone_id.m_data, time_zone_id.m_size}, + reference_ts, + serializer->m_ir_buf + ); + } else { + static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); + } + if (false == success) { return static_cast(IRErrorCode_Corrupted_IR); } @@ -90,31 +89,38 @@ extern "C" auto ir_serializer_serialize_eight_byte_preamble( return static_cast(IRErrorCode_Success); } -extern "C" auto ir_serializer_serialize_four_byte_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - epoch_time_ms_t reference_ts, - void** ir_serializer_ptr, +template +auto serialize_log_event( + StringView log_message, + epoch_time_ms_t timestamp_or_delta, + void* ir_serializer, ByteSpan* ir_view ) -> int { - if (nullptr == ir_serializer_ptr || nullptr == ir_view) { - return static_cast(IRErrorCode_Corrupted_IR); - } - Serializer* serializer{new Serializer{}}; - if (nullptr == serializer) { + if (nullptr == ir_serializer || nullptr == ir_view) { return static_cast(IRErrorCode_Corrupted_IR); } - *ir_serializer_ptr = serializer; - if (false - == four_byte_encoding::encode_preamble( - std::string_view{ts_pattern.m_data, ts_pattern.m_size}, - std::string_view{ts_pattern_syntax.m_data, ts_pattern_syntax.m_size}, - std::string_view{time_zone_id.m_data, time_zone_id.m_size}, - reference_ts, + Serializer* serializer{static_cast(ir_serializer)}; + serializer->m_ir_buf.clear(); + + bool success{false}; + if constexpr (std::is_same_v) { + success = eight_byte_encoding::encode_message( + timestamp_or_delta, + std::string_view{log_message.m_data, log_message.m_size}, + serializer->m_logtype, serializer->m_ir_buf - )) - { + ); + } else if constexpr (std::is_same_v) { + success = four_byte_encoding::encode_message( + timestamp_or_delta, + std::string_view{log_message.m_data, log_message.m_size}, + serializer->m_logtype, + serializer->m_ir_buf + ); + } else { + static_assert(cAlwaysFalse, "Invalid/unhandled encoding type"); + } + if (false == success) { return static_cast(IRErrorCode_Corrupted_IR); } @@ -122,6 +128,47 @@ extern "C" auto ir_serializer_serialize_four_byte_preamble( ir_view->m_size = serializer->m_ir_buf.size(); return static_cast(IRErrorCode_Success); } +} // namespace + +extern "C" auto ir_serializer_close(void* ir_serializer) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast(ir_serializer); +} + +extern "C" auto ir_serializer_new_eight_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view +) -> int { + return new_serializer_with_preamble( + ts_pattern, + ts_pattern_syntax, + time_zone_id, + 0, + ir_serializer_ptr, + ir_view + ); +} + +extern "C" auto ir_serializer_new_four_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +) -> int { + return new_serializer_with_preamble( + ts_pattern, + ts_pattern_syntax, + time_zone_id, + reference_ts, + ir_serializer_ptr, + ir_view + ); +} extern "C" auto ir_serializer_serialize_eight_byte_log_event( StringView log_message, diff --git a/cpp/src/ffi_go/ir/serializer.h b/cpp/src/ffi_go/ir/serializer.h index 5e8aa46..632089f 100644 --- a/cpp/src/ffi_go/ir/serializer.h +++ b/cpp/src/ffi_go/ir/serializer.h @@ -13,100 +13,100 @@ extern "C" { #endif -/** - * Clean up the underlying ir::Serializer of a Go ir.Serializer. - * @param[in] ir_serializer Address of a ir::Serializer created and returned by - * ir_serializer_serialize_*_preamble - */ -void ir_serializer_close(void* ir_serializer); + /** + * Clean up the underlying ir::Serializer of a Go ir.Serializer. + * @param[in] ir_serializer Address of a ir::Serializer created and returned by + * ir_serializer_serialize_*_preamble + */ + void ir_serializer_close(void* ir_serializer); -/** - * Given the fields of a CLP IR preamble, serialize them into an IR byte stream - * with eight byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::encode_preamble - */ -int ir_serializer_serialize_eight_byte_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - void** ir_serializer_ptr, - ByteSpan* ir_view -); + /** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with eight byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_preamble + */ + int ir_serializer_new_eight_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view + ); -/** - * Given the fields of a CLP IR preamble, serialize them into an IR byte stream - * with four byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::encode_preamble - */ -int ir_serializer_serialize_four_byte_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - epoch_time_ms_t reference_ts, - void** ir_serializer_ptr, - ByteSpan* ir_view -); + /** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with four byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_preamble + */ + int ir_serializer_new_four_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view + ); -/** - * Given the fields of a log event, serialize them into an IR byte stream with - * eight byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message of the log event to serialize - * @param[in] timestamp Timestamp of the log event to serialize - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::encode_message - */ -int ir_serializer_serialize_eight_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp, - void* ir_serializer, - ByteSpan* ir_view -); + /** + * Given the fields of a log event, serialize them into an IR byte stream with + * eight byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message of the log event to serialize + * @param[in] timestamp Timestamp of the log event to serialize + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_message + */ + int ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view + ); -/** - * Given the fields of a log event, serialize them into an IR byte stream with - * four byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to serialize - * @param[in] timestamp_delta Timestamp delta to the previous log event in the - * IR stream - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::encode_message - */ -int ir_serializer_serialize_four_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp_delta, - void* ir_serializer, - ByteSpan* ir_view -); + /** + * Given the fields of a log event, serialize them into an IR byte stream with + * four byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to serialize + * @param[in] timestamp_delta Timestamp delta to the previous log event in the + * IR stream + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_message + */ + int ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view + ); #ifdef __cplusplus } diff --git a/cpp/src/ffi_go/search/wildcard_query.cpp b/cpp/src/ffi_go/search/wildcard_query.cpp index b21faa1..2d707bd 100644 --- a/cpp/src/ffi_go/search/wildcard_query.cpp +++ b/cpp/src/ffi_go/search/wildcard_query.cpp @@ -8,19 +8,18 @@ #include namespace ffi_go::search { -extern "C" auto wildcard_query_delete(void* str) -> void { - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - delete static_cast(str); -} - -extern "C" auto wildcard_query_clean(StringView query, void** ptr) -> StringView { +extern "C" auto wildcard_query_new(StringView query, void** ptr) -> StringView { auto* clean{new std::string{ - clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size}) - }}; + clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size})}}; *ptr = clean; return {clean->data(), clean->size()}; } +extern "C" auto wildcard_query_delete(void* str) -> void { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete static_cast(str); +} + extern "C" auto wildcard_query_match(StringView target, WildcardQueryView query) -> int { return static_cast(wildcard_match_unsafe( {target.m_data, target.m_size}, diff --git a/cpp/src/ffi_go/search/wildcard_query.h b/cpp/src/ffi_go/search/wildcard_query.h index 3aea7bf..3adcc5d 100644 --- a/cpp/src/ffi_go/search/wildcard_query.h +++ b/cpp/src/ffi_go/search/wildcard_query.h @@ -43,6 +43,15 @@ extern "C" { BoolSpan m_case_sensitivity; } MergedWildcardQueryView; + /** + * Given a query string, allocate and return a clean string that is safe for + * matching. See `clean_up_wildcard_search_string` in CLP for more details. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ + StringView wildcard_query_new(StringView query, void** ptr); + /** * Delete a std::string holding a wildcard query. * @param[in] str Address of a std::string created and returned by @@ -50,15 +59,6 @@ extern "C" { */ void wildcard_query_delete(void* str); - /** - * Given a query string, clean it to be safe for matching. See - * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. - * @param[in] query Query string to clean - * @param[in] ptr Address of a new std::string - * @return New string holding cleaned query - */ - StringView wildcard_query_clean(StringView query, void** ptr); - /** * Given a target string perform CLP wildcard matching using query. See * `wildcard_match_unsafe` in CLP src/string_utils.hpp. diff --git a/include/ffi_go/ir/decoder.h b/include/ffi_go/ir/decoder.h index 3c59ea1..32e5b51 100644 --- a/include/ffi_go/ir/decoder.h +++ b/include/ffi_go/ir/decoder.h @@ -5,15 +5,15 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. * @return New ir::Decoder's address @@ -38,7 +38,7 @@ extern "C" { * @param[in] vars Array of encoded variables * @param[in] dict_vars String containing all dictionary variables concatenated * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log * message diff --git a/include/ffi_go/ir/deserializer.h b/include/ffi_go/ir/deserializer.h index 7ad65f7..ba8fcc9 100644 --- a/include/ffi_go/ir/deserializer.h +++ b/include/ffi_go/ir/deserializer.h @@ -4,25 +4,25 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-trailing-return-type) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. * @param[in] ir_deserializer The address of a ir::Deserializer created and - * returned by ir_deserializer_deserialize_preamble + * returned by ir_deserializer_new_deserializer_with_preamble */ void ir_deserializer_close(void* ir_deserializer); /** - * Given a CLP IR buffer (any encoding), attempt to deserialize a premable and + * Given a CLP IR buffer (any encoding), attempt to deserialize a preamble and * extract its information. An ir::Deserializer will be allocated to use as the * backing storage for a Go ir.Deserializer (i.e. subsequent calls to * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read @@ -40,7 +40,7 @@ extern "C" { * @return ffi::ir_stream::IRErrorCode forwarded from either * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble */ - int ir_deserializer_deserialize_preamble( + int ir_deserializer_new_deserializer_with_preamble( ByteSpan ir_view, size_t* ir_pos, int8_t* ir_encoding, diff --git a/include/ffi_go/ir/encoder.h b/include/ffi_go/ir/encoder.h index b8d78e3..1737ada 100644 --- a/include/ffi_go/ir/encoder.h +++ b/include/ffi_go/ir/encoder.h @@ -5,15 +5,15 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. * @return New ir::Encoder's address @@ -52,7 +52,7 @@ extern "C" { * @param[out] vars Array of encoded variables * @param[out] dict_vars String containing all dictionary variables concatenated * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message * returns false @@ -80,7 +80,7 @@ extern "C" { * @param[out] vars Array of encoded variables * @param[out] dict_vars String containing all dictionary variables concatenated * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message * returns false diff --git a/include/ffi_go/ir/serializer.h b/include/ffi_go/ir/serializer.h index 0e200a8..632089f 100644 --- a/include/ffi_go/ir/serializer.h +++ b/include/ffi_go/ir/serializer.h @@ -9,100 +9,108 @@ #include -/** - * Clean up the underlying ir::Serializer of a Go ir.Serializer. - * @param[in] ir_serializer Address of a ir::Serializer created and returned by - * ir_serializer_serialize_*_preamble - */ -void ir_serializer_close(void* ir_serializer); +#ifdef __cplusplus +extern "C" { +#endif -/** - * Given the fields of a CLP IR premable, serialize them into an IR byte stream - * with eight byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwared from - * ffi::ir_stream::eight_byte_encoding::encode_preamble - */ -int ir_serializer_serialize_eight_byte_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - void** ir_serializer_ptr, - ByteSpan* ir_view -); + /** + * Clean up the underlying ir::Serializer of a Go ir.Serializer. + * @param[in] ir_serializer Address of a ir::Serializer created and returned by + * ir_serializer_serialize_*_preamble + */ + void ir_serializer_close(void* ir_serializer); -/** - * Given the fields of a CLP IR premable, serialize them into an IR byte stream - * with four byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwared from - * ffi::ir_stream::four_byte_encoding::encode_preamble - */ -int ir_serializer_serialize_four_byte_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - epoch_time_ms_t reference_ts, - void** ir_serializer_ptr, - ByteSpan* ir_view -); + /** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with eight byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_preamble + */ + int ir_serializer_new_eight_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view + ); -/** - * Given the fields of a log event, serialize them into an IR byte stream with - * eight byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message of the log event to serialize - * @param[in] timestamp Timestamp of the log event to serialize - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwared from - * ffi::ir_stream::eight_byte_encoding::encode_message - */ -int ir_serializer_serialize_eight_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp, - void* ir_serializer, - ByteSpan* ir_view -); + /** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with four byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_preamble + */ + int ir_serializer_new_four_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view + ); -/** - * Given the fields of a log event, serialize them into an IR byte stream with - * four byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to serialize - * @param[in] timestamp_delta Timestamp delta to the previous log event in the - * IR stream - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwared from - * ffi::ir_stream::four_byte_encoding::encode_message - */ -int ir_serializer_serialize_four_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp_delta, - void* ir_serializer, - ByteSpan* ir_view -); + /** + * Given the fields of a log event, serialize them into an IR byte stream with + * eight byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message of the log event to serialize + * @param[in] timestamp Timestamp of the log event to serialize + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_message + */ + int ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view + ); + + /** + * Given the fields of a log event, serialize them into an IR byte stream with + * four byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to serialize + * @param[in] timestamp_delta Timestamp delta to the previous log event in the + * IR stream + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_message + */ + int ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view + ); + +#ifdef __cplusplus +} +#endif // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/include/ffi_go/search/wildcard_query.h b/include/ffi_go/search/wildcard_query.h index 36c1aac..3adcc5d 100644 --- a/include/ffi_go/search/wildcard_query.h +++ b/include/ffi_go/search/wildcard_query.h @@ -5,14 +5,14 @@ // NOLINTBEGIN(modernize-use-trailing-return-type) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include +#ifdef __cplusplus +extern "C" { +#endif + /** * A timestamp interval of [m_lower, m_upper). */ @@ -43,6 +43,15 @@ extern "C" { BoolSpan m_case_sensitivity; } MergedWildcardQueryView; + /** + * Given a query string, allocate and return a clean string that is safe for + * matching. See `clean_up_wildcard_search_string` in CLP for more details. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ + StringView wildcard_query_new(StringView query, void** ptr); + /** * Delete a std::string holding a wildcard query. * @param[in] str Address of a std::string created and returned by @@ -50,15 +59,6 @@ extern "C" { */ void wildcard_query_delete(void* str); - /** - * Given a query string, clean it to be safe for matching. See - * `clean_up_wildcard_search_string` in CLP src/string_utils.hpp. - * @param[in] query Query string to clean - * @param[in] ptr Address of a new std::string - * @return New string holding cleaned query - */ - StringView wildcard_query_clean(StringView query, void** ptr); - /** * Given a target string perform CLP wildcard matching using query. See * `wildcard_match_unsafe` in CLP src/string_utils.hpp. diff --git a/ir/deserializer.go b/ir/deserializer.go index 27df183..ce4c1c3 100644 --- a/ir/deserializer.go +++ b/ir/deserializer.go @@ -56,7 +56,7 @@ func DeserializePreamble(irBuf []byte) (Deserializer, int, error) { var metadataSize C.uint16_t var deserializerCptr unsafe.Pointer var timestampCptr unsafe.Pointer - if err := IrError(C.ir_deserializer_deserialize_preamble( + if err := IrError(C.ir_deserializer_new_deserializer_with_preamble( newCByteSpan(irBuf), &pos, &irEncoding, diff --git a/ir/serializer.go b/ir/serializer.go index 2aad104..85de39c 100644 --- a/ir/serializer.go +++ b/ir/serializer.go @@ -40,7 +40,7 @@ func EightByteSerializer( irs := eightByteSerializer{ commonSerializer{TimestampInfo{tsPattern, tsPatternSyntax, timeZoneId}, nil}, } - if err := IrError(C.ir_serializer_serialize_eight_byte_preamble( + if err := IrError(C.ir_serializer_new_eight_byte_serializer_with_preamble( newCStringView(tsPattern), newCStringView(tsPatternSyntax), newCStringView(timeZoneId), @@ -69,7 +69,7 @@ func FourByteSerializer( commonSerializer{TimestampInfo{tsPattern, tsPatternSyntax, timeZoneId}, nil}, referenceTs, } - if err := IrError(C.ir_serializer_serialize_four_byte_preamble( + if err := IrError(C.ir_serializer_new_four_byte_serializer_with_preamble( newCStringView(tsPattern), newCStringView(tsPatternSyntax), newCStringView(timeZoneId), diff --git a/lib/libclp_ffi_linux_amd64.a b/lib/libclp_ffi_linux_amd64.a index aa630bac3ec7c4844f45461e0dff6adc85b2c5b5..82aca71f701b999f014bc65dfe87919345c175bd 100644 GIT binary patch delta 17660 zcmeHudwdi{wtsg|B^g3U53js{WYCdKL?DTx2`EfR0zKH#AVedIfCA!zfEpBeIHQ?B znql0mgBD(Ojl2F_R^5eNy@u=ZlPD5=!w+BhSi~1F;!0FRh!1|>Q`M6+XuP}kvyXpn zebTAwb52#AsybEmJzX`sj`iKs*sp1H@SGAag&C=eqI{_+6F&IoJU3meC_nZDe~kF| z(*C$wQU1HBX6fwfr{8o<-Q34KWt&(ov1VU$_1wDYSKeGVd-_Q-v3D!829d~|g^RE6PT~}P z{sGoMHfH>27ex63scaChKFl&=q49cs>-bfz<5fIbOY3-7`^{R>+5G!sTA@0l<-=Cj zay4jJAD3NHc5zuvmGyGjl*vZ@r%Q}TWtI_{<~6O#CNomyF(WJT4Xd(omJwm6Q z;&p-2Z}hO@QWP^%?m_)u1WI?-%{S^hbbVE^swjBXZ!18;96c>mH`$0*t42es9v*`% zM#C<>;dN9Mh%Qo1>pjz|S!Y_&MaPdHH>~IqLu(MJ8VUSwGxuwF4rpVPyy zKsO@-R_Th69(HSd_g0rz{e~ag>Kdqi%`={L4G&&U-4UI=`?9jj%chlGQ8sQ}5&^u`L&SC#0A)*A*^sjY}_>{o?0HXf#CTb1kdypU|iigiZ)c2#eDM=+vuWkadG zrgdj$d(DW;jEL8OwUtJp(2%W1Cx|Q!t<;Tr_%L-&!>+pVrqw38FFHe($RFP3$_*kH ziK8r0L!^OfK#^u?WvI?T!5-7vj{deYiK4|oNO|i%helH9qymKiMhAm7z_7|aao6|9 zkE3HYVQiIn#she>hX_h$!zQ3}oZq<3l{av-AivC39#>Nz~ADB|SU+jS`{rHGXH95H5uy)3& zF>@%l%#5!1QqS$E`4`N};7?7q9Lno2I)7MAP3|Bbl4M})GAhxrC+mpDb-jz`^ zHd(NLz<p9+zLV!o6vd1pfwJLpsR!fbXbpXgy}HBOm3++BH{jos>?+l>`%eZi)m z+}0g@nTL(Y{*|LEvK%VzY1!&w6MAKy1FT=r&~y0PV((^;Obec+CH z9?6Z3x;9IFj!z!N#%g_1V>7pA@Zl{kPwdHSvz+gI;!Z6q)k_X4LJx(q@=>E%R%)S5 zGbC*(XljVZMzgW0A)8erSsldM%ln?o#`bvKW@YjFpK|5#rRSogcAHi&#h(OCtDVGO zJC}{=u>^C5!s)RK*XQw3=Rtg; zFvzZP42tliOWLb;ji71n$^12vKVb8omo<)scq`!tglm#S^Ou6A&gHSOXiq_Z$r{V^ zTF{;ju(Y}T`J=~JZfwtfHy`C^Sv^9wP?0EdDM+cUHfdN-QRI`Lsf+lleiZpvuPpKc zS>&iPlrVa*q>YiZrJ!k^X?$%N8>24gua>c~=z_lE(D&7+NXa!!>N*aRg;(&kd9~0;?zb3*dpz1$GMD$80L>b0;?fBqR?p>YiP$tU*18{p zMB6;F)7bea1BBL*G5>)fd@0hYd+o{doGa$pkjtM04I}-k0WF|(03Fp@DLWDqtXhXJRpB#= zTFpq<*Mx%1$Qs`|#75*^Uo*vUc)}@IxDD%Nqv3fy{0)dz4S&_cUx)-eFT#*;9s<^D zrd8o{>uaB>YIxqX(tP^bR`)Y{!}H)z70y~KQf9Qy=Pi030MD?#H?6M)d^JGK#7kR+ zFH;c|GlHVvku>}gdGsehH?5~ltHZQDgIr^!lxk>QT*s5E*(t$E1~O!A$%k~nVm`Z! zG~XQk5V;;6W4qrA9-2W(HzoDnet3tfQQzu?XI9jDuvY>F3va-PT<$Yw8AAsOmvgZW zFRb>mX$f~~tEjQCP9E#8kuan4eICNN z)=TJxBJ|cF)7r-WZ#B!!9YhtEbX7bH6&teRt#g|A_torlbrUa`#7+&aZ4#OuTw^Qa z7vP;n+ExW4?LT3pOUP|LQL(2Jif!sfF^r8H+Q}}ZVT1E>$k<-t;}1<@89v`h`p$#C z{zON?=?tM*{+CHCUwvF?oz_HNiB!CSf3B$mf49$wV$AmQyeVwZkc&i{%Du*Dnyk_D=@m)O@>ZjMv#*&{*2A@Y#uS!u!F|*U zgGi=z#I*JcrOP%Wi-M^)g615&a*Ru5(MJZRQp6RoDlvxi8K%;W*o)XhVwht2yl5&r zjW4TZK6L|cu4P%6a20?En+eAq+|FOAWka%ue}zUx&-?nYXw1Ei##Gg?oXnfxg_l{c z#m@j}DcekVD?S)WzP<)uoj&CFx2U(uVtb$P1Uu)!zsItI=q2;tXjucuwEl;dmHNdA zmi3|>45wxN?h-ZxA8~BUA~W%0EE21--Y|{K3bp(3l_g{g;trKb-Qa^x;M%y)AQFPm8BQ1OfE|=sZXg$Z&LXK0awa8%V>v;UKdf$K98dGf#(sjT`JLZl#tn*O31mdw`4C$cu;mLU+x`i{U9@|kDb|# z90;%24(2L8{c7OP-}Dh$w8?0{jBb}vQeRPizrM1B>a-I}nAn#$PjzLt44%ur(Zxsi zAAFCWzUcbdGq1gJ{_NNvlX6=wSi%-)G2csG{$ER(kH6kq%jVh3*u0jN%h+?O`T)Oh z17tq1rgT~L&va4s9-XTGSQk})YI|KLSN*vzt3Kl3JFEUq zmsM*p$GbB4o)F8*8Dp!7tzUtCCrx1t&89{HZ&Un*af98Zh?RyYtv_u5uU z0`w$|d-qB7gjN5HZ>VRPeBT`|Pmhp29+mv~9j;9E9?6<|I*r>MuC()>rvecu>W9aR zhr(_LostQQe&0dw$2XjgdX|PU??<+*3bQS$mg^zcj~sl>4?k@s^k3lVbb5q`?|koo z+#(P6uYxZLt6c?O=S#i?k>)A6s}Y$O_U3-X@XCUXpe`!mtwdJ(++`txw+k<9j8<66 zLliIS&s!Az{}N;yKZH z*hh$)06DO0GRYHm{SGwYO}#E1DcgP6M}^Zl*I$qz{B3=C0I47jqAI<@$+7kjI5q;2 zO2OYQaUoF?!dzHcbex>EB~)yd5tUg5 z07sqhUVs?^orwno=oMJtJtG2VffQ51I5FFY*%Mlhdg=h#CAO4c4=t(7GXRb@LeePs zUgRMED@0js8NhSuAb3Jdpnbi;gKss)tvYu zNugTMI-G%qg>N1hy||Fh{m>a!vQgg_1lnGlAD<_VvceDsgFN&%k-$L}^PM{kJ`rA` z)jLBSJ@i#iA$@M);#8ylJ5_HGV}Qo2qkQCu?z2_a!*>!18W2mWUj-4Vktv!1FHH}I z1EL+3Xveeqs!4chV+Fl`vK>uSBDJGo6o!lZAcH!T$fEZ6uf!P|PAtDu>-wM-6+X58 zK+(BwedQ!%Xi8{9THqv!)Cgyz18G!+76V0Z7W5%4o`|hn(wOvE2xJrA>MF|MJF%*9yVk0 zbc=Rjx{5ZD`NW?V?W#}|aV{7S;#HnFmTiK@aAk{j8T5@Kk)=|JA0(M3{Ry=(MHF$1 zcz=?1pm-b7!NjSu-H9|>vJ)?d4q!@~2-JV8>nlHjm58dFsv17h8=nRwFw|C>&DGRz z^>1L#lFQt5wD`qDs-q}Jq-)1J53l+IJge2$APY4eS!l}TJUOxBu&d& zh?->VbvZONIKHJ-vDUz;t&pL$qm~>6_`X&6bW`bx2cV(U^+_~N=qej16drW4!UGv@ z(g=pq04<)dc$T=XbiGU>jQY;qQ~q70V|gWi$ZO| zCu(q~JtZ-rX6WH&*pakTEDPvcx$x37UQasJZ`BMej!z%O#i&SW!Y?jvjD#31sAwa;?#Z>M zN?qX>ubm$q?2PdgNYkw9TIr1fK71|fp}x#Z*0M9;NX%Z#hUY>vzcCb>lipB^s-bg9 zm3o-=oiwz8|8XtrS6yYTNGz{&^!ZPh>AfpU;uFLwiVE!IJrG?{E&4q^1sn|77HB{r zXj<~@o%%$41mXSfWW$1;(~af{KIP@(+ejc%Rz_{3ekqK#C?jHmyBX1>eKGN(^rbtaiw3YogA1byf+iFq0)2)CA?WI)>>yR@x3NI!r+S!n7O-VEVLN`z zC_RWNN1xC-K?6q8El>kK=$X=L2w!;u(Sc_hrQ7xJdng1Q<}pgQ>MJ8uJeDs{mDS$0 z$Opm4IyTMS#zez{{gW>b+3GnwbDuUqe$D*wF4C^b= zI3rqaqua2_V92@?;I6ton0 zmqh6$AGO$c7oCtrV-8r~0jwkzN}&kB-5uCf0C(ppbyw4>OBUA!cTb_&(M%;K;I60Z z+RcD2$h#gQ4#@ji4?gx6Ec31!mJHY_o5C-=8!G=AuogFZs=83^HDb4bs@NL{)t(X{ zR680}LbU@rQSEiK&vc<$6k=NQP&%MH&Gq@?+880)2Ss0*uE%{L=u~BWYz~!}Nqh?q zb^_Zg%*Z_WJg%zf;smbs3QZ;hiM^q(`Vd2kBJ=w~#np{tusd7 z06_Z<3^_+!Q}u-iH(tlzyc2?FPAFy zMB($LS^^--dvE1X3lsbT=m@n6dUvPRBB55pWSR)rgT2%@i^O2Z37B9js1E|1=0LCU z32b@|gu8~-*21Ad4%&#%;!=Y_vSSMRKm!3$yLyNAr4ZUehabWW38pR(Fat1kHWFk` z>k^A?GeptWB?70eOzP@K$beM=pt@Y}CWBYe1yqR{KO}%^2uWa7%pTE1x-0f7wNiYd zmrKGgJKqynm49*%>m5|YNa2(Y0G0AJK@R(j1gLL!X6o$3I@yWyZGenL3%E3nLK3+2 zh~C(80?cc3;L`s*5#>FUj7|^*;u0djCd{Sov;a9a;k9V2xM%p2u#|@5Ct#^z^)%?B z;6&&rHQj<12~F*tJ_r+q#(huLcwhoar=FxCN@;BZD*Y>=QZfR)L72iU%n6!vKZ;Ay4L^!Y3w{VpF|lhafJ_s>6f+6H z^tXImj13#{7R>=0aaKgm*Z|16K|;>Jk3dd~&%BRiRwMxC0tdj%!({6Om>;0j69A@R zE%OV2c_zdJz~ujRAIt3}w}5z(n5jHxEgPh6aX`njfgtZ)%L;~>)-oqL)O(kg#Qnn3 zD`*Z+p>1KA4GpXL7i-xmV}PD;(iOl7Ucu*jcnWHeHgCN^PLGtO-J?2QB6uz;rjX9g zKrG>X?q|bPKM&o>GJ-}Y(CckjW$yR{0rbK&NfZ0=GJo6yedtQ*-vabtHa`LM)}bU? z;Fc4jYf|u;Tu#!vZKSt^7v9BkgEIx7tLp@GlyU&NLvUp-h@Ry&)NQ6EeT)Q_!#=gL z$~t^7fR&+4e`>k!L?|anU0`lMnkh(KXzoRjW(Vq@WmTmg>tO<45I9$!^aIa5YJ%MG z?Fo1eW+?I8c70X3Sb0a&R*nxr=Lk6W4ho^!L|=bauEI-|b=0&z=e;>b;QB7;w!90v z4F(m>FsuV+!CnV&tC7HsZt*(R(!*DP60gQTsP2kSkO?h5Mkdraxj$lOJ`Ug}eIC*C z{o+3;t09%oc!2fKKa8!JRPe6#o1JESpY+5}{@918i=u z9?f{CjbtOAw1Exe#s)TcIF6~(ZdI{Hy`g^4e^aJ7w({b&lfGMR>;J0+y=C(T*5T5g z?a964U0xjfq$Gtj*VQTy&19uoNaJ~rv+3Hy4tBfhS}56k(BrIk%aO;~axL5bE}PI# zxmKj`DYhlj!tz7~k3PwsNUgRnRH4NAe`eQt&Qyk*<;C=`kp30L)Nun^+?(0bBu&%g z=l)A%oO3r8Hwsu}6~eXHenfat;~|`W@Qy3Bv@wMa#_#RpAIji7ewa=K1d2xCNfh)h zi+R^&6#oI8MS<8O@g&&qcSFxj7TIpVEuG^E#r@5tX|1?+=8>CH9chU+W<2ukXmQrNvO5QL3dW0D$jLd9XsrU zp-1h5o-KC#o*f@a6^ApudQuczY+EN)GsaQP%5Lbq8~T5CL-#n2ikyQoQ__Q?ea?#S zcVuvO-|roCr~EVB(BHS|WPI6RDwr!vKXsm$3Z*3-;STi5Yz5u5d}wNl8#bK zr!|l3hF;SRy{;SjOAfko6b598YJWKGE|Cn`ZIDR}_Z$b^sp#@<==a$4lc!y?L*AZt zIe&7?=Wg^$(m|XZ;~x4%(0VlcPACB+ZN_;1;5#Tm86sIpvV?abo%AzF z8YXXpPb6BgO`9k$SFZ+5Yey09qKM@-uTb(*|3X^Wv=`)E>4~6ep?rB4c{ROMPv8&! z1r2V@7uSwc_ki|zerKQaiA30xFRmT0ru1$I?!#^B4Icgiz0aM%KiWg*;lsG^JsgP7 zpydZ4FRAlDJav6Q(l#)8>G*~BSP?5dyU;66$4k<9;a;|!ADyH2^3>x9Ns;G|2GX_M zdY=^xq@OjNi`88KA zUQow#KV)82;qQ-goyXs9XKDQ>(LhY3pZ+XsOLr^ET)dX>gCDYC?3VP}Tlk=l*vMo- zeUPYEFFXqZ9ymi3@XL5Rb47Yqy{jVKtBp@h&ni#%l%?YrLm=YT7?PD++r)*k#C5d4 zBVTOhzCp32TXSQrHCeItnnK=uP&H!Si~I0fK4!J4ACU$KOBA|4#h>{S_hpXZy$>!> z(5+tz37O%lxMTG@kcsW5)e1of6w~@f zSLa$F)?erQL;JIc-@1Q{_G^dOkONuTdKXrVF{#hlyFy5%-zdZ$0;jqs-}_e-Y+!x( z4C{qIg%pL$kzw@Uvh|zQwh;W&(DeXEL-6W%z zWHeskO}NvCsZ>~tYf*w&d>Glp(L{?1oy#A%Cd zHgzDmq6zj!n@+{`=Od257ztx0*ildH~i9-S#!oND0zEL1$Xgx~AOu_xg9Kp}onR6} zI}F4frC3)NR@cYxE?4j;>%d;FL>@s9O<)z)^{QEvg%L%>2LXZnzNfk;4Ef=2_wMJn zyZ_w&FgaDHPMve=)TuhBs+*73BptojscxuuU`_@-C2^7@eIrR@8vZfA6+n*zbnDsQ~ZqN`4fu01hY#!m^J z7;X9dXO^77xAMk%OMh7kw$@u#$zHd9)UCg*=_g%NT~l0>i}ZI~lid2Z9{KDE|`)5w&_d(NS}VLA;R~{l&6YQ?FKc zp`JBIE5>>B29JKhqx(x*TU$N)vkL`ysvJPm{VPSSaN}w8l{`L!$XB@0<1+|-2BA+7 zYW}6NM?c`vCvOD5vBUxliG|T!9{>&l>l0XyZ}LX9do{7fC?>CgRjjScD>eVan~U^6 zdh{b6{Vx;HP{$wNYw4PNSaZImRzHj3XuhItZs%3Cxr)-ttwfh3h_>XJS5=ZL0T%GAYs)I>Okzi^vJpTAkt54iPxq32RT-ez7* zb!T`6lzo*yzTeV4c@bLcF!`F^cy03e@qLzVGq0aqkFSOd0tUOF$h5GqW!)1~wxzyLCHF9WAm|oN|g6u22yup&hJYdM1pJrY8rUpx#H>~A1 zYPU<1|M|OhK>o?gX6kk8=6{wAZ6W`&Y_QCJeA#?*$kH)zw&^yu}Lb<7`CxHfuRRg9&# z+%H4<_qc=@Ye77pb4*i|%M$pXY%D=} zKE~kisR8AAd{a70RTf%#9Y&?xXXW)Yx(a^5&iZ#=X6m7UcUlDhs^Ex3Hl{;oCpF+1 zXaI?-^m7L{bz{d9h0j%Qp}5Q zXTt+`97pr*XfEcBz{}-a9ggO6@w|9A_@2I#*8$2t-bm3r3qjVB%Zm$9)%bc;`MLt=d#n&lS8{bUxRvnn1oHWM0?-8 z`2$K*sAL;WRz*q48W^yD(2IXRhNUUz7I3=;_BnjK25B<~woM-Yca3F|nSkeaj3G0* zA=b*ryCDeb5I{2lOwd?%mnAQEP+<3~Ir7lJxmVMJua9L(QQiZTSGx6gau2#D73qg3 zQ(ieu^Hn=Us;4J8JpKwt1|^63RU~tJ={R5AF!b1$57hG!!!_5`FHQ!8!R;u;xDpFRp<}ngcwQt&t zS+1F`S+2WXv-gGrJ%7HK_2~M)HD6bU<|}Zd?amVFSPbFdW91(ev+f;{-Dp+y7(thY4+wJJeG*H(B?f3sU**UU9W@_H zMsF}HcI$gI`2aPSsNMOTNmvAFylN5~AgA--B$nHk4l9};l$w9wrSB>0tafap!TO^X zU7+muzO|xGxo-ncp3HjPY0jI6y!>Mt^8xrVx)_cLfzVykETb|gLB~}Nv>TP-aW<>| zH>oldYhcwLt!kHJGpdqWozL&Qi>371)W*l7e-E}|IIdmLPoneFQKIc$z<)i1-OTM% zm_vS_%hOpGZvn!#{@$4YWx=~B$edY2oQhGBs8A9vOcMov)Yb1WO#i?G02tmg*M>)9 zY({dwPt7*gt(sjelnim?)yA-V6^^XJ+|M*UTJu>n)UtC#8K(h#v=yfitew$zqRrFv ztk4hWXiTVnk?)STB=eI`vzr1fuXpg4^$Rb~vbul1?=k*@Y)Sl=I^WHM-Og{8?{+&c zsnwBgcC7sReNrATN#`x|UW$>vjE=(JnwY3x z#z-4tut-{4W#%+sSCek`kJAN?+*256z9%Jk|4hcB2GC;HXkFsDjo@gTeZ5j>OIv3d zY0L1lF}AF=Q3bY~$D&=fysDT2Tb<1Fcd%5iJ3bH8Pnum1Q-5^zDTWlCI-{v zY&kB$0~7`iOJTgkhqqYzD6d&~U8DSym}1*z#qvvqH}7Ch{=r-}@cNd3Xi1lo_|$o< zfAH~n?6f6Lj2PlO*#k{eQt^RiGlJ1 zshtyzR(p(Sl*G_b+?z=JZ`C>)&F9SK*Km(S&%?lHDmJ5nzgDyKi;g@;L3-Yi7h4Dj zC94hag8=DqoeY-S;*CCGmie7{Wi;y?EWC&Ppo*(HYx({2=iEDY@%+H6QQd;8e#Vw4 zyu&J%$;TwHZv2Zxr5hi!iY>wo+^ehDK{6$l8&J)O=FNcVD^2@QCLh^*T{JIw8)JzUy{s_?^?1`D?WxOR0?j2U^45Ub zn5vvKiEB*rv?p$~8Y+AmOPv%~9Eq6wIK7C#L)6*$g@@TJtOF9`S^o4VihI;ho`;r>l1|X^7Fj$5DrFHI*{^n zMw&j+UiofxWn^-RR6fhZw^zP@1U^#vLHsLBhB8z&reS0RS)}e`R9<|LrSP~%Sz3A^ zVt{Zysf*A=k~YN$nvbNUPmJI}&3Bp2v(@0@M{$Y>M3>ongjdgd<`zKu(l5MjdMdnJ zS5i9X5fV?hJ-l#SeD*ESn;T@_T@hp?GyLq2z$XEjVK%%Mfu~}4@q844C;BXtKFeIE z)J*(`oA_Ji#rbnU==;tYyRFvbq^0Wu5q| zdz558>o2T_JcMsq%cip2+wyX73hS@DtOfJ@xD>!8{{`b%U?k|Pzu1A^lyRRUh(tF{ z(~nb%o<%uw4khTw#E=9;98{_uFN3)4=rt;X(rTQrhz>W&2*00b2aME@RHH0%p;_=c z%Bh5N+HBxQiEV|_#5zKnxd-Lbvva?svtT)~v=d?>l`=H{D3RskVw>>3Nl4c9uRS`l z%L+5~uST-F3rj z{n9jl!Yyv+KDBlg1f2(H`^XyJV^NUcfF?}n7DZKc^KPb%z}TCW97&O zb~CVaP_is`EOhH%czTyQ7JBrv9{rT)Qe9v1Q;%;RGG|K>4pg${#PL9_o+vb!Xm>l` zS8IEr0!|s7K;+RESw}V$;J8JZ9p33I!#kX6$B;E7e(16swK|)+B7drCKuW$WD^!Dg z377cQq#*SFOszhS0&e;WrD1*5>VM;3KfyYA-Bp)mwPqVyH2;$h;sy<&r>Ue`{S-C9 z%|sI^jOkoys0@cOw5E(o^O;(2$38_`C;qtBmBf>juJI1X#L(GIBk_b6fCJ z73rj~Ty;8km{omjI*6|wAIM=1`EwzX#YQlV5z>O5?2*Tf^TzS0>7kYf94sW^U zY*MRMkT~aiYSj`H$jdf@Uh_REybS{`(VWNB>g5P6!oW<^4Uz)A)KMzhMxw1i?*;3( zHHKOcKj}!HdlU;xj0=tudJ7_qmn-V)a)&iYdxwU82OQ3E{LD*oXLT?}iOLQN4xuSm z0_%QA<3GG4#}58agX;I9;iWbJgw196JI=iuC^_HUSQ{+)K$AI_9&6++c-_v`Q!>-nB zAuq&y;wl7pH{T0&$YIm4(Ol%$NPwM6I-)8eT&sVJH=YGvnU3M+I1oGO4A?8FRE$9D>Ts1%-i|6=UBI!(Hcr5w+gS6Yg>hueZi2S z);_}zZ(x0Jf%xWgY!E=pb1b{B;H*ES;pF@#UW?_0iiL@d1t+!v&HqrbF(K3W9nWKT zxAUdXvzvGWXLf8dI7d%1Pe2|=4u9o&mN6H5unTq?m9P0|1J7>KbS$Ka#U>h;ZXTTL zfPS+@p(ap=7G0#NHRK9$n(v|f5WSTvt0OboJ=u6addbja1kd>m>+J0W_c0nZeGitr zTW>Jh$u$fodGivT?Iyv&{f1*01h2Ii=#uNu?H5hx{vT*1;a;amzv|9D>+$!=a?5z% za!zPohI8t=AY7}+Us{7L=gTwsv{KYJ9+rg^1cRD)(F}yrhM7FioumK1s#LxGAOy+uRxsA^FvO7YiH;l^Ki`MOBKt3%7+OwdUQ{nRMP zq4{%+k|i{a3KT@H$`zwE;419fqNy7f0dCl;N>zpM&u04K~Wq_ z!b2&;B@Op(vxI05I%gk^=@ai~JCT`4qAo zZ2=v!UcyESrL&BHzKqfk-0GrlSa^vM%cqcsV(S+vP>C3P=o2GkxvO}|uUI#4n}8X# z-;0R72oZa^EC%%oR3bqU0Xr;>c9L?+uU3k9{R^Lqb-OpH~aHRn{aXaC!?#e$?j`o-DbK{!*nan?BPd63=`qTjOd$C!8WKq zTGK9~SCW2aM5jTj-WoDe=nlBbjY9gnc<)vSK92?LxXQ$#H{#z5xblb3ufw zU5-kK3ulnn8S@lGRS^W7r`5;aB1ibY0>51ZeupD6?muC~eXAMw=S0N)TVU+Axc@#l z-XQJ|2Mw({tBCuV6!$wBb9BRq|AL74U95$xwsN z$OU>1NGo%zOAfRKK^ ziG;gqA1iX6tXK;2VGf}b_R(z~=Sj6@rbqK7c$2D|t$98tf5`8w7uHpjQ zYBHn8+Rn+AE`0YzOA0@!Ssd~?+?B{#eB@5tq#TIl%}4MOd-L0J6eydor)&YhgWG8hxwO&ogR3@RNGBDN6Z+ zY~24jxVHhBq+6vmmk2#6L<3w z2hjBi1H37KayCjdDv8lNA4E!Bw748yC@xl&2Mut^0N6*wmF{#a&o~TD_&9@Zc%QS1 zE}6!)>M#S0H^6-csEZXBgvv`M{4SPP;=)jgjx%7Y33E+Y6K80%)2x3TCoUkB%SK&J zj(CAA0~- zTGQ;pR{atM$Tz_66M4>CuxFwvy`#Z(5FpuiZbw1 z=S!Mcj`CAjzX?hVNDl63Vr5aX%DW$9{grZj?s5$Nn93I%V`JoA!L7#_k5L9E)AeZF zw%ZI_m&EclAG1NqspQC8$IR>MbWM5+qCdeMrj)`f3EGGAdV)o%xGx>9l&A81f=$DD zrGRO?o?yv{4S%NQbe>NzBcE3iZ0f@60Ru}Ou=3~=SlCS&JpTk5wt;|PP7hvBaA403 zp90M7$@2;3yLcs_WlK+q?{3AbU;2uOfAlHKRGNEk_zV==96X=kW>6B`;o$WIKXbH= zkKc3>^ru015)Ia$@JfO^T)duOPWFbs0Mzy8`2<@=@yfs8)^zaTK^fxKv@)J2oMNlS z7U}qM#jU?fAFsSGK8l{K=~s5=!L0NNOB&shT1Bj&n^5{J7^gBdF&_t`z4#u)PS_&- zB2I?<^Hc1$VJ^LX+78-SrWYc(bVCN4L7AyIl~2=j?6#w_#F^b?%>}!CX%QEa{QlGI zTQ3v=yZtzLP4xI(85+LZhE&am;!sys1ulnr=LqaVG6!J)1vLOx<^vTFYn((oT zA;mgl7JcF&J{|ch{GETdfWXJQ`*3BH1>YcD#2NG5&zU{77?vPg(o;`=j2EJTrF%Lh zyNy?%#S?7P9*?9WGpFeSyZEX9* zKgN8U$dAmmByoF&k`&m|XL9iEuh@DSeE77l*{$BSLYfh;Xp0T+tIZL3dczgQmzu_) z_!dbo4kW-4-X1|e!qk}Xc!k==XOn^FvFF)cE;<>|Lpb_{O+<+kJz;#ZFupv{ zzs$-1>pc6Fa#-Q3erp*l$MWsp;5)iE4&zly?Rb0JCxz`WeUB#w4xO?a*awzp@Tb4U z8xy=eiTDgLmjB5_(rcFV?^s&wG&9O;eCT&0yPu*#@3*yE4E{ukK-IlUjs^rk!fIL$(V;ds-fK*_XJ>L6Z+n~gs+;R?R> z5(0R@p#6mb&I-UE{1R4Z73I!0;hBoD($JTL($kY~`hg9}5dU?g-@N-4mr5({SzJ1A z&i$pcmoA@w|IcQZ&R;ygVt(LAU1xsxSr(ntt~vaFPP$u`thhI@wZqgv>1T;NVL!9+ zf4j`Y|3Vpm6A9`;fAF+-t$X?SD=fA1BSx!erZ<9NPyJtfE1LNuS6JHQoCumo@1&bN zddP%GMBDlz~#rD(CSPa@6j?EBD)jqm`%} eInZyJl|N#Ms#F%q*BmO}x}iDzrtzJ7WB(7fJ@N_w diff --git a/search/wildcard_query.go b/search/wildcard_query.go index a5d7e42..bcaf283 100644 --- a/search/wildcard_query.go +++ b/search/wildcard_query.go @@ -35,7 +35,7 @@ type WildcardQuery struct { // NewWildcardQuery will sanitize the provided query and store the safe version. func NewWildcardQuery(query string, caseSensitive bool) WildcardQuery { var cptr unsafe.Pointer - cleanQuery := C.wildcard_query_clean( + cleanQuery := C.wildcard_query_new( C.StringView{ (*C.char)(unsafe.Pointer(unsafe.StringData(query))), C.size_t(len(query)), From 99e9cea9789f0638d973e5c34f124fa2234a9090 Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 25 Apr 2024 18:56:40 -0400 Subject: [PATCH 13/27] Rename LogEvent to LogEventStorage. --- cpp/src/ffi_go/ir/types.hpp | 2 +- cpp/src/ffi_go/types.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/src/ffi_go/ir/types.hpp b/cpp/src/ffi_go/ir/types.hpp index 366f519..794707c 100644 --- a/cpp/src/ffi_go/ir/types.hpp +++ b/cpp/src/ffi_go/ir/types.hpp @@ -49,7 +49,7 @@ struct Encoder { * ir.Deserializer (without any warning or way to guard in Go). */ struct Deserializer { - ffi_go::LogEvent m_log_event; + ffi_go::LogEventStorage m_log_event; ffi::epoch_time_ms_t m_timestamp{}; }; diff --git a/cpp/src/ffi_go/types.hpp b/cpp/src/ffi_go/types.hpp index 16bc6a2..345d10e 100644 --- a/cpp/src/ffi_go/types.hpp +++ b/cpp/src/ffi_go/types.hpp @@ -16,7 +16,7 @@ using LogMessage = std::string; * Mutating a field will invalidate the corresponding View (slice) stored in the * ffi.LogEventView (without any warning or way to guard in Go). */ -struct LogEvent { +struct LogEventStorage { auto reserve(size_t cap) -> void { m_log_message.reserve(cap); } LogMessage m_log_message; From 06ea187ef6d3e5b548757046e06e188d50a36804 Mon Sep 17 00:00:00 2001 From: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> Date: Sun, 28 Apr 2024 21:41:05 -0400 Subject: [PATCH 14/27] Apply suggestions from code review --- cpp/src/ffi_go/ir/decoder.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp/src/ffi_go/ir/decoder.h b/cpp/src/ffi_go/ir/decoder.h index 32e5b51..9f30e52 100644 --- a/cpp/src/ffi_go/ir/decoder.h +++ b/cpp/src/ffi_go/ir/decoder.h @@ -67,7 +67,7 @@ extern "C" { * @param[in] vars Array of encoded variables * @param[in] dict_vars String containing all dictionary variables concatenated * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the * end of a dictionary variable * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log * message From 00b79040cfa324919632ac4e0d420a3a9198012c Mon Sep 17 00:00:00 2001 From: davidlion Date: Mon, 29 Apr 2024 11:28:33 -0400 Subject: [PATCH 15/27] Apply suggestions from code review Improves/rewrites/fixes many doc strings Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> --- ir/deserializer.go | 22 +++++++++++++++++++++- ir/ir.go | 16 ++++++++-------- ir/serializer.go | 6 +++--- ir/writer.go | 3 ++- 4 files changed, 34 insertions(+), 13 deletions(-) diff --git a/ir/deserializer.go b/ir/deserializer.go index ce4c1c3..8b5bd81 100644 --- a/ir/deserializer.go +++ b/ir/deserializer.go @@ -17,7 +17,7 @@ import ( ) // A Deserializer exports functions to deserialize log events from a CLP IR byte -// stream. Deserializatoin functions take an IR buffer as input, but how that +// stream. Deserialization functions take an IR buffer as input, but how that // buffer is materialized is left to the user. These functions return views // (slices) of the log events extracted from the IR. Each Deserializer owns its // own unique underlying memory for the views it produces/returns. This memory @@ -173,6 +173,16 @@ func (self *fourByteDeserializer) DeserializeLogEvent( return deserializeLogEvent(self, irBuf) } +// DeserializeWildcardMatch attempts to read the next log event from the IR +// stream in irBuf that matches mergedQuery within timeInterval. It returns the +// deserialized [ffi.LogEventView], the position read to in irBuf (the end of +// the log event in irBuf), the index of the matched query in mergedQuery, +// and an error. On error returns: +// - nil *ffi.LogEventView +// - 0 position +// - -1 index +// - [IrError] error: CLP failed to successfully deserialize +// - [EndOfIr] error: CLP found the IR stream EOF tag func (self *eightByteDeserializer) DeserializeWildcardMatch( irBuf []byte, timeInterval search.TimestampInterval, @@ -181,6 +191,16 @@ func (self *eightByteDeserializer) DeserializeWildcardMatch( return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) } +// DeserializeWildcardMatch attempts to read the next log event from the IR +// stream in irBuf that matches mergedQuery within timeInterval. It returns the +// deserialized [ffi.LogEventView], the position read to in irBuf (the end of +// the log event in irBuf), the index of the matched query in mergedQuery, +// and an error. On error returns: +// - nil *ffi.LogEventView +// - 0 position +// - -1 index +// - [IrError] error: CLP failed to successfully deserialize +// - [EndOfIr] error: CLP found the IR stream EOF tag func (self *fourByteDeserializer) DeserializeWildcardMatch( irBuf []byte, timeInterval search.TimestampInterval, diff --git a/ir/ir.go b/ir/ir.go index 4e40b53..edd31dd 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -29,10 +29,10 @@ type TimestampInfo struct { TimeZoneId string } -// ir.BufView is a slice of CLP IR backed by C++ allocated memory rather than -// the Go heap. A BufView, x, is valid when returned and will remain valid until -// a new BufView is returned by the same object (e.g. an [ir.Serializer]) that -// retuend x. +// ir.BufView represents a slice of CLP IR, utilizing memory allocated by C++ +// instead of the Go heap. A BufView, denoted as x, is valid upon being returned +// and maintains its validity until the same object (e.g., an [ir.Serializer]) +// that issued x returns a new BufView. type BufView = []byte // A ir.LogMessage contains all the different components of a log message @@ -44,10 +44,10 @@ type LogMessage[T EightByteEncoding | FourByteEncoding] struct { DictVarEndOffsets []int32 } -// A ir.LogMessageView is a [ir.LogMessage] that is backed by C++ allocated -// memory rather than the Go heap. A LogMessageView, x, is valid when returned -// and will remain valid until a new LogMessageView is returned by the same -// object (e.g. an [ir.Encoder]) that retuend x. +// ir.LogMessageView is a [ir.LogMessage] using memory allocated by C++ instead +// of the Go heap. A LogMessageView, denoted as x, is valid upon being returned +// and maintains its validity until the same object (e.g., an [ir.Encoder]) +// that issued x returns a new LogMessageView. type LogMessageView[T EightByteEncoding | FourByteEncoding] struct { LogMessage[T] } diff --git a/ir/serializer.go b/ir/serializer.go index 85de39c..67a80b6 100644 --- a/ir/serializer.go +++ b/ir/serializer.go @@ -92,8 +92,8 @@ type commonSerializer struct { cptr unsafe.Pointer } -// Close will delete the underlying C++ allocated memory used by the -// deserializer. Failure to call Close will result in a memory leak. +// Closes the serializer by releasing the underlying C++ allocated memory. +// Failure to call Close will result in a memory leak. func (self *commonSerializer) Close() error { if nil != self.cptr { C.ir_serializer_close(self.cptr) @@ -111,7 +111,7 @@ type eightByteSerializer struct { commonSerializer } -// SerializeLogEvent attempts to serialize the log event, event, into a eight +// SerializeLogEvent attempts to serialize the log event, event, into an eight // byte encoded CLP IR byte stream. On error returns: // - a nil BufView // - [IrError] based on the failure of the Cgo call diff --git a/ir/writer.go b/ir/writer.go index e3f6d11..8500694 100644 --- a/ir/writer.go +++ b/ir/writer.go @@ -125,7 +125,8 @@ func (self *Writer) Write(event ffi.LogEvent) (int, error) { // [self.Bytes] and [self.Reset] to manually handle the buffer's contents before // continuing. Returns: // - success: number of bytes written, nil -// - error: number of bytes written, error propagated from [bytes.Buffer.WriteTo] +// - error: number of bytes written, error propagated from +// [bytes.Buffer.WriteTo] func (self *Writer) WriteTo(w io.Writer) (int64, error) { n, err := self.buf.WriteTo(w) if nil == err { From c6b9ec581b7405fd38355c6c3676bf1d8172f2dd Mon Sep 17 00:00:00 2001 From: LinZhihao-723 Date: Mon, 29 Apr 2024 16:35:57 -0400 Subject: [PATCH 16/27] Refactor... --- cpp/src/ffi_go/ir/deserializer.cpp | 13 ++++++++----- cpp/src/ffi_go/search/wildcard_query.cpp | 3 ++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index b24cd86..f4bdaf3 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -119,10 +119,12 @@ auto deserialize_wildcard_match( std::string_view const query_view{merged_query.m_queries.m_data, merged_query.m_queries.m_size}; std::span const end_offsets{ merged_query.m_end_offsets.m_data, - merged_query.m_end_offsets.m_size}; + merged_query.m_end_offsets.m_size + }; std::span const case_sensitivity{ merged_query.m_case_sensitivity.m_data, - merged_query.m_case_sensitivity.m_size}; + merged_query.m_case_sensitivity.m_size + }; std::vector> queries(merged_query.m_end_offsets.m_size); size_t pos{0}; @@ -184,7 +186,8 @@ auto deserialize_wildcard_match( continue; } auto const [has_matching_query, matching_query_idx]{ - query_fn(deserializer->m_log_event.m_log_message)}; + query_fn(deserializer->m_log_event.m_log_message) + }; if (false == has_matching_query) { continue; } @@ -233,8 +236,8 @@ extern "C" auto ir_deserializer_new_deserializer_with_preamble( } *ir_encoding = four_byte_encoding ? 1 : 0; - if (IRErrorCode const err{ - decode_preamble(ir_buf, *metadata_type, *metadata_pos, *metadata_size)}; + if (IRErrorCode const err{decode_preamble(ir_buf, *metadata_type, *metadata_pos, *metadata_size) + }; IRErrorCode_Success != err) { return static_cast(err); diff --git a/cpp/src/ffi_go/search/wildcard_query.cpp b/cpp/src/ffi_go/search/wildcard_query.cpp index 2d707bd..28d9dc8 100644 --- a/cpp/src/ffi_go/search/wildcard_query.cpp +++ b/cpp/src/ffi_go/search/wildcard_query.cpp @@ -10,7 +10,8 @@ namespace ffi_go::search { extern "C" auto wildcard_query_new(StringView query, void** ptr) -> StringView { auto* clean{new std::string{ - clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size})}}; + clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size}) + }}; *ptr = clean; return {clean->data(), clean->size()}; } From 8f7802c371f56995cc993bd27965f4389ddd8254 Mon Sep 17 00:00:00 2001 From: LinZhihao-723 Date: Mon, 29 Apr 2024 17:10:19 -0400 Subject: [PATCH 17/27] Using api decoration. --- cpp/src/ffi_go/api_decoration.h | 21 ++ cpp/src/ffi_go/defs.h | 110 ++++----- cpp/src/ffi_go/ir/decoder.cpp | 9 +- cpp/src/ffi_go/ir/decoder.h | 143 ++++++------ cpp/src/ffi_go/ir/deserializer.cpp | 13 +- cpp/src/ffi_go/ir/deserializer.h | 281 +++++++++++------------ cpp/src/ffi_go/ir/encoder.cpp | 13 +- cpp/src/ffi_go/ir/encoder.h | 159 ++++++------- cpp/src/ffi_go/ir/serializer.cpp | 11 +- cpp/src/ffi_go/ir/serializer.h | 189 ++++++++------- cpp/src/ffi_go/search/wildcard_query.cpp | 7 +- cpp/src/ffi_go/search/wildcard_query.h | 107 ++++----- 12 files changed, 523 insertions(+), 540 deletions(-) create mode 100644 cpp/src/ffi_go/api_decoration.h diff --git a/cpp/src/ffi_go/api_decoration.h b/cpp/src/ffi_go/api_decoration.h new file mode 100644 index 0000000..897dcf3 --- /dev/null +++ b/cpp/src/ffi_go/api_decoration.h @@ -0,0 +1,21 @@ +#ifndef FFI_GO_API_DECORATION_H +#define FFI_GO_API_DECORATION_H + +/** + * If the file is compiled with a C++ compiler, `extern "C"` must be defined to + * ensure C linkage. + */ +#ifdef __cplusplus +#define _CLP_FFI_GO_EXTERN_C extern "C" +#else +#define _CLP_FFI_GO_EXTERN_C +#endif + +/** + * `CLP_FFI_GO_METHOD` should be added at the beginning of a function's + * declaration/implementation to decorate any APIs that are exposed to the + * Golang layer. + */ +#define CLP_FFI_GO_METHOD _CLP_FFI_GO_EXTERN_C + +#endif diff --git a/cpp/src/ffi_go/defs.h b/cpp/src/ffi_go/defs.h index 17f2a4f..563c89a 100644 --- a/cpp/src/ffi_go/defs.h +++ b/cpp/src/ffi_go/defs.h @@ -4,76 +4,68 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include - // TODO: replace with clp c-compatible header once it exists - typedef int64_t epoch_time_ms_t; - - /** - * A span of a bool array passed down through Cgo. - */ - typedef struct { - bool* m_data; - size_t m_size; - } BoolSpan; +// TODO: replace with clp c-compatible header once it exists +typedef int64_t epoch_time_ms_t; - /** - * A span of a byte array passed down through Cgo. - */ - typedef struct { - void* m_data; - size_t m_size; - } ByteSpan; +/** + * A span of a bool array passed down through Cgo. + */ +typedef struct { + bool* m_data; + size_t m_size; +} BoolSpan; - /** - * A span of a Go int32 array passed down through Cgo. - */ - typedef struct { - int32_t* m_data; - size_t m_size; - } Int32tSpan; +/** + * A span of a byte array passed down through Cgo. + */ +typedef struct { + void* m_data; + size_t m_size; +} ByteSpan; - /** - * A span of a Go int64 array passed down through Cgo. - */ - typedef struct { - int64_t* m_data; - size_t m_size; - } Int64tSpan; +/** + * A span of a Go int32 array passed down through Cgo. + */ +typedef struct { + int32_t* m_data; + size_t m_size; +} Int32tSpan; - /** - * A span of a Go int/C.size_t array passed down through Cgo. - */ - typedef struct { - size_t* m_data; - size_t m_size; - } SizetSpan; +/** + * A span of a Go int64 array passed down through Cgo. + */ +typedef struct { + int64_t* m_data; + size_t m_size; +} Int64tSpan; - /** - * A view of a Go string passed down through Cgo. - */ - typedef struct { - char const* m_data; - size_t m_size; - } StringView; +/** + * A span of a Go int/C.size_t array passed down through Cgo. + */ +typedef struct { + size_t* m_data; + size_t m_size; +} SizetSpan; - /** - * A view of a Go ffi.LogEvent passed down through Cgo. - */ - typedef struct { - StringView m_log_message; - epoch_time_ms_t m_timestamp; - } LogEventView; +/** + * A view of a Go string passed down through Cgo. + */ +typedef struct { + char const* m_data; + size_t m_size; +} StringView; -#ifdef __cplusplus -} -#endif +/** + * A view of a Go ffi.LogEvent passed down through Cgo. + */ +typedef struct { + StringView m_log_message; + epoch_time_ms_t m_timestamp; +} LogEventView; // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-deprecated-headers) diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index fd2a281..2e22297 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -60,16 +61,16 @@ template } } // namespace -extern "C" auto ir_decoder_new() -> void* { +CLP_FFI_GO_METHOD auto ir_decoder_new() -> void* { return new Decoder{}; } -extern "C" auto ir_decoder_close(void* ir_decoder) -> void { +CLP_FFI_GO_METHOD auto ir_decoder_close(void* ir_decoder) -> void { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete static_cast(ir_decoder); } -extern "C" auto ir_decoder_decode_eight_byte_log_message( +CLP_FFI_GO_METHOD auto ir_decoder_decode_eight_byte_log_message( StringView logtype, Int64tSpan vars, StringView dict_vars, @@ -87,7 +88,7 @@ extern "C" auto ir_decoder_decode_eight_byte_log_message( ); } -extern "C" auto ir_decoder_decode_four_byte_log_message( +CLP_FFI_GO_METHOD auto ir_decoder_decode_four_byte_log_message( StringView logtype, Int32tSpan vars, StringView dict_vars, diff --git a/cpp/src/ffi_go/ir/decoder.h b/cpp/src/ffi_go/ir/decoder.h index 9f30e52..5d003be 100644 --- a/cpp/src/ffi_go/ir/decoder.h +++ b/cpp/src/ffi_go/ir/decoder.h @@ -8,86 +8,79 @@ #include #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. + * @return New ir::Decoder's address + */ +CLP_FFI_GO_METHOD void* ir_decoder_new(); - /** - * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. - * @return New ir::Decoder's address - */ - void* ir_decoder_new(); +/** + * Clean up the underlying ir::Decoder of a Go ir.Decoder. + * @param[in] ir_encoder Address of a ir::Decoder created and returned by + * ir_decoder_new + */ +CLP_FFI_GO_METHOD void ir_decoder_close(void* decoder); - /** - * Clean up the underlying ir::Decoder of a Go ir.Decoder. - * @param[in] ir_encoder Address of a ir::Decoder created and returned by - * ir_decoder_new - */ - void ir_decoder_close(void* decoder); +/** + * Given the fields of a CLP IR encoded log message with eight byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); - /** - * Given the fields of a CLP IR encoded log message with eight byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_decoder_decode_eight_byte_log_message( - StringView logtype, - Int64tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message - ); - - /** - * Given the fields of a CLP IR encoded log message with four byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_decoder_decode_four_byte_log_message( - StringView logtype, - Int32tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message - ); - -#ifdef __cplusplus -} -#endif +/** + * Given the fields of a CLP IR encoded log message with four byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index f4bdaf3..4207783 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -205,12 +206,12 @@ auto deserialize_wildcard_match( } } // namespace -extern "C" auto ir_deserializer_close(void* ir_deserializer) -> void { +CLP_FFI_GO_METHOD auto ir_deserializer_close(void* ir_deserializer) -> void { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete static_cast(ir_deserializer); } -extern "C" auto ir_deserializer_new_deserializer_with_preamble( +CLP_FFI_GO_METHOD auto ir_deserializer_new_deserializer_with_preamble( ByteSpan ir_view, size_t* ir_pos, int8_t* ir_encoding, @@ -254,7 +255,7 @@ extern "C" auto ir_deserializer_new_deserializer_with_preamble( return static_cast(IRErrorCode_Success); } -extern "C" auto ir_deserializer_deserialize_eight_byte_log_event( +CLP_FFI_GO_METHOD auto ir_deserializer_deserialize_eight_byte_log_event( ByteSpan ir_view, void* ir_deserializer, size_t* ir_pos, @@ -268,7 +269,7 @@ extern "C" auto ir_deserializer_deserialize_eight_byte_log_event( ); } -extern "C" auto ir_deserializer_deserialize_four_byte_log_event( +CLP_FFI_GO_METHOD auto ir_deserializer_deserialize_four_byte_log_event( ByteSpan ir_view, void* ir_deserializer, size_t* ir_pos, @@ -282,7 +283,7 @@ extern "C" auto ir_deserializer_deserialize_four_byte_log_event( ); } -extern "C" auto ir_deserializer_deserialize_eight_byte_wildcard_match( +CLP_FFI_GO_METHOD auto ir_deserializer_deserialize_eight_byte_wildcard_match( ByteSpan ir_view, void* ir_deserializer, TimestampInterval time_interval, @@ -302,7 +303,7 @@ extern "C" auto ir_deserializer_deserialize_eight_byte_wildcard_match( ); } -extern "C" auto ir_deserializer_deserialize_four_byte_wildcard_match( +CLP_FFI_GO_METHOD auto ir_deserializer_deserialize_four_byte_wildcard_match( ByteSpan ir_view, void* ir_deserializer, TimestampInterval time_interval, diff --git a/cpp/src/ffi_go/ir/deserializer.h b/cpp/src/ffi_go/ir/deserializer.h index ba8fcc9..71ae1ab 100644 --- a/cpp/src/ffi_go/ir/deserializer.h +++ b/cpp/src/ffi_go/ir/deserializer.h @@ -7,158 +7,151 @@ #include #include +#include #include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. + * @param[in] ir_deserializer The address of a ir::Deserializer created and + * returned by ir_deserializer_new_deserializer_with_preamble + */ +CLP_FFI_GO_METHOD void ir_deserializer_close(void* ir_deserializer); - /** - * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. - * @param[in] ir_deserializer The address of a ir::Deserializer created and - * returned by ir_deserializer_new_deserializer_with_preamble - */ - void ir_deserializer_close(void* ir_deserializer); +/** + * Given a CLP IR buffer (any encoding), attempt to deserialize a preamble and + * extract its information. An ir::Deserializer will be allocated to use as the + * backing storage for a Go ir.Deserializer (i.e. subsequent calls to + * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read + * the metadata based on the returned type. All pointer parameters must be + * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[out] ir_pos Position in ir_view read to + * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) + * @param[out] metadata_type Type of metadata in preamble (e.g. json) + * @param[out] metadata_pos Position in ir_view where the metadata begins + * @param[out] metadata_size Size of the metadata (in bytes) + * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer + * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer + * to be filled in by Go using the metadata contents + * @return ffi::ir_stream::IRErrorCode forwarded from either + * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble + */ +CLP_FFI_GO_METHOD int ir_deserializer_new_deserializer_with_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr +); - /** - * Given a CLP IR buffer (any encoding), attempt to deserialize a preamble and - * extract its information. An ir::Deserializer will be allocated to use as the - * backing storage for a Go ir.Deserializer (i.e. subsequent calls to - * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read - * the metadata based on the returned type. All pointer parameters must be - * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[out] ir_pos Position in ir_view read to - * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) - * @param[out] metadata_type Type of metadata in preamble (e.g. json) - * @param[out] metadata_pos Position in ir_view where the metadata begins - * @param[out] metadata_size Size of the metadata (in bytes) - * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer - * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer - * to be filled in by Go using the metadata contents - * @return ffi::ir_stream::IRErrorCode forwarded from either - * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble - */ - int ir_deserializer_new_deserializer_with_preamble( - ByteSpan ir_view, - size_t* ir_pos, - int8_t* ir_encoding, - int8_t* metadata_type, - size_t* metadata_pos, - uint16_t* metadata_size, - void** ir_deserializer_ptr, - void** timestamp_ptr - ); +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::decode_next_message + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); - /** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::decode_next_message - */ - int ir_deserializer_deserialize_eight_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event - ); +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); - /** - * Given a CLP IR buffer with four byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - */ - int ir_deserializer_deserialize_four_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event - ); +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event until finding an event that is both within the time interval and + * matches any query. If queries is empty, the first log event within the time + * interval is treated as a match. Returns the components of the found log event + * and the buffer position it ends at. All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the first matching query or + * 0 if queries is empty + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); - /** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event until finding an event that is both within the time interval and - * matches any query. If queries is empty, the first log event within the time - * interval is treated as a match. Returns the components of the found log event - * and the buffer position it ends at. All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the first matching query or - * 0 if queries is empty - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ - int ir_deserializer_deserialize_eight_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query - ); - - /** - * Given a CLP IR buffer with four byte encoding, deserialize the next log event - * until finding an event that is both within the time interval and matches any - * query. If queries is empty, the first log event within the time interval is - * treated as a match. Returns the components of the found log event and the - * buffer position it ends at. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the matching query - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ - int ir_deserializer_deserialize_four_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query - ); - -#ifdef __cplusplus -} -#endif +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log event + * until finding an event that is both within the time interval and matches any + * query. If queries is empty, the first log event within the time interval is + * treated as a match. Returns the components of the found log event and the + * buffer position it ends at. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the matching query + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index fa722d1..77d5167 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -83,25 +84,25 @@ auto encode_log_message( } } // namespace -extern "C" auto ir_encoder_eight_byte_new() -> void* { +CLP_FFI_GO_METHOD auto ir_encoder_eight_byte_new() -> void* { return new Encoder{}; } -extern "C" auto ir_encoder_four_byte_new() -> void* { +CLP_FFI_GO_METHOD auto ir_encoder_four_byte_new() -> void* { return new Encoder{}; } -extern "C" auto ir_encoder_eight_byte_close(void* ir_encoder) -> void { +CLP_FFI_GO_METHOD auto ir_encoder_eight_byte_close(void* ir_encoder) -> void { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete static_cast*>(ir_encoder); } -extern "C" auto ir_encoder_four_byte_close(void* ir_encoder) -> void { +CLP_FFI_GO_METHOD auto ir_encoder_four_byte_close(void* ir_encoder) -> void { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete static_cast*>(ir_encoder); } -extern "C" auto ir_encoder_encode_eight_byte_log_message( +CLP_FFI_GO_METHOD auto ir_encoder_encode_eight_byte_log_message( StringView log_message, void* ir_encoder, StringView* logtype, @@ -119,7 +120,7 @@ extern "C" auto ir_encoder_encode_eight_byte_log_message( ); } -extern "C" auto ir_encoder_encode_four_byte_log_message( +CLP_FFI_GO_METHOD auto ir_encoder_encode_four_byte_log_message( StringView log_message, void* ir_encoder, StringView* logtype, diff --git a/cpp/src/ffi_go/ir/encoder.h b/cpp/src/ffi_go/ir/encoder.h index 1737ada..d1ae99e 100644 --- a/cpp/src/ffi_go/ir/encoder.h +++ b/cpp/src/ffi_go/ir/encoder.h @@ -8,96 +8,89 @@ #include #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. + * @return New ir::Encoder's address + */ +CLP_FFI_GO_METHOD void* ir_encoder_eight_byte_new(); - /** - * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. - * @return New ir::Encoder's address - */ - void* ir_encoder_eight_byte_new(); +/** + * @copydoc ir_encoder_eight_byte_new() + */ +CLP_FFI_GO_METHOD void* ir_encoder_four_byte_new(); - /** - * @copydoc ir_encoder_eight_byte_new() - */ - void* ir_encoder_four_byte_new(); +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_eight_byte_new + */ +CLP_FFI_GO_METHOD void ir_encoder_eight_byte_close(void* ir_encoder); - /** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_eight_byte_new - */ - void ir_encoder_eight_byte_close(void* ir_encoder); +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_four_byte_new + */ +CLP_FFI_GO_METHOD void ir_encoder_four_byte_close(void* ir_encoder); - /** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_four_byte_new - */ - void ir_encoder_four_byte_close(void* ir_encoder); +/** + * Given a log message, encode it into a CLP IR object with eight byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); - /** - * Given a log message, encode it into a CLP IR object with eight byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_encoder_encode_eight_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int64tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets - ); - - /** - * Given a log message, encode it into a CLP IR object with four byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_encoder_encode_four_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int32tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets - ); - -#ifdef __cplusplus -} -#endif +/** + * Given a log message, encode it into a CLP IR object with four byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/cpp/src/ffi_go/ir/serializer.cpp b/cpp/src/ffi_go/ir/serializer.cpp index 81d97d6..7236a65 100644 --- a/cpp/src/ffi_go/ir/serializer.cpp +++ b/cpp/src/ffi_go/ir/serializer.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -130,12 +131,12 @@ auto serialize_log_event( } } // namespace -extern "C" auto ir_serializer_close(void* ir_serializer) -> void { +CLP_FFI_GO_METHOD auto ir_serializer_close(void* ir_serializer) -> void { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete static_cast(ir_serializer); } -extern "C" auto ir_serializer_new_eight_byte_serializer_with_preamble( +CLP_FFI_GO_METHOD auto ir_serializer_new_eight_byte_serializer_with_preamble( StringView ts_pattern, StringView ts_pattern_syntax, StringView time_zone_id, @@ -152,7 +153,7 @@ extern "C" auto ir_serializer_new_eight_byte_serializer_with_preamble( ); } -extern "C" auto ir_serializer_new_four_byte_serializer_with_preamble( +CLP_FFI_GO_METHOD auto ir_serializer_new_four_byte_serializer_with_preamble( StringView ts_pattern, StringView ts_pattern_syntax, StringView time_zone_id, @@ -170,7 +171,7 @@ extern "C" auto ir_serializer_new_four_byte_serializer_with_preamble( ); } -extern "C" auto ir_serializer_serialize_eight_byte_log_event( +CLP_FFI_GO_METHOD auto ir_serializer_serialize_eight_byte_log_event( StringView log_message, epoch_time_ms_t timestamp, void* ir_serializer, @@ -184,7 +185,7 @@ extern "C" auto ir_serializer_serialize_eight_byte_log_event( ); } -extern "C" auto ir_serializer_serialize_four_byte_log_event( +CLP_FFI_GO_METHOD auto ir_serializer_serialize_four_byte_log_event( StringView log_message, epoch_time_ms_t timestamp_delta, void* ir_serializer, diff --git a/cpp/src/ffi_go/ir/serializer.h b/cpp/src/ffi_go/ir/serializer.h index 632089f..bd02f8c 100644 --- a/cpp/src/ffi_go/ir/serializer.h +++ b/cpp/src/ffi_go/ir/serializer.h @@ -7,110 +7,103 @@ #include #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Clean up the underlying ir::Serializer of a Go ir.Serializer. + * @param[in] ir_serializer Address of a ir::Serializer created and returned by + * ir_serializer_serialize_*_preamble + */ +CLP_FFI_GO_METHOD void ir_serializer_close(void* ir_serializer); - /** - * Clean up the underlying ir::Serializer of a Go ir.Serializer. - * @param[in] ir_serializer Address of a ir::Serializer created and returned by - * ir_serializer_serialize_*_preamble - */ - void ir_serializer_close(void* ir_serializer); +/** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with eight byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_preamble + */ +CLP_FFI_GO_METHOD int ir_serializer_new_eight_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view +); - /** - * Given the fields of a CLP IR preamble, serialize them into an IR byte stream - * with eight byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::encode_preamble - */ - int ir_serializer_new_eight_byte_serializer_with_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - void** ir_serializer_ptr, - ByteSpan* ir_view - ); +/** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with four byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_preamble + */ +CLP_FFI_GO_METHOD int ir_serializer_new_four_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +); - /** - * Given the fields of a CLP IR preamble, serialize them into an IR byte stream - * with four byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::encode_preamble - */ - int ir_serializer_new_four_byte_serializer_with_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - epoch_time_ms_t reference_ts, - void** ir_serializer_ptr, - ByteSpan* ir_view - ); +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * eight byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message of the log event to serialize + * @param[in] timestamp Timestamp of the log event to serialize + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_message + */ +CLP_FFI_GO_METHOD int ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view +); - /** - * Given the fields of a log event, serialize them into an IR byte stream with - * eight byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message of the log event to serialize - * @param[in] timestamp Timestamp of the log event to serialize - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::encode_message - */ - int ir_serializer_serialize_eight_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp, - void* ir_serializer, - ByteSpan* ir_view - ); - - /** - * Given the fields of a log event, serialize them into an IR byte stream with - * four byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to serialize - * @param[in] timestamp_delta Timestamp delta to the previous log event in the - * IR stream - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::encode_message - */ - int ir_serializer_serialize_four_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp_delta, - void* ir_serializer, - ByteSpan* ir_view - ); - -#ifdef __cplusplus -} -#endif +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * four byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to serialize + * @param[in] timestamp_delta Timestamp delta to the previous log event in the + * IR stream + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_message + */ +CLP_FFI_GO_METHOD int ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view +); // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/cpp/src/ffi_go/search/wildcard_query.cpp b/cpp/src/ffi_go/search/wildcard_query.cpp index 28d9dc8..8032720 100644 --- a/cpp/src/ffi_go/search/wildcard_query.cpp +++ b/cpp/src/ffi_go/search/wildcard_query.cpp @@ -5,10 +5,11 @@ #include +#include #include namespace ffi_go::search { -extern "C" auto wildcard_query_new(StringView query, void** ptr) -> StringView { +CLP_FFI_GO_METHOD auto wildcard_query_new(StringView query, void** ptr) -> StringView { auto* clean{new std::string{ clean_up_wildcard_search_string(std::string_view{query.m_data, query.m_size}) }}; @@ -16,12 +17,12 @@ extern "C" auto wildcard_query_new(StringView query, void** ptr) -> StringView { return {clean->data(), clean->size()}; } -extern "C" auto wildcard_query_delete(void* str) -> void { +CLP_FFI_GO_METHOD auto wildcard_query_delete(void* str) -> void { // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) delete static_cast(str); } -extern "C" auto wildcard_query_match(StringView target, WildcardQueryView query) -> int { +CLP_FFI_GO_METHOD auto wildcard_query_match(StringView target, WildcardQueryView query) -> int { return static_cast(wildcard_match_unsafe( {target.m_data, target.m_size}, {query.m_query.m_data, query.m_query.m_size}, diff --git a/cpp/src/ffi_go/search/wildcard_query.h b/cpp/src/ffi_go/search/wildcard_query.h index 3adcc5d..960beb1 100644 --- a/cpp/src/ffi_go/search/wildcard_query.h +++ b/cpp/src/ffi_go/search/wildcard_query.h @@ -7,70 +7,63 @@ #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * A timestamp interval of [m_lower, m_upper). + */ +typedef struct { + epoch_time_ms_t m_lower; + epoch_time_ms_t m_upper; +} TimestampInterval; - /** - * A timestamp interval of [m_lower, m_upper). - */ - typedef struct { - epoch_time_ms_t m_lower; - epoch_time_ms_t m_upper; - } TimestampInterval; +/** + * A view of a wildcard query passed down from Go. The query string is assumed + * to have been cleaned using the CLP function `clean_up_wildcard_search_string` + * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case + * insensitive). + */ +typedef struct { + StringView m_query; + bool m_case_sensitive; +} WildcardQueryView; - /** - * A view of a wildcard query passed down from Go. The query string is assumed - * to have been cleaned using the CLP function `clean_up_wildcard_search_string` - * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case - * insensitive). - */ - typedef struct { - StringView m_query; - bool m_case_sensitive; - } WildcardQueryView; +/** + * A view of a Go search.MergedWildcardQuery passed down through Cgo. The + * string is a concatenation of all wildcard queries, while m_end_offsets stores + * the size of each query. + */ +typedef struct { + StringView m_queries; + SizetSpan m_end_offsets; + BoolSpan m_case_sensitivity; +} MergedWildcardQueryView; - /** - * A view of a Go search.MergedWildcardQuery passed down through Cgo. The - * string is a concatenation of all wildcard queries, while m_end_offsets stores - * the size of each query. - */ - typedef struct { - StringView m_queries; - SizetSpan m_end_offsets; - BoolSpan m_case_sensitivity; - } MergedWildcardQueryView; +/** + * Given a query string, allocate and return a clean string that is safe for + * matching. See `clean_up_wildcard_search_string` in CLP for more details. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ +CLP_FFI_GO_METHOD StringView wildcard_query_new(StringView query, void** ptr); - /** - * Given a query string, allocate and return a clean string that is safe for - * matching. See `clean_up_wildcard_search_string` in CLP for more details. - * @param[in] query Query string to clean - * @param[in] ptr Address of a new std::string - * @return New string holding cleaned query - */ - StringView wildcard_query_new(StringView query, void** ptr); +/** + * Delete a std::string holding a wildcard query. + * @param[in] str Address of a std::string created and returned by + * clean_wildcard_query + */ +CLP_FFI_GO_METHOD void wildcard_query_delete(void* str); - /** - * Delete a std::string holding a wildcard query. - * @param[in] str Address of a std::string created and returned by - * clean_wildcard_query - */ - void wildcard_query_delete(void* str); - - /** - * Given a target string perform CLP wildcard matching using query. See - * `wildcard_match_unsafe` in CLP src/string_utils.hpp. - * @param[in] target String to perform matching on - * @param[in] query Query to use for matching - * @return 1 if query matches target, 0 otherwise - */ - int wildcard_query_match(StringView target, WildcardQueryView query); - -#ifdef __cplusplus -} -#endif +/** + * Given a target string perform CLP wildcard matching using query. See + * `wildcard_match_unsafe` in CLP src/string_utils.hpp. + * @param[in] target String to perform matching on + * @param[in] query Query to use for matching + * @return 1 if query matches target, 0 otherwise + */ +CLP_FFI_GO_METHOD int wildcard_query_match(StringView target, WildcardQueryView query); // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) From bc6fca926e76938bb2a6f01d7f65b472a8c9c235 Mon Sep 17 00:00:00 2001 From: LinZhihao-723 Date: Mon, 29 Apr 2024 17:33:39 -0400 Subject: [PATCH 18/27] Apply clang-tidy suggestions on src/ffi_go/ir/* --- cpp/src/ffi_go/ir/decoder.cpp | 10 ++++------ cpp/src/ffi_go/ir/deserializer.cpp | 6 +++--- cpp/src/ffi_go/ir/encoder.cpp | 12 ++++++------ cpp/src/ffi_go/ir/serializer.cpp | 4 ---- cpp/src/ffi_go/ir/types.hpp | 3 ++- 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/cpp/src/ffi_go/ir/decoder.cpp b/cpp/src/ffi_go/ir/decoder.cpp index 2e22297..3f47ee3 100644 --- a/cpp/src/ffi_go/ir/decoder.cpp +++ b/cpp/src/ffi_go/ir/decoder.cpp @@ -1,10 +1,8 @@ #include "decoder.h" -#include -#include #include #include -#include +#include #include #include @@ -12,7 +10,6 @@ #include #include #include -#include namespace ffi_go::ir { using namespace ffi::ir_stream; @@ -30,10 +27,10 @@ template void* ir_decoder, StringView* log_msg_view ) -> int { - using encoded_var_t = typename std::conditional< + using encoded_var_t = std::conditional_t< std::is_same_v, ffi::eight_byte_encoded_variable_t, - ffi::four_byte_encoded_variable_t>::type; + ffi::four_byte_encoded_variable_t>; if (nullptr == ir_decoder || nullptr == log_msg_view) { return static_cast(IRErrorCode_Corrupted_IR); } @@ -62,6 +59,7 @@ template } // namespace CLP_FFI_GO_METHOD auto ir_decoder_new() -> void* { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) return new Decoder{}; } diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index 4207783..81f450a 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -4,17 +4,17 @@ #include #include #include -#include #include -#include #include #include +#include +#include #include #include #include #include -#include +#include #include #include diff --git a/cpp/src/ffi_go/ir/encoder.cpp b/cpp/src/ffi_go/ir/encoder.cpp index 77d5167..073e5f8 100644 --- a/cpp/src/ffi_go/ir/encoder.cpp +++ b/cpp/src/ffi_go/ir/encoder.cpp @@ -1,8 +1,7 @@ #include "encoder.h" -#include -#include -#include +#include +#include #include #include #include @@ -13,7 +12,6 @@ #include #include #include -#include namespace ffi_go::ir { using namespace ffi::ir_stream; @@ -31,10 +29,10 @@ auto encode_log_message( StringView* dict_vars, Int32tSpan* dict_var_end_offsets ) -> int { - using encoded_var_t = typename std::conditional< + using encoded_var_t = std::conditional_t< std::is_same_v, ffi::eight_byte_encoded_variable_t, - ffi::four_byte_encoded_variable_t>::type; + ffi::four_byte_encoded_variable_t>; if (nullptr == ir_encoder || nullptr == logtype || nullptr == vars || nullptr == dict_vars || nullptr == dict_var_end_offsets) { @@ -85,10 +83,12 @@ auto encode_log_message( } // namespace CLP_FFI_GO_METHOD auto ir_encoder_eight_byte_new() -> void* { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) return new Encoder{}; } CLP_FFI_GO_METHOD auto ir_encoder_four_byte_new() -> void* { + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) return new Encoder{}; } diff --git a/cpp/src/ffi_go/ir/serializer.cpp b/cpp/src/ffi_go/ir/serializer.cpp index 7236a65..0cd7bb9 100644 --- a/cpp/src/ffi_go/ir/serializer.cpp +++ b/cpp/src/ffi_go/ir/serializer.cpp @@ -1,8 +1,5 @@ #include "serializer.h" -#include -#include -#include #include #include @@ -13,7 +10,6 @@ #include #include #include -#include namespace ffi_go::ir { using namespace ffi; diff --git a/cpp/src/ffi_go/ir/types.hpp b/cpp/src/ffi_go/ir/types.hpp index 794707c..848d08c 100644 --- a/cpp/src/ffi_go/ir/types.hpp +++ b/cpp/src/ffi_go/ir/types.hpp @@ -1,10 +1,11 @@ #ifndef FFI_GO_IR_LOG_TYPES_HPP #define FFI_GO_IR_LOG_TYPES_HPP +#include +#include #include #include -#include #include #include From 3d06c36d120b0de58a7ae42c7596b374635efac1 Mon Sep 17 00:00:00 2001 From: LinZhihao-723 Date: Mon, 29 Apr 2024 17:36:41 -0400 Subject: [PATCH 19/27] Apply clang-tidy suggestions on regular files --- cpp/src/ffi_go/api_decoration.h | 6 +++--- cpp/src/ffi_go/types.hpp | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cpp/src/ffi_go/api_decoration.h b/cpp/src/ffi_go/api_decoration.h index 897dcf3..0a5ff4a 100644 --- a/cpp/src/ffi_go/api_decoration.h +++ b/cpp/src/ffi_go/api_decoration.h @@ -6,9 +6,9 @@ * ensure C linkage. */ #ifdef __cplusplus -#define _CLP_FFI_GO_EXTERN_C extern "C" +#define CLP_FFI_GO_EXTERN_C extern "C" #else -#define _CLP_FFI_GO_EXTERN_C +#define CLP_FFI_GO_EXTERN_C #endif /** @@ -16,6 +16,6 @@ * declaration/implementation to decorate any APIs that are exposed to the * Golang layer. */ -#define CLP_FFI_GO_METHOD _CLP_FFI_GO_EXTERN_C +#define CLP_FFI_GO_METHOD CLP_FFI_GO_EXTERN_C #endif diff --git a/cpp/src/ffi_go/types.hpp b/cpp/src/ffi_go/types.hpp index 345d10e..0175249 100644 --- a/cpp/src/ffi_go/types.hpp +++ b/cpp/src/ffi_go/types.hpp @@ -1,6 +1,7 @@ #ifndef FFI_GO_LOG_TYPES_HPP #define FFI_GO_LOG_TYPES_HPP +#include #include namespace ffi_go { From 72e48bc48cbd4f7e2b24afef8896ad8c59ffb598 Mon Sep 17 00:00:00 2001 From: LinZhihao-723 Date: Tue, 30 Apr 2024 12:44:17 -0400 Subject: [PATCH 20/27] Update github workflow to specify Go versions. --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 19625c1..23a7185 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,9 @@ jobs: submodules: recursive - uses: actions/setup-go@v4 + with: + go-version: '1.20.x' + check-latest: true - run: go build ./... @@ -36,6 +39,9 @@ jobs: submodules: recursive - uses: actions/setup-go@v4 + with: + go-version: '1.20.x' + check-latest: true - if: ${{ 'macos-latest' == matrix.os }} run: | From 89139e02fb362d8955b622df29e0367839fdf67e Mon Sep 17 00:00:00 2001 From: davidlion Date: Wed, 8 May 2024 15:48:24 -0400 Subject: [PATCH 21/27] Workflow debugging. --- .github/workflows/build.yml | 12 +- cpp/CMakeLists.txt | 1 + cpp/src/ffi_go/ir/deserializer.cpp | 13 +- include/ffi_go/defs.h | 110 +++++----- include/ffi_go/ir/decoder.h | 143 ++++++------- include/ffi_go/ir/deserializer.h | 281 ++++++++++++------------- include/ffi_go/ir/encoder.h | 159 +++++++------- include/ffi_go/ir/serializer.h | 189 ++++++++--------- include/ffi_go/search/wildcard_query.h | 107 +++++----- ir/deserializer.go | 54 +++-- ir/reader.go | 19 +- ir/writer.go | 5 + 12 files changed, 530 insertions(+), 563 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23a7185..335fd51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,9 +5,7 @@ on: push: workflow_call: -# TODO: add separate jobs for: -# 1. building, linting, testing c++ -# 2. building and testing go using a fresh built c++ library +# TODO: add separate jobs for building, linting, and testing c++ jobs: prebuilt-test: strategy: @@ -24,9 +22,9 @@ jobs: go-version: '1.20.x' check-latest: true - - run: go build ./... + - run: go build - - run: go test ./... + - run: go test -count=1 ./... build-lint-test: strategy: @@ -57,7 +55,7 @@ jobs: go install github.com/segmentio/golines@latest go install golang.org/x/tools/cmd/stringer@latest - - run: go generate ./... + - run: go generate - run: | diff="$(golines -m 100 -t 4 --base-formatter='gofumpt' --dry-run .)" @@ -65,4 +63,4 @@ jobs: # - run: cmake -S ./cpp -B ./cpp/build -DCMAKE_EXPORT_COMPILE_COMMANDS=1 - - run: go test ./... + - run: go test -count=1 ./... diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 9c3c56e..13ba499 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -82,6 +82,7 @@ target_sources(${LIB_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS src/ FILES + src/ffi_go/api_decoration.h src/ffi_go/defs.h src/ffi_go/ir/decoder.h src/ffi_go/ir/deserializer.h diff --git a/cpp/src/ffi_go/ir/deserializer.cpp b/cpp/src/ffi_go/ir/deserializer.cpp index 81f450a..d7da93a 100644 --- a/cpp/src/ffi_go/ir/deserializer.cpp +++ b/cpp/src/ffi_go/ir/deserializer.cpp @@ -120,12 +120,10 @@ auto deserialize_wildcard_match( std::string_view const query_view{merged_query.m_queries.m_data, merged_query.m_queries.m_size}; std::span const end_offsets{ merged_query.m_end_offsets.m_data, - merged_query.m_end_offsets.m_size - }; + merged_query.m_end_offsets.m_size}; std::span const case_sensitivity{ merged_query.m_case_sensitivity.m_data, - merged_query.m_case_sensitivity.m_size - }; + merged_query.m_case_sensitivity.m_size}; std::vector> queries(merged_query.m_end_offsets.m_size); size_t pos{0}; @@ -187,8 +185,7 @@ auto deserialize_wildcard_match( continue; } auto const [has_matching_query, matching_query_idx]{ - query_fn(deserializer->m_log_event.m_log_message) - }; + query_fn(deserializer->m_log_event.m_log_message)}; if (false == has_matching_query) { continue; } @@ -237,8 +234,8 @@ CLP_FFI_GO_METHOD auto ir_deserializer_new_deserializer_with_preamble( } *ir_encoding = four_byte_encoding ? 1 : 0; - if (IRErrorCode const err{decode_preamble(ir_buf, *metadata_type, *metadata_pos, *metadata_size) - }; + if (IRErrorCode const err{ + decode_preamble(ir_buf, *metadata_type, *metadata_pos, *metadata_size)}; IRErrorCode_Success != err) { return static_cast(err); diff --git a/include/ffi_go/defs.h b/include/ffi_go/defs.h index 17f2a4f..563c89a 100644 --- a/include/ffi_go/defs.h +++ b/include/ffi_go/defs.h @@ -4,76 +4,68 @@ // NOLINTBEGIN(modernize-deprecated-headers) // NOLINTBEGIN(modernize-use-using) -#ifdef __cplusplus -extern "C" { -#endif - #include #include #include - // TODO: replace with clp c-compatible header once it exists - typedef int64_t epoch_time_ms_t; - - /** - * A span of a bool array passed down through Cgo. - */ - typedef struct { - bool* m_data; - size_t m_size; - } BoolSpan; +// TODO: replace with clp c-compatible header once it exists +typedef int64_t epoch_time_ms_t; - /** - * A span of a byte array passed down through Cgo. - */ - typedef struct { - void* m_data; - size_t m_size; - } ByteSpan; +/** + * A span of a bool array passed down through Cgo. + */ +typedef struct { + bool* m_data; + size_t m_size; +} BoolSpan; - /** - * A span of a Go int32 array passed down through Cgo. - */ - typedef struct { - int32_t* m_data; - size_t m_size; - } Int32tSpan; +/** + * A span of a byte array passed down through Cgo. + */ +typedef struct { + void* m_data; + size_t m_size; +} ByteSpan; - /** - * A span of a Go int64 array passed down through Cgo. - */ - typedef struct { - int64_t* m_data; - size_t m_size; - } Int64tSpan; +/** + * A span of a Go int32 array passed down through Cgo. + */ +typedef struct { + int32_t* m_data; + size_t m_size; +} Int32tSpan; - /** - * A span of a Go int/C.size_t array passed down through Cgo. - */ - typedef struct { - size_t* m_data; - size_t m_size; - } SizetSpan; +/** + * A span of a Go int64 array passed down through Cgo. + */ +typedef struct { + int64_t* m_data; + size_t m_size; +} Int64tSpan; - /** - * A view of a Go string passed down through Cgo. - */ - typedef struct { - char const* m_data; - size_t m_size; - } StringView; +/** + * A span of a Go int/C.size_t array passed down through Cgo. + */ +typedef struct { + size_t* m_data; + size_t m_size; +} SizetSpan; - /** - * A view of a Go ffi.LogEvent passed down through Cgo. - */ - typedef struct { - StringView m_log_message; - epoch_time_ms_t m_timestamp; - } LogEventView; +/** + * A view of a Go string passed down through Cgo. + */ +typedef struct { + char const* m_data; + size_t m_size; +} StringView; -#ifdef __cplusplus -} -#endif +/** + * A view of a Go ffi.LogEvent passed down through Cgo. + */ +typedef struct { + StringView m_log_message; + epoch_time_ms_t m_timestamp; +} LogEventView; // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-deprecated-headers) diff --git a/include/ffi_go/ir/decoder.h b/include/ffi_go/ir/decoder.h index 32e5b51..5d003be 100644 --- a/include/ffi_go/ir/decoder.h +++ b/include/ffi_go/ir/decoder.h @@ -8,86 +8,79 @@ #include #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. + * @return New ir::Decoder's address + */ +CLP_FFI_GO_METHOD void* ir_decoder_new(); - /** - * Create a ir::Decoder used as the underlying data storage for a Go ir.Decoder. - * @return New ir::Decoder's address - */ - void* ir_decoder_new(); +/** + * Clean up the underlying ir::Decoder of a Go ir.Decoder. + * @param[in] ir_encoder Address of a ir::Decoder created and returned by + * ir_decoder_new + */ +CLP_FFI_GO_METHOD void ir_decoder_close(void* decoder); - /** - * Clean up the underlying ir::Decoder of a Go ir.Decoder. - * @param[in] ir_encoder Address of a ir::Decoder created and returned by - * ir_decoder_new - */ - void ir_decoder_close(void* decoder); +/** + * Given the fields of a CLP IR encoded log message with eight byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_decoder_decode_eight_byte_log_message( + StringView logtype, + Int64tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); - /** - * Given the fields of a CLP IR encoded log message with eight byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_decoder_decode_eight_byte_log_message( - StringView logtype, - Int64tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message - ); - - /** - * Given the fields of a CLP IR encoded log message with four byte encoding, - * decode it into the original log message. An ir::Decoder must be provided to - * use as the backing storage for the corresponding Go ir.Decoder. All pointer - * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer - * from Go). - * @param[in] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[in] vars Array of encoded variables - * @param[in] dict_vars String containing all dictionary variables concatenated - * together - * @param[in] dict_var_end_offsets Array of offsets into dict_vars makring the - * end of a dictionary variable - * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log - * message - * @param[out] log_message Decoded log message - * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message - * throws or errors - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_decoder_decode_four_byte_log_message( - StringView logtype, - Int32tSpan vars, - StringView dict_vars, - Int32tSpan dict_var_end_offsets, - void* ir_decoder, - StringView* log_message - ); - -#ifdef __cplusplus -} -#endif +/** + * Given the fields of a CLP IR encoded log message with four byte encoding, + * decode it into the original log message. An ir::Decoder must be provided to + * use as the backing storage for the corresponding Go ir.Decoder. All pointer + * parameters must be non-null (non-nil Cgo C. pointer or unsafe.Pointer + * from Go). + * @param[in] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[in] vars Array of encoded variables + * @param[in] dict_vars String containing all dictionary variables concatenated + * together + * @param[in] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @param[in] ir_decoder ir::Decoder to be used as storage for the decoded log + * message + * @param[out] log_message Decoded log message + * @return ffi::ir_stream::IRErrorCode_Decode_Error if ffi::decode_message + * throws or errors + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_decoder_decode_four_byte_log_message( + StringView logtype, + Int32tSpan vars, + StringView dict_vars, + Int32tSpan dict_var_end_offsets, + void* ir_decoder, + StringView* log_message +); // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/include/ffi_go/ir/deserializer.h b/include/ffi_go/ir/deserializer.h index ba8fcc9..71ae1ab 100644 --- a/include/ffi_go/ir/deserializer.h +++ b/include/ffi_go/ir/deserializer.h @@ -7,158 +7,151 @@ #include #include +#include #include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. + * @param[in] ir_deserializer The address of a ir::Deserializer created and + * returned by ir_deserializer_new_deserializer_with_preamble + */ +CLP_FFI_GO_METHOD void ir_deserializer_close(void* ir_deserializer); - /** - * Clean up the underlying ir::Deserializer of a Go ir.Deserializer. - * @param[in] ir_deserializer The address of a ir::Deserializer created and - * returned by ir_deserializer_new_deserializer_with_preamble - */ - void ir_deserializer_close(void* ir_deserializer); +/** + * Given a CLP IR buffer (any encoding), attempt to deserialize a preamble and + * extract its information. An ir::Deserializer will be allocated to use as the + * backing storage for a Go ir.Deserializer (i.e. subsequent calls to + * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read + * the metadata based on the returned type. All pointer parameters must be + * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[out] ir_pos Position in ir_view read to + * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) + * @param[out] metadata_type Type of metadata in preamble (e.g. json) + * @param[out] metadata_pos Position in ir_view where the metadata begins + * @param[out] metadata_size Size of the metadata (in bytes) + * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer + * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer + * to be filled in by Go using the metadata contents + * @return ffi::ir_stream::IRErrorCode forwarded from either + * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble + */ +CLP_FFI_GO_METHOD int ir_deserializer_new_deserializer_with_preamble( + ByteSpan ir_view, + size_t* ir_pos, + int8_t* ir_encoding, + int8_t* metadata_type, + size_t* metadata_pos, + uint16_t* metadata_size, + void** ir_deserializer_ptr, + void** timestamp_ptr +); - /** - * Given a CLP IR buffer (any encoding), attempt to deserialize a preamble and - * extract its information. An ir::Deserializer will be allocated to use as the - * backing storage for a Go ir.Deserializer (i.e. subsequent calls to - * ir_deserializer_deserialize_*_log_event). It is left to the Go layer to read - * the metadata based on the returned type. All pointer parameters must be - * non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[out] ir_pos Position in ir_view read to - * @param[out] ir_encoding IR encoding type (1: four byte, 0: eight byte) - * @param[out] metadata_type Type of metadata in preamble (e.g. json) - * @param[out] metadata_pos Position in ir_view where the metadata begins - * @param[out] metadata_size Size of the metadata (in bytes) - * @param[out] ir_deserializer_ptr Address of a new ir::Deserializer - * @param[out] timestamp_ptr Address of m_timestamp inside the ir::Deserializer - * to be filled in by Go using the metadata contents - * @return ffi::ir_stream::IRErrorCode forwarded from either - * ffi::ir_stream::get_encoding_type or ffi::ir_stream::decode_preamble - */ - int ir_deserializer_new_deserializer_with_preamble( - ByteSpan ir_view, - size_t* ir_pos, - int8_t* ir_encoding, - int8_t* metadata_type, - size_t* metadata_pos, - uint16_t* metadata_size, - void** ir_deserializer_ptr, - void** timestamp_ptr - ); +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::decode_next_message + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_eight_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); - /** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::decode_next_message - */ - int ir_deserializer_deserialize_eight_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event - ); +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log + * event. Returns the components of the found log event and the buffer position + * it ends at. All pointer parameters must be non-null (non-nil Cgo C. + * pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_four_byte_log_event( + ByteSpan ir_view, + void* ir_deserializer, + size_t* ir_pos, + LogEventView* log_event +); - /** - * Given a CLP IR buffer with four byte encoding, deserialize the next log - * event. Returns the components of the found log event and the buffer position - * it ends at. All pointer parameters must be non-null (non-nil Cgo C. - * pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - */ - int ir_deserializer_deserialize_four_byte_log_event( - ByteSpan ir_view, - void* ir_deserializer, - size_t* ir_pos, - LogEventView* log_event - ); +/** + * Given a CLP IR buffer with eight byte encoding, deserialize the next log + * event until finding an event that is both within the time interval and + * matches any query. If queries is empty, the first log event within the time + * interval is treated as a match. Returns the components of the found log event + * and the buffer position it ends at. All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the first matching query or + * 0 if queries is empty + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_eight_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); - /** - * Given a CLP IR buffer with eight byte encoding, deserialize the next log - * event until finding an event that is both within the time interval and - * matches any query. If queries is empty, the first log event within the time - * interval is treated as a match. Returns the components of the found log event - * and the buffer position it ends at. All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the first matching query or - * 0 if queries is empty - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ - int ir_deserializer_deserialize_eight_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query - ); - - /** - * Given a CLP IR buffer with four byte encoding, deserialize the next log event - * until finding an event that is both within the time interval and matches any - * query. If queries is empty, the first log event within the time interval is - * treated as a match. Returns the components of the found log event and the - * buffer position it ends at. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ir_view Byte buffer/slice containing CLP IR - * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found - * log event - * @param[in] time_interval Timestamp interval: [lower, upper) - * @param[in] merged_query A concatenation of all queries to filter for; if - * empty any log event as a match - * @param[out] ir_pos Position in ir_view read to - * @param[out] log_event Log event stored in ir_deserializer - * @param[out] matching_query Index into queries of the matching query - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::decode_next_message - * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is - * found before time_interval.m_upper (TODO this should be replaced/fix in - * clp core) - */ - int ir_deserializer_deserialize_four_byte_wildcard_match( - ByteSpan ir_view, - void* ir_deserializer, - TimestampInterval time_interval, - MergedWildcardQueryView merged_query, - size_t* ir_pos, - LogEventView* log_event, - size_t* matching_query - ); - -#ifdef __cplusplus -} -#endif +/** + * Given a CLP IR buffer with four byte encoding, deserialize the next log event + * until finding an event that is both within the time interval and matches any + * query. If queries is empty, the first log event within the time interval is + * treated as a match. Returns the components of the found log event and the + * buffer position it ends at. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ir_view Byte buffer/slice containing CLP IR + * @param[in] ir_deserializer ir::Deserializer to be used as storage for a found + * log event + * @param[in] time_interval Timestamp interval: [lower, upper) + * @param[in] merged_query A concatenation of all queries to filter for; if + * empty any log event as a match + * @param[out] ir_pos Position in ir_view read to + * @param[out] log_event Log event stored in ir_deserializer + * @param[out] matching_query Index into queries of the matching query + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::decode_next_message + * @return ffi::ir_stream::IRErrorCode_Unsupported_Version + 1 if no query is + * found before time_interval.m_upper (TODO this should be replaced/fix in + * clp core) + */ +CLP_FFI_GO_METHOD int ir_deserializer_deserialize_four_byte_wildcard_match( + ByteSpan ir_view, + void* ir_deserializer, + TimestampInterval time_interval, + MergedWildcardQueryView merged_query, + size_t* ir_pos, + LogEventView* log_event, + size_t* matching_query +); // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/include/ffi_go/ir/encoder.h b/include/ffi_go/ir/encoder.h index 1737ada..d1ae99e 100644 --- a/include/ffi_go/ir/encoder.h +++ b/include/ffi_go/ir/encoder.h @@ -8,96 +8,89 @@ #include #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. + * @return New ir::Encoder's address + */ +CLP_FFI_GO_METHOD void* ir_encoder_eight_byte_new(); - /** - * Create a ir::Encoder used as the underlying data storage for a Go ir.Encoder. - * @return New ir::Encoder's address - */ - void* ir_encoder_eight_byte_new(); +/** + * @copydoc ir_encoder_eight_byte_new() + */ +CLP_FFI_GO_METHOD void* ir_encoder_four_byte_new(); - /** - * @copydoc ir_encoder_eight_byte_new() - */ - void* ir_encoder_four_byte_new(); +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_eight_byte_new + */ +CLP_FFI_GO_METHOD void ir_encoder_eight_byte_close(void* ir_encoder); - /** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_eight_byte_new - */ - void ir_encoder_eight_byte_close(void* ir_encoder); +/** + * Clean up the underlying ir::Encoder of a Go ir.Encoder. + * @param[in] ir_encoder Address of a ir::Encoder created and returned by + * ir_encoder_four_byte_new + */ +CLP_FFI_GO_METHOD void ir_encoder_four_byte_close(void* ir_encoder); - /** - * Clean up the underlying ir::Encoder of a Go ir.Encoder. - * @param[in] ir_encoder Address of a ir::Encoder created and returned by - * ir_encoder_four_byte_new - */ - void ir_encoder_four_byte_close(void* ir_encoder); +/** + * Given a log message, encode it into a CLP IR object with eight byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_encoder_encode_eight_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int64tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); - /** - * Given a log message, encode it into a CLP IR object with eight byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_encoder_encode_eight_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int64tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets - ); - - /** - * Given a log message, encode it into a CLP IR object with four byte encoding. - * An ir::Encoder must be provided to use as the backing storage for the - * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil - * Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to encode - * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log - * message - * @param[out] logtype Type of the log message (the log message with variables - * extracted and replaced with placeholders) - * @param[out] vars Array of encoded variables - * @param[out] dict_vars String containing all dictionary variables concatenated - * together - * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the - * end of a dictionary variable - * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message - * returns false - * @return ffi::ir_stream::IRErrorCode_Success on success - */ - int ir_encoder_encode_four_byte_log_message( - StringView log_message, - void* ir_encoder, - StringView* logtype, - Int32tSpan* vars, - StringView* dict_vars, - Int32tSpan* dict_var_end_offsets - ); - -#ifdef __cplusplus -} -#endif +/** + * Given a log message, encode it into a CLP IR object with four byte encoding. + * An ir::Encoder must be provided to use as the backing storage for the + * corresponding Go ir.Encoder. All pointer parameters must be non-null (non-nil + * Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to encode + * @param[in] ir_encoder ir::Encoder to be used as storage for the encoded log + * message + * @param[out] logtype Type of the log message (the log message with variables + * extracted and replaced with placeholders) + * @param[out] vars Array of encoded variables + * @param[out] dict_vars String containing all dictionary variables concatenated + * together + * @param[out] dict_var_end_offsets Array of offsets into dict_vars marking the + * end of a dictionary variable + * @return ffi::ir_stream::IRErrorCode_Corrupted_IR if ffi::encode_message + * returns false + * @return ffi::ir_stream::IRErrorCode_Success on success + */ +CLP_FFI_GO_METHOD int ir_encoder_encode_four_byte_log_message( + StringView log_message, + void* ir_encoder, + StringView* logtype, + Int32tSpan* vars, + StringView* dict_vars, + Int32tSpan* dict_var_end_offsets +); // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/include/ffi_go/ir/serializer.h b/include/ffi_go/ir/serializer.h index 632089f..bd02f8c 100644 --- a/include/ffi_go/ir/serializer.h +++ b/include/ffi_go/ir/serializer.h @@ -7,110 +7,103 @@ #include #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * Clean up the underlying ir::Serializer of a Go ir.Serializer. + * @param[in] ir_serializer Address of a ir::Serializer created and returned by + * ir_serializer_serialize_*_preamble + */ +CLP_FFI_GO_METHOD void ir_serializer_close(void* ir_serializer); - /** - * Clean up the underlying ir::Serializer of a Go ir.Serializer. - * @param[in] ir_serializer Address of a ir::Serializer created and returned by - * ir_serializer_serialize_*_preamble - */ - void ir_serializer_close(void* ir_serializer); +/** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with eight byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_preamble + */ +CLP_FFI_GO_METHOD int ir_serializer_new_eight_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + void** ir_serializer_ptr, + ByteSpan* ir_view +); - /** - * Given the fields of a CLP IR preamble, serialize them into an IR byte stream - * with eight byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::encode_preamble - */ - int ir_serializer_new_eight_byte_serializer_with_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - void** ir_serializer_ptr, - ByteSpan* ir_view - ); +/** + * Given the fields of a CLP IR preamble, serialize them into an IR byte stream + * with four byte encoding. An ir::Serializer will be allocated to use as the + * backing storage for a Go ir.Serializer (i.e. subsequent calls to + * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null + * (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] ts_pattern Format string for the timestamp to be used when + * deserializing the IR + * @param[in] ts_pattern_syntax Type of the format string for understanding how + * to parse it + * @param[in] time_zone_id TZID timezone of the timestamps in the IR + * @param[out] ir_serializer_ptr Address of a new ir::Serializer + * @param[out] ir_view View of a IR buffer containing the serialized preamble + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_preamble + */ +CLP_FFI_GO_METHOD int ir_serializer_new_four_byte_serializer_with_preamble( + StringView ts_pattern, + StringView ts_pattern_syntax, + StringView time_zone_id, + epoch_time_ms_t reference_ts, + void** ir_serializer_ptr, + ByteSpan* ir_view +); - /** - * Given the fields of a CLP IR preamble, serialize them into an IR byte stream - * with four byte encoding. An ir::Serializer will be allocated to use as the - * backing storage for a Go ir.Serializer (i.e. subsequent calls to - * ir_serializer_serialize_*_log_event). All pointer parameters must be non-null - * (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] ts_pattern Format string for the timestamp to be used when - * deserializing the IR - * @param[in] ts_pattern_syntax Type of the format string for understanding how - * to parse it - * @param[in] time_zone_id TZID timezone of the timestamps in the IR - * @param[out] ir_serializer_ptr Address of a new ir::Serializer - * @param[out] ir_view View of a IR buffer containing the serialized preamble - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::encode_preamble - */ - int ir_serializer_new_four_byte_serializer_with_preamble( - StringView ts_pattern, - StringView ts_pattern_syntax, - StringView time_zone_id, - epoch_time_ms_t reference_ts, - void** ir_serializer_ptr, - ByteSpan* ir_view - ); +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * eight byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message of the log event to serialize + * @param[in] timestamp Timestamp of the log event to serialize + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::eight_byte_encoding::encode_message + */ +CLP_FFI_GO_METHOD int ir_serializer_serialize_eight_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp, + void* ir_serializer, + ByteSpan* ir_view +); - /** - * Given the fields of a log event, serialize them into an IR byte stream with - * eight byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message of the log event to serialize - * @param[in] timestamp Timestamp of the log event to serialize - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::eight_byte_encoding::encode_message - */ - int ir_serializer_serialize_eight_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp, - void* ir_serializer, - ByteSpan* ir_view - ); - - /** - * Given the fields of a log event, serialize them into an IR byte stream with - * four byte encoding. An ir::Serializer must be provided to use as the backing - * storage for the corresponding Go ir.Serializer. All pointer parameters must - * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). - * @param[in] log_message Log message to serialize - * @param[in] timestamp_delta Timestamp delta to the previous log event in the - * IR stream - * @param[in] ir_serializer ir::Serializer object to be used as storage - * @param[out] ir_view View of a IR buffer containing the serialized log event - * @return ffi::ir_stream::IRErrorCode forwarded from - * ffi::ir_stream::four_byte_encoding::encode_message - */ - int ir_serializer_serialize_four_byte_log_event( - StringView log_message, - epoch_time_ms_t timestamp_delta, - void* ir_serializer, - ByteSpan* ir_view - ); - -#ifdef __cplusplus -} -#endif +/** + * Given the fields of a log event, serialize them into an IR byte stream with + * four byte encoding. An ir::Serializer must be provided to use as the backing + * storage for the corresponding Go ir.Serializer. All pointer parameters must + * be non-null (non-nil Cgo C. pointer or unsafe.Pointer from Go). + * @param[in] log_message Log message to serialize + * @param[in] timestamp_delta Timestamp delta to the previous log event in the + * IR stream + * @param[in] ir_serializer ir::Serializer object to be used as storage + * @param[out] ir_view View of a IR buffer containing the serialized log event + * @return ffi::ir_stream::IRErrorCode forwarded from + * ffi::ir_stream::four_byte_encoding::encode_message + */ +CLP_FFI_GO_METHOD int ir_serializer_serialize_four_byte_log_event( + StringView log_message, + epoch_time_ms_t timestamp_delta, + void* ir_serializer, + ByteSpan* ir_view +); // NOLINTEND(modernize-use-trailing-return-type) // NOLINTEND(modernize-deprecated-headers) diff --git a/include/ffi_go/search/wildcard_query.h b/include/ffi_go/search/wildcard_query.h index 3adcc5d..960beb1 100644 --- a/include/ffi_go/search/wildcard_query.h +++ b/include/ffi_go/search/wildcard_query.h @@ -7,70 +7,63 @@ #include +#include #include -#ifdef __cplusplus -extern "C" { -#endif +/** + * A timestamp interval of [m_lower, m_upper). + */ +typedef struct { + epoch_time_ms_t m_lower; + epoch_time_ms_t m_upper; +} TimestampInterval; - /** - * A timestamp interval of [m_lower, m_upper). - */ - typedef struct { - epoch_time_ms_t m_lower; - epoch_time_ms_t m_upper; - } TimestampInterval; +/** + * A view of a wildcard query passed down from Go. The query string is assumed + * to have been cleaned using the CLP function `clean_up_wildcard_search_string` + * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case + * insensitive). + */ +typedef struct { + StringView m_query; + bool m_case_sensitive; +} WildcardQueryView; - /** - * A view of a wildcard query passed down from Go. The query string is assumed - * to have been cleaned using the CLP function `clean_up_wildcard_search_string` - * on construction. m_case_sensitive is 1 for a case sensitive query (0 for case - * insensitive). - */ - typedef struct { - StringView m_query; - bool m_case_sensitive; - } WildcardQueryView; +/** + * A view of a Go search.MergedWildcardQuery passed down through Cgo. The + * string is a concatenation of all wildcard queries, while m_end_offsets stores + * the size of each query. + */ +typedef struct { + StringView m_queries; + SizetSpan m_end_offsets; + BoolSpan m_case_sensitivity; +} MergedWildcardQueryView; - /** - * A view of a Go search.MergedWildcardQuery passed down through Cgo. The - * string is a concatenation of all wildcard queries, while m_end_offsets stores - * the size of each query. - */ - typedef struct { - StringView m_queries; - SizetSpan m_end_offsets; - BoolSpan m_case_sensitivity; - } MergedWildcardQueryView; +/** + * Given a query string, allocate and return a clean string that is safe for + * matching. See `clean_up_wildcard_search_string` in CLP for more details. + * @param[in] query Query string to clean + * @param[in] ptr Address of a new std::string + * @return New string holding cleaned query + */ +CLP_FFI_GO_METHOD StringView wildcard_query_new(StringView query, void** ptr); - /** - * Given a query string, allocate and return a clean string that is safe for - * matching. See `clean_up_wildcard_search_string` in CLP for more details. - * @param[in] query Query string to clean - * @param[in] ptr Address of a new std::string - * @return New string holding cleaned query - */ - StringView wildcard_query_new(StringView query, void** ptr); +/** + * Delete a std::string holding a wildcard query. + * @param[in] str Address of a std::string created and returned by + * clean_wildcard_query + */ +CLP_FFI_GO_METHOD void wildcard_query_delete(void* str); - /** - * Delete a std::string holding a wildcard query. - * @param[in] str Address of a std::string created and returned by - * clean_wildcard_query - */ - void wildcard_query_delete(void* str); - - /** - * Given a target string perform CLP wildcard matching using query. See - * `wildcard_match_unsafe` in CLP src/string_utils.hpp. - * @param[in] target String to perform matching on - * @param[in] query Query to use for matching - * @return 1 if query matches target, 0 otherwise - */ - int wildcard_query_match(StringView target, WildcardQueryView query); - -#ifdef __cplusplus -} -#endif +/** + * Given a target string perform CLP wildcard matching using query. See + * `wildcard_match_unsafe` in CLP src/string_utils.hpp. + * @param[in] target String to perform matching on + * @param[in] query Query to use for matching + * @return 1 if query matches target, 0 otherwise + */ +CLP_FFI_GO_METHOD int wildcard_query_match(StringView target, WildcardQueryView query); // NOLINTEND(modernize-use-using) // NOLINTEND(modernize-use-trailing-return-type) diff --git a/ir/deserializer.go b/ir/deserializer.go index 8b5bd81..955ef72 100644 --- a/ir/deserializer.go +++ b/ir/deserializer.go @@ -16,6 +16,13 @@ import ( "github.com/y-scope/clp-ffi-go/search" ) +const ( + metadata_reference_timestamp_key = "REFERENCE_TIMESTAMP" + metadata_timestamp_pattern_key = "TIMESTAMP_PATTERN" + metadata_timestamp_pattern_syntax_key = "TIMESTAMP_PATTERN_SYNTAX" + metadata_tz_id_key = "TZ_ID" +) + // A Deserializer exports functions to deserialize log events from a CLP IR byte // stream. Deserialization functions take an IR buffer as input, but how that // buffer is materialized is left to the user. These functions return views @@ -49,6 +56,9 @@ func DeserializePreamble(irBuf []byte) (Deserializer, int, error) { return nil, 0, IncompleteIr } + // TODO: Add version validation in this method or ir_deserializer_new_deserializer_with_preamble + // after updating the clp version. + var pos C.size_t var irEncoding C.int8_t var metadataType C.int8_t @@ -82,20 +92,20 @@ func DeserializePreamble(irBuf []byte) (Deserializer, int, error) { } var tsInfo TimestampInfo - if tsPat, ok := metadata["TIMESTAMP_PATTERN"].(string); ok { + if tsPat, ok := metadata[metadata_timestamp_pattern_key].(string); ok { tsInfo.Pattern = tsPat } - if tsSyn, ok := metadata["TIMESTAMP_PATTERN_SYNTAX"].(string); ok { + if tsSyn, ok := metadata[metadata_timestamp_pattern_syntax_key].(string); ok { tsInfo.PatternSyntax = tsSyn } - if tzid, ok := metadata["TZ_ID"].(string); ok { + if tzid, ok := metadata[metadata_tz_id_key].(string); ok { tsInfo.TimeZoneId = tzid } var deserializer Deserializer if 1 == irEncoding { var refTs ffi.EpochTimeMs = 0 - if tsStr, ok := metadata["REFERENCE_TIMESTAMP"].(string); ok { + if tsStr, ok := metadata[metadata_reference_timestamp_key].(string); ok { if tsInt, err := strconv.ParseInt(tsStr, 10, 64); nil == err { refTs = ffi.EpochTimeMs(tsInt) *(*ffi.EpochTimeMs)(timestampCptr) = refTs @@ -151,6 +161,24 @@ func (self *eightByteDeserializer) DeserializeLogEvent( return deserializeLogEvent(self, irBuf) } +// DeserializeWildcardMatch attempts to read the next log event from the IR +// stream in irBuf that matches mergedQuery within timeInterval. It returns the +// deserialized [ffi.LogEventView], the position read to in irBuf (the end of +// the log event in irBuf), the index of the matched query in mergedQuery, +// and an error. On error returns: +// - nil *ffi.LogEventView +// - 0 position +// - -1 index +// - [IrError] error: CLP failed to successfully deserialize +// - [EndOfIr] error: CLP found the IR stream EOF tag +func (self *eightByteDeserializer) DeserializeWildcardMatch( + irBuf []byte, + timeInterval search.TimestampInterval, + mergedQuery search.MergedWildcardQuery, +) (*ffi.LogEventView, int, int, error) { + return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) +} + // fourByteDeserializer contains both a common CLP IR deserializer and stores // the previously seen log event's timestamp. The previous timestamp is // necessary to calculate the current timestamp as four byte encoding only @@ -173,24 +201,6 @@ func (self *fourByteDeserializer) DeserializeLogEvent( return deserializeLogEvent(self, irBuf) } -// DeserializeWildcardMatch attempts to read the next log event from the IR -// stream in irBuf that matches mergedQuery within timeInterval. It returns the -// deserialized [ffi.LogEventView], the position read to in irBuf (the end of -// the log event in irBuf), the index of the matched query in mergedQuery, -// and an error. On error returns: -// - nil *ffi.LogEventView -// - 0 position -// - -1 index -// - [IrError] error: CLP failed to successfully deserialize -// - [EndOfIr] error: CLP found the IR stream EOF tag -func (self *eightByteDeserializer) DeserializeWildcardMatch( - irBuf []byte, - timeInterval search.TimestampInterval, - mergedQuery search.MergedWildcardQuery, -) (*ffi.LogEventView, int, int, error) { - return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) -} - // DeserializeWildcardMatch attempts to read the next log event from the IR // stream in irBuf that matches mergedQuery within timeInterval. It returns the // deserialized [ffi.LogEventView], the position read to in irBuf (the end of diff --git a/ir/reader.go b/ir/reader.go index c19eb45..b09e661 100644 --- a/ir/reader.go +++ b/ir/reader.go @@ -29,7 +29,7 @@ type Reader struct { // grow if it is too small to contain the preamble or next log event. Returns: // - success: valid [*Reader], nil // - error: nil [*Reader], error propagated from [DeserializePreamble] or -// [ioReader.Read] +// [io.Reader.Read] func NewReaderSize(r io.Reader, size int) (*Reader, error) { irr := &Reader{nil, r, make([]byte, size), 0, 0} var err error @@ -62,11 +62,10 @@ func (self *Reader) Close() error { return self.Deserializer.Close() } -// Read uses [DeserializeLogEvent] to read from the CLP IR byte stream. The -// underlying buffer will grow if it is too small to contain the next log event. -// On error returns: +// Read uses [Deserializer.DeserializeLogEvent] to read from the CLP IR byte stream. The underlying +// buffer will grow if it is too small to contain the next log event. On error returns: // - nil *ffi.LogEventView -// - error propagated from [DeserializeLogEvent] or [ioReader.Read] +// - error propagated from [Deserializer.DeserializeLogEvent] or [io.Reader.Read] func (self *Reader) Read() (*ffi.LogEventView, error) { var event *ffi.LogEventView var pos int @@ -171,12 +170,12 @@ func (self *Reader) ReadToSuffix(suffix string) (*ffi.LogEventView, error) { } // fillBuf shifts the remaining valid IR in [Reader.buf] to the front and then -// calls [ioReader.Read] to fill the remainder with more IR. Before reading into -// the buffer, it is grown by 1.5x if more than 1/4th of it is unconsumed IR. -// Forwards the return of [ioReader.Read]. +// calls [io.Reader.Read] to fill the remainder with more IR. Before reading into +// the buffer, it is doubled if more than half of it is unconsumed IR. +// Forwards the return of [io.Reader.Read]. func (self *Reader) fillBuf() (int, error) { - if (self.end - self.start) > len(self.buf)>>2 { - buf := make([]byte, len(self.buf)+len(self.buf)/2) + if (self.end - self.start) > len(self.buf)/2 { + buf := make([]byte, len(self.buf)*2) copy(buf, self.buf[self.start:self.end]) self.buf = buf } else { diff --git a/ir/writer.go b/ir/writer.go index 8500694..49d48b4 100644 --- a/ir/writer.go +++ b/ir/writer.go @@ -113,6 +113,11 @@ func (self *Writer) Write(event ffi.LogEvent) (int, error) { if nil != err { return 0, err } + // bytes.Buffer.Write will always return nil for err (https://pkg.go.dev/bytes#Buffer.Write) + // However, err is still propagated to correctly alert the user in case this ever changes. If + // Write can fail in the future, we should either: + // 1. fix the issue and retry the write + // 2. store irView and provide a retry API (allowing the user to fix the issue and retry) n, err := self.buf.Write(irView) if nil != err { return n, err From ef2eb59a3a0ecb6aa9293c2d8c5fc8da3bd974a9 Mon Sep 17 00:00:00 2001 From: davidlion Date: Wed, 8 May 2024 15:55:28 -0400 Subject: [PATCH 22/27] Add api_decoration header. --- include/ffi_go/api_decoration.h | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 include/ffi_go/api_decoration.h diff --git a/include/ffi_go/api_decoration.h b/include/ffi_go/api_decoration.h new file mode 100644 index 0000000..0a5ff4a --- /dev/null +++ b/include/ffi_go/api_decoration.h @@ -0,0 +1,21 @@ +#ifndef FFI_GO_API_DECORATION_H +#define FFI_GO_API_DECORATION_H + +/** + * If the file is compiled with a C++ compiler, `extern "C"` must be defined to + * ensure C linkage. + */ +#ifdef __cplusplus +#define CLP_FFI_GO_EXTERN_C extern "C" +#else +#define CLP_FFI_GO_EXTERN_C +#endif + +/** + * `CLP_FFI_GO_METHOD` should be added at the beginning of a function's + * declaration/implementation to decorate any APIs that are exposed to the + * Golang layer. + */ +#define CLP_FFI_GO_METHOD CLP_FFI_GO_EXTERN_C + +#endif From 85dab7570515994eb5ee4a3a01daabb4c8d8aef6 Mon Sep 17 00:00:00 2001 From: davidlion Date: Wed, 8 May 2024 15:57:34 -0400 Subject: [PATCH 23/27] Fix linting. --- ir/deserializer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ir/deserializer.go b/ir/deserializer.go index 955ef72..e2012c5 100644 --- a/ir/deserializer.go +++ b/ir/deserializer.go @@ -17,10 +17,10 @@ import ( ) const ( - metadata_reference_timestamp_key = "REFERENCE_TIMESTAMP" - metadata_timestamp_pattern_key = "TIMESTAMP_PATTERN" + metadata_reference_timestamp_key = "REFERENCE_TIMESTAMP" + metadata_timestamp_pattern_key = "TIMESTAMP_PATTERN" metadata_timestamp_pattern_syntax_key = "TIMESTAMP_PATTERN_SYNTAX" - metadata_tz_id_key = "TZ_ID" + metadata_tz_id_key = "TZ_ID" ) // A Deserializer exports functions to deserialize log events from a CLP IR byte From b18618bc54c70e10e5c50d935ca45805484be380 Mon Sep 17 00:00:00 2001 From: davidlion Date: Wed, 8 May 2024 16:07:56 -0400 Subject: [PATCH 24/27] Fix workflows hopefully. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 335fd51..50fb14f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: go-version: '1.20.x' check-latest: true - - run: go build + - run: go clean -cache && go build ./... - run: go test -count=1 ./... @@ -55,7 +55,7 @@ jobs: go install github.com/segmentio/golines@latest go install golang.org/x/tools/cmd/stringer@latest - - run: go generate + - run: go clean -cache && go generate ./... - run: | diff="$(golines -m 100 -t 4 --base-formatter='gofumpt' --dry-run .)" From ed428c7fd526ddb47511ee7eef9c25ded75c233b Mon Sep 17 00:00:00 2001 From: davidlion Date: Wed, 8 May 2024 22:05:52 -0400 Subject: [PATCH 25/27] Removed confusing comment. --- search/wildcard_query.go | 1 - 1 file changed, 1 deletion(-) diff --git a/search/wildcard_query.go b/search/wildcard_query.go index bcaf283..7913253 100644 --- a/search/wildcard_query.go +++ b/search/wildcard_query.go @@ -17,7 +17,6 @@ import ( // query is case sensitive or not. The fields must be accessed through getters // to ensure that the query string remains clean/safe after creation by // NewWildcardQuery. -// (Copied from clp/components/core/src/string_utils.hpp) // Two wildcards are currently supported: '*' to match 0 or more characters, and // '?' to match any single character. Each can be escaped using a preceding '\'. // Other characters which are escaped are treated as normal characters. From 0dd51a2fa151161a9acd039b2e8e30e5081e5ac8 Mon Sep 17 00:00:00 2001 From: LittleStar <59785146+LinZhihao-723@users.noreply.github.com> Date: Thu, 9 May 2024 14:27:50 -0400 Subject: [PATCH 26/27] Add darwin arm64 lib --- lib/libclp_ffi_darwin_arm64.a | Bin 0 -> 208360 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/libclp_ffi_darwin_arm64.a diff --git a/lib/libclp_ffi_darwin_arm64.a b/lib/libclp_ffi_darwin_arm64.a new file mode 100644 index 0000000000000000000000000000000000000000..56264af3866df7911f83ef68bd2c845a9702122b GIT binary patch literal 208360 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 #tlu|#OB4I&Gh#}7TR9;49lOOX`3Kv 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

e6J;`SCB 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 F{|Djusonqp literal 0 HcmV?d00001 From 49d61fb3ca43b62a6ab4297a4a077a8312101137 Mon Sep 17 00:00:00 2001 From: davidlion Date: Thu, 9 May 2024 15:09:47 -0400 Subject: [PATCH 27/27] Added helper function to reader to perform a wildcard query with the max time interval. --- ir/deserializer.go | 38 +++++++++++++++++++------------------- ir/reader.go | 27 ++++++++++++++++++++++++--- ir/reader_test.go | 4 ++-- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/ir/deserializer.go b/ir/deserializer.go index e2012c5..cd0dc8d 100644 --- a/ir/deserializer.go +++ b/ir/deserializer.go @@ -33,10 +33,10 @@ const ( // failure to do so will result in a memory leak. type Deserializer interface { DeserializeLogEvent(irBuf []byte) (*ffi.LogEventView, int, error) - DeserializeWildcardMatch( + DeserializeWildcardMatchWithTimeInterval( irBuf []byte, - timeInterval search.TimestampInterval, mergedQuery search.MergedWildcardQuery, + timeInterval search.TimestampInterval, ) (*ffi.LogEventView, int, int, error) TimestampInfo() TimestampInfo Close() error @@ -161,22 +161,22 @@ func (self *eightByteDeserializer) DeserializeLogEvent( return deserializeLogEvent(self, irBuf) } -// DeserializeWildcardMatch attempts to read the next log event from the IR -// stream in irBuf that matches mergedQuery within timeInterval. It returns the -// deserialized [ffi.LogEventView], the position read to in irBuf (the end of -// the log event in irBuf), the index of the matched query in mergedQuery, -// and an error. On error returns: +// DeserializeWildcardMatchWithTimeInterval attempts to read the next log event +// from the IR stream in irBuf that matches mergedQuery within timeInterval. It +// returns the deserialized [ffi.LogEventView], the position read to in irBuf +// (the end of the log event in irBuf), the index of the matched query in +// mergedQuery, and an error. On error returns: // - nil *ffi.LogEventView // - 0 position // - -1 index // - [IrError] error: CLP failed to successfully deserialize // - [EndOfIr] error: CLP found the IR stream EOF tag -func (self *eightByteDeserializer) DeserializeWildcardMatch( +func (self *eightByteDeserializer) DeserializeWildcardMatchWithTimeInterval( irBuf []byte, - timeInterval search.TimestampInterval, mergedQuery search.MergedWildcardQuery, + timeInterval search.TimestampInterval, ) (*ffi.LogEventView, int, int, error) { - return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) + return deserializeWildcardMatch(self, irBuf, mergedQuery, timeInterval) } // fourByteDeserializer contains both a common CLP IR deserializer and stores @@ -201,22 +201,22 @@ func (self *fourByteDeserializer) DeserializeLogEvent( return deserializeLogEvent(self, irBuf) } -// DeserializeWildcardMatch attempts to read the next log event from the IR -// stream in irBuf that matches mergedQuery within timeInterval. It returns the -// deserialized [ffi.LogEventView], the position read to in irBuf (the end of -// the log event in irBuf), the index of the matched query in mergedQuery, -// and an error. On error returns: +// DeserializeWildcardMatchWithTimeInterval attempts to read the next log event +// from the IR stream in irBuf that matches mergedQuery within timeInterval. It +// returns the deserialized [ffi.LogEventView], the position read to in irBuf +// (the end of the log event in irBuf), the index of the matched query in +// mergedQuery, and an error. On error returns: // - nil *ffi.LogEventView // - 0 position // - -1 index // - [IrError] error: CLP failed to successfully deserialize // - [EndOfIr] error: CLP found the IR stream EOF tag -func (self *fourByteDeserializer) DeserializeWildcardMatch( +func (self *fourByteDeserializer) DeserializeWildcardMatchWithTimeInterval( irBuf []byte, - timeInterval search.TimestampInterval, mergedQuery search.MergedWildcardQuery, + timeInterval search.TimestampInterval, ) (*ffi.LogEventView, int, int, error) { - return deserializeWildcardMatch(self, irBuf, timeInterval, mergedQuery) + return deserializeWildcardMatch(self, irBuf, mergedQuery, timeInterval) } func deserializeLogEvent( @@ -264,8 +264,8 @@ func deserializeLogEvent( func deserializeWildcardMatch( deserializer Deserializer, irBuf []byte, - time search.TimestampInterval, mergedQuery search.MergedWildcardQuery, + time search.TimestampInterval, ) (*ffi.LogEventView, int, int, error) { if 0 >= len(irBuf) { return nil, 0, -1, IncompleteIr diff --git a/ir/reader.go b/ir/reader.go index b09e661..816c8d4 100644 --- a/ir/reader.go +++ b/ir/reader.go @@ -2,6 +2,7 @@ package ir import ( "io" + "math" "strings" "github.com/y-scope/clp-ffi-go/ffi" @@ -86,9 +87,29 @@ func (self *Reader) Read() (*ffi.LogEventView, error) { return event, nil } +// ReadToWildcardMatch wraps ReadToWildcardMatchWithTimeInterval, attempting to +// read the next log event that matches any query in queries, within the entire +// IR. It forwards the result of ReadToWildcardMatchWithTimeInterval. func (self *Reader) ReadToWildcardMatch( - timeInterval search.TimestampInterval, queries []search.WildcardQuery, +) (*ffi.LogEventView, int, error) { + return self.ReadToWildcardMatchWithTimeInterval( + queries, + search.TimestampInterval{0, math.MaxInt64}, + ) +} + +// ReadToWildcardMatchWithTimeInterval attempts to read the next log event that +// matches any query in queries, within timeInterval. It returns the +// deserialized [ffi.LogEventView], the index of the matched query in queries, +// and an error. On error returns: +// - nil *ffi.LogEventView +// - -1 index +// - [IrError] error: CLP failed to successfully deserialize +// - [EndOfIr] error: CLP found the IR stream EOF tag +func (self *Reader) ReadToWildcardMatchWithTimeInterval( + queries []search.WildcardQuery, + timeInterval search.TimestampInterval, ) (*ffi.LogEventView, int, error) { var event *ffi.LogEventView var pos int @@ -96,10 +117,10 @@ func (self *Reader) ReadToWildcardMatch( var err error mergedQuery := search.MergeWildcardQueries(queries) for { - event, pos, matchingQuery, err = self.DeserializeWildcardMatch( + event, pos, matchingQuery, err = self.DeserializeWildcardMatchWithTimeInterval( self.buf[self.start:self.end], - timeInterval, mergedQuery, + timeInterval, ) if IncompleteIr != err { break diff --git a/ir/reader_test.go b/ir/reader_test.go index a37cd32..4d7a439 100644 --- a/ir/reader_test.go +++ b/ir/reader_test.go @@ -42,9 +42,9 @@ func TestIrReader(t *testing.T) { // log, err = irr.Read() // log, err = irr.ReadToContains("ERROR") // var _ search.WildcardQuery - log, _, err = irr.ReadToWildcardMatch( - interval, + log, _, err = irr.ReadToWildcardMatchWithTimeInterval( queries, + interval, ) if nil != err { break