diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8f060dc48cf..4b179b38144 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,7 @@ CHANGELOG* /libbeat/ @elastic/elastic-agent-data-plane /libbeat/docs/processors-list.asciidoc @elastic/ingest-docs /libbeat/management @elastic/elastic-agent-control-plane -/libbeat/processors/add_cloud_metadata @elastic/obs-cloud-monitoring +/libbeat/processors/add_cloud_metadata @elastic/obs-ds-hosted-services /libbeat/processors/add_kubernetes_metadata @elastic/obs-cloudnative-monitoring /libbeat/processors/cache/ @elastic/security-service-integrations /libbeat/processors/community_id/ @elastic/sec-deployment-and-devices @@ -116,10 +116,10 @@ CHANGELOG* /x-pack/filebeat @elastic/elastic-agent-data-plane /x-pack/filebeat/docs/ # Listed without an owner to avoid maintaining doc ownership for each input and module. /x-pack/filebeat/docs/inputs/input-salesforce.asciidoc @elastic/obs-infraobs-integrations -/x-pack/filebeat/input/awscloudwatch/ @elastic/obs-cloud-monitoring -/x-pack/filebeat/input/awss3/ @elastic/obs-cloud-monitoring +/x-pack/filebeat/input/awscloudwatch/ @elastic/obs-ds-hosted-services +/x-pack/filebeat/input/awss3/ @elastic/obs-ds-hosted-services /x-pack/filebeat/input/azureblobstorage/ @elastic/security-service-integrations -/x-pack/filebeat/input/azureeventhub/ @elastic/obs-cloud-monitoring +/x-pack/filebeat/input/azureeventhub/ @elastic/obs-ds-hosted-services /x-pack/filebeat/input/cel/ @elastic/security-service-integrations /x-pack/filebeat/input/cometd/ @elastic/obs-infraobs-integrations /x-pack/filebeat/input/entityanalytics/ @elastic/security-service-integrations @@ -137,9 +137,9 @@ CHANGELOG* /x-pack/filebeat/input/salesforce @elastic/obs-infraobs-integrations /x-pack/filebeat/input/streaming/ @elastic/security-service-integrations /x-pack/filebeat/module/activemq @elastic/obs-infraobs-integrations -/x-pack/filebeat/module/aws @elastic/obs-cloud-monitoring -/x-pack/filebeat/module/awsfargate @elastic/obs-cloud-monitoring -/x-pack/filebeat/module/azure @elastic/obs-cloud-monitoring +/x-pack/filebeat/module/aws @elastic/obs-ds-hosted-services +/x-pack/filebeat/module/awsfargate @elastic/obs-ds-hosted-services +/x-pack/filebeat/module/azure @elastic/obs-ds-hosted-services /x-pack/filebeat/module/barracuda @elastic/security-service-integrations /x-pack/filebeat/module/bluecoat @elastic/sec-deployment-and-devices /x-pack/filebeat/module/cef @elastic/sec-deployment-and-devices diff --git a/CHANGELOG-developer.next.asciidoc b/CHANGELOG-developer.next.asciidoc index 0998beb9f7d..15b46777965 100644 --- a/CHANGELOG-developer.next.asciidoc +++ b/CHANGELOG-developer.next.asciidoc @@ -213,6 +213,7 @@ The list below covers the major changes between 7.0.0-rc2 and main only. - Add field redaction package. {pull}40997[40997] - Add support for marked redaction to x-pack/filebeat/input/internal/private {pull}41212[41212] - Add support for collecting Okta role and factor data for users with filebeat entityanalytics input. {pull}41044[41044] +- Add CEL input program evaluation coverage collection support. {pull}41884[41884] ==== Deprecated diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 140a231e2c8..7af434990cf 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -441,6 +441,11 @@ filebeat.inputs: === Beats version 8.14.3 https://github.com/elastic/beats/compare/v8.14.2\...v8.14.3[View commits] +==== Known Issues + +*Filebeat* + - Filestream input will resend files that have been inactive for 30min or more. Workaround: set `clean_inactive` to a very high value, like 5 years: `clean_inactive: 43800h0m0s`. {issue}40178[40178] + ==== Bugfixes *Filebeat* @@ -471,6 +476,11 @@ https://github.com/elastic/beats/compare/v8.14.2\...v8.14.3[View commits] === Beats version 8.14.2 https://github.com/elastic/beats/compare/v8.14.1\...v8.14.2[View commits] +==== Known Issues + +*Filebeat* + - Filestream input will resend files that have been inactive for 30min or more. Workaround: set `clean_inactive` to a very high value, like 5 years: `clean_inactive: 43800h0m0s`. {issue}40178[40178] + ==== Breaking changes *Filebeat* @@ -507,6 +517,11 @@ https://github.com/elastic/beats/compare/v8.14.1\...v8.14.2[View commits] === Beats version 8.14.1 https://github.com/elastic/beats/compare/v8.14.0\...v8.14.1[View commits] +==== Known Issues + +*Filebeat* + - Filestream input will resend files that have been inactive for 30min or more. Workaround: set `clean_inactive` to a very high value, like 5 years: `clean_inactive: 43800h0m0s`. {issue}40178[40178] + ==== Bugfixes *Heartbeat* @@ -518,6 +533,11 @@ https://github.com/elastic/beats/compare/v8.14.0\...v8.14.1[View commits] === Beats version 8.14.0 https://github.com/elastic/beats/compare/v8.13.4\...v8.14.0[View commits] +==== Known Issues + +*Filebeat* + - Filestream input will resend files that have been inactive for 30min or more. Workaround: set `clean_inactive` to a very high value, like 5 years: `clean_inactive: 43800h0m0s`. {issue}40178[40178] + ==== Breaking changes *Filebeat* @@ -2911,6 +2931,7 @@ https://github.com/elastic/beats/compare/v7.17.0\...v8.0.0[View commits] - Add `while_pattern` type to multiline reader. {pull}19662[19662] - auditd dataset: Use `process.args` to store program arguments instead of `auditd.log.aNNN` fields. {pull}29601[29601] - Remove deprecated old `awscloudwatch` input name. {pull}29844[29844] +- Remove `docker` input. Please use `filestream` input with `container` parser or `container` input. {pull}28817[28817] *Metricbeat* diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index 5721762aa96..bb6c4df0f62 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -115,8 +115,11 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Ensure Elasticsearch output can always recover from network errors {pull}40794[40794] - Add `translate_ldap_attribute` processor. {pull}41472[41472] - Remove unnecessary debug logs during idle connection teardown {issue}40824[40824] +- Remove unnecessary reload for Elastic Agent managed beats when apm tracing config changes from nil to nil {pull}41794[41794] - Fix incorrect cloud provider identification in add_cloud_metadata processor using provider priority mechanism {pull}41636[41636] - Prevent panic if libbeat processors are loaded more than once. {issue}41475[41475] {pull}41857[51857] +- Allow network condition to handle field values that are arrays of IP addresses. {pull}41918[41918] +- Fix a bug where log files are rotated on startup when interval is configured and rotateonstartup is disabled {issue}41894[41894] {pull}41895[41895] *Auditbeat* @@ -187,6 +190,8 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Fix missing key in streaming input logging. {pull}41600[41600] - Improve S3 object size metric calculation to support situations where Content-Length is not available. {pull}41755[41755] - Fix handling of http_endpoint request exceeding memory limits. {issue}41764[41764] {pull}41765[41765] +- Rate limiting fixes in the Okta provider of the Entity Analytics input. {issue}40106[40106] {pull}41583[41583] +- Redact authorization headers in HTTPJSON debug logs. {pull}41920[41920] *Heartbeat* @@ -353,7 +358,10 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Refactor & cleanup with updates to default values and documentation. {pull}41834[41834] - Update CEL mito extensions to v1.16.0. {pull}41727[41727] - Add evaluation state dump debugging option to CEL input. {pull}41335[41335] +- Added support for retry configuration in GCS input. {issue}11580[11580] {pull}41862[41862] - Improve S3 polling mode states registry when using list prefix option. {pull}41869[41869] +- AWS S3 input registry cleanup for untracked s3 objects. {pull}41694[41694] +- The environment variable `BEATS_AZURE_EVENTHUB_INPUT_TRACING_ENABLED: true` enables internal logs tracer for the azure-eventhub input. {issue}41931[41931] {pull}41932[41932] *Auditbeat* @@ -407,6 +415,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Add support for location label as an optional configuration parameter in GCP metrics metricset. {issue}41550[41550] {pull}41626[41626] *Metricbeat* +- Add benchmark module {pull}41801[41801] *Osquerybeat* @@ -425,6 +434,7 @@ https://github.com/elastic/beats/compare/v8.8.1\...main[Check the HEAD diff] - Language setting also added to decode xml wineventlog processor {pull}41525[41525] - Format embedded messages in the experimental api {pull}41525[41525] - Implement exclusion range support for event_id. {issue}38623[38623] {pull}41639[41639] +- Make the experimental API GA and rename it to winlogbeat-raw {issue}39580[39580] {pull}41770[41770] *Functionbeat* diff --git a/NOTICE.txt b/NOTICE.txt index e19f4d4fcbe..8ffa2dc1790 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -13344,11 +13344,11 @@ SOFTWARE -------------------------------------------------------------------------------- Dependency : github.com/elastic/elastic-agent-libs -Version: v0.17.3 +Version: v0.17.4 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-libs@v0.17.3/LICENSE: +Contents of probable licence file $GOMODCACHE/github.com/elastic/elastic-agent-libs@v0.17.4/LICENSE: Apache License Version 2.0, January 2004 @@ -18169,218 +18169,6 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------------------------------- -Dependency : github.com/golang/mock -Version: v1.6.0 -Licence type (autodetected): Apache-2.0 --------------------------------------------------------------------------------- - -Contents of probable licence file $GOMODCACHE/github.com/golang/mock@v1.6.0/LICENSE: - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - 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. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - -------------------------------------------------------------------------------- Dependency : github.com/golang/snappy Version: v0.0.4 @@ -25345,24 +25133,236 @@ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +-------------------------------------------------------------------------------- +Dependency : go.mongodb.org/mongo-driver +Version: v1.14.0 +Licence type (autodetected): Apache-2.0 +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/go.mongodb.org/mongo-driver@v1.14.0/LICENSE: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. -------------------------------------------------------------------------------- -Dependency : go.mongodb.org/mongo-driver -Version: v1.14.0 +Dependency : go.opentelemetry.io/collector/component +Version: v0.109.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.mongodb.org/mongo-driver@v1.14.0/LICENSE: +Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/component@v0.109.0/LICENSE: + Apache License Version 2.0, January 2004 @@ -25568,12 +25568,12 @@ Contents of probable licence file $GOMODCACHE/go.mongodb.org/mongo-driver@v1.14. -------------------------------------------------------------------------------- -Dependency : go.opentelemetry.io/collector/component +Dependency : go.opentelemetry.io/collector/consumer Version: v0.109.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/component@v0.109.0/LICENSE: +Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/consumer@v0.109.0/LICENSE: Apache License @@ -25780,12 +25780,12 @@ Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/comp -------------------------------------------------------------------------------- -Dependency : go.opentelemetry.io/collector/consumer -Version: v0.109.0 +Dependency : go.opentelemetry.io/collector/pdata +Version: v1.15.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/consumer@v0.109.0/LICENSE: +Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/pdata@v1.15.0/LICENSE: Apache License @@ -25992,12 +25992,12 @@ Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/cons -------------------------------------------------------------------------------- -Dependency : go.opentelemetry.io/collector/pdata -Version: v1.15.0 +Dependency : go.opentelemetry.io/collector/receiver +Version: v0.109.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/pdata@v1.15.0/LICENSE: +Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/receiver@v0.109.0/LICENSE: Apache License @@ -26204,12 +26204,12 @@ Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/pdat -------------------------------------------------------------------------------- -Dependency : go.opentelemetry.io/collector/receiver -Version: v0.109.0 +Dependency : go.uber.org/mock +Version: v0.5.0 Licence type (autodetected): Apache-2.0 -------------------------------------------------------------------------------- -Contents of probable licence file $GOMODCACHE/go.opentelemetry.io/collector/receiver@v0.109.0/LICENSE: +Contents of probable licence file $GOMODCACHE/go.uber.org/mock@v0.5.0/LICENSE: Apache License @@ -42489,6 +42489,218 @@ third-party archives. limitations under the License. +-------------------------------------------------------------------------------- +Dependency : github.com/golang/mock +Version: v1.6.0 +Licence type (autodetected): Apache-2.0 +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/golang/mock@v1.6.0/LICENSE: + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -------------------------------------------------------------------------------- Dependency : github.com/golang/protobuf Version: v1.5.4 diff --git a/filebeat/autodiscover/builder/hints/logs_test.go b/filebeat/autodiscover/builder/hints/logs_test.go index 5e76c8d5344..402ceadfde4 100644 --- a/filebeat/autodiscover/builder/hints/logs_test.go +++ b/filebeat/autodiscover/builder/hints/logs_test.go @@ -18,6 +18,7 @@ package hints import ( + "os" "path/filepath" "testing" @@ -30,8 +31,10 @@ import ( "github.com/elastic/elastic-agent-libs/paths" ) -func TestMain(t *testing.M) { +func TestMain(m *testing.M) { InitializeModule() + + os.Exit(m.Run()) } func TestGenerateHints(t *testing.T) { diff --git a/filebeat/beater/filebeat.go b/filebeat/beater/filebeat.go index ceab21aa359..4909941b90a 100644 --- a/filebeat/beater/filebeat.go +++ b/filebeat/beater/filebeat.go @@ -272,10 +272,18 @@ func (fb *Filebeat) Run(b *beat.Beat) error { waitEvents := newSignalWait() // count active events for waiting on shutdown + var reg *monitoring.Registry + + if b.Info.Monitoring.Namespace != nil { + reg = b.Info.Monitoring.Namespace.GetRegistry().GetRegistry("stats") + if reg == nil { + reg = b.Info.Monitoring.Namespace.GetRegistry().NewRegistry("stats") + } + } wgEvents := &eventCounter{ - count: monitoring.NewInt(nil, "filebeat.events.active"), // Gauge - added: monitoring.NewUint(nil, "filebeat.events.added"), - done: monitoring.NewUint(nil, "filebeat.events.done"), + count: monitoring.NewInt(reg, "filebeat.events.active"), // Gauge + added: monitoring.NewUint(reg, "filebeat.events.added"), + done: monitoring.NewUint(reg, "filebeat.events.done"), } finishedLogger := newFinishedLogger(wgEvents) diff --git a/filebeat/channel/outlet.go b/filebeat/channel/outlet.go index fd5c9b12fc1..25747046396 100644 --- a/filebeat/channel/outlet.go +++ b/filebeat/channel/outlet.go @@ -18,8 +18,9 @@ package channel import ( + "sync/atomic" + "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" ) type outlet struct { @@ -31,15 +32,14 @@ type outlet struct { func newOutlet(client beat.Client) *outlet { o := &outlet{ client: client, - isOpen: atomic.MakeBool(true), done: make(chan struct{}), } + o.isOpen.Store(true) return o } func (o *outlet) Close() error { - isOpen := o.isOpen.Swap(false) - if isOpen { + if o.isOpen.Swap(false) { close(o.done) return o.client.Close() } diff --git a/filebeat/input/filestream/input_integration_test.go b/filebeat/input/filestream/input_integration_test.go index 79658970c3f..80327d8bcf2 100644 --- a/filebeat/input/filestream/input_integration_test.go +++ b/filebeat/input/filestream/input_integration_test.go @@ -30,6 +30,7 @@ import ( "testing" "time" + "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/require" "golang.org/x/text/encoding" "golang.org/x/text/encoding/unicode" @@ -49,8 +50,9 @@ func TestFilestreamCloseRenamed(t *testing.T) { // than close.on_state_change.check_interval to make sure // the Harvester detects the rename first thus allowing // the output to receive the event and then close the source file. + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName) + "*"}, "prospector.scanner.check_interval": "10ms", "close.on_state_change.check_interval": "1ms", @@ -65,7 +67,7 @@ func TestFilestreamCloseRenamed(t *testing.T) { // first event has made it successfully env.waitUntilEventCount(1) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) testlogNameRotated := "test.log.rotated" env.mustRenameFile(testlogName, testlogNameRotated) @@ -78,8 +80,8 @@ func TestFilestreamCloseRenamed(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogNameRotated, "fake-ID", len(testlines)) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(newerTestlines)) + env.requireOffsetInRegistry(testlogNameRotated, id, len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(newerTestlines)) } func TestFilestreamMetadataUpdatedOnRename(t *testing.T) { @@ -90,8 +92,9 @@ func TestFilestreamMetadataUpdatedOnRename(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName) + "*"}, "prospector.scanner.check_interval": "1ms", }) @@ -103,20 +106,20 @@ func TestFilestreamMetadataUpdatedOnRename(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(1) - env.waitUntilMetaInRegistry(testlogName, "fake-ID", fileMeta{Source: env.abspath(testlogName), IdentifierName: "native"}) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testline)) + env.waitUntilMetaInRegistry(testlogName, id, fileMeta{Source: env.abspath(testlogName), IdentifierName: "native"}) + env.requireOffsetInRegistry(testlogName, id, len(testline)) testlogNameRenamed := "test.log.renamed" env.mustRenameFile(testlogName, testlogNameRenamed) // check if the metadata is updated and cursor data stays the same - env.waitUntilMetaInRegistry(testlogNameRenamed, "fake-ID", fileMeta{Source: env.abspath(testlogNameRenamed), IdentifierName: "native"}) - env.requireOffsetInRegistry(testlogNameRenamed, "fake-ID", len(testline)) + env.waitUntilMetaInRegistry(testlogNameRenamed, id, fileMeta{Source: env.abspath(testlogNameRenamed), IdentifierName: "native"}) + env.requireOffsetInRegistry(testlogNameRenamed, id, len(testline)) env.mustAppendToFile(testlogNameRenamed, testline) env.waitUntilEventCount(2) - env.requireOffsetInRegistry(testlogNameRenamed, "fake-ID", len(testline)*2) + env.requireOffsetInRegistry(testlogNameRenamed, id, len(testline)*2) cancelInput() env.waitUntilInputStops() @@ -127,8 +130,9 @@ func TestFilestreamCloseRemoved(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName) + "*"}, "prospector.scanner.check_interval": "24h", "close.on_state_change.check_interval": "1ms", @@ -144,7 +148,7 @@ func TestFilestreamCloseRemoved(t *testing.T) { // first event has made it successfully env.waitUntilEventCount(1) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) fi, err := os.Stat(env.abspath(testlogName)) if err != nil { @@ -158,8 +162,8 @@ func TestFilestreamCloseRemoved(t *testing.T) { cancelInput() env.waitUntilInputStops() - id := getIDFromPath(env.abspath(testlogName), "fake-ID", fi) - env.requireOffsetInRegistryByID(id, len(testlines)) + idFromPath := getIDFromPath(env.abspath(testlogName), id, fi) + env.requireOffsetInRegistryByID(idFromPath, len(testlines)) } // test_close_eof from test_harvester.py @@ -167,8 +171,9 @@ func TestFilestreamCloseEOF(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "24h", "close.reader.on_eof": "true", @@ -183,7 +188,7 @@ func TestFilestreamCloseEOF(t *testing.T) { // first event has made it successfully env.waitUntilEventCount(1) - env.requireOffsetInRegistry(testlogName, "fake-ID", expectedOffset) + env.requireOffsetInRegistry(testlogName, id, expectedOffset) // the second log line will not be picked up as scan_interval is set to one day. env.mustWriteToFile(testlogName, []byte("first line\nsecond log line\n")) @@ -194,7 +199,7 @@ func TestFilestreamCloseEOF(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogName, "fake-ID", expectedOffset) + env.requireOffsetInRegistry(testlogName, id, expectedOffset) } // test_empty_lines from test_harvester.py @@ -202,8 +207,9 @@ func TestFilestreamEmptyLine(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "1ms", }) @@ -215,7 +221,7 @@ func TestFilestreamEmptyLine(t *testing.T) { env.mustWriteToFile(testlogName, testlines) env.waitUntilEventCount(2) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) moreTestlines := []byte("\nafter an empty line\n") env.mustAppendToFile(testlogName, moreTestlines) @@ -230,7 +236,7 @@ func TestFilestreamEmptyLine(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)+len(moreTestlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)+len(moreTestlines)) } // test_empty_lines_only from test_harvester.py @@ -240,8 +246,9 @@ func TestFilestreamEmptyLinesOnly(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "1ms", }) @@ -255,7 +262,7 @@ func TestFilestreamEmptyLinesOnly(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireNoEntryInRegistry(testlogName, "fake-ID") + env.requireNoEntryInRegistry(testlogName, id) } // test_bom_utf8 from test_harvester.py @@ -263,8 +270,9 @@ func TestFilestreamBOMUTF8(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, }) @@ -305,8 +313,9 @@ func TestFilestreamUTF16BOMs(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "encoding": name, }) @@ -337,8 +346,9 @@ func TestFilestreamCloseTimeout(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "24h", "close.on_state_change.check_interval": "100ms", @@ -352,7 +362,7 @@ func TestFilestreamCloseTimeout(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(1) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.waitUntilHarvesterIsDone() env.mustWriteToFile(testlogName, []byte("first line\nsecond log line\n")) @@ -362,7 +372,7 @@ func TestFilestreamCloseTimeout(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) } // test_close_inactive from test_input.py @@ -370,8 +380,9 @@ func TestFilestreamCloseAfterInterval(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "24h", "close.on_state_change.check_interval": "100ms", @@ -385,7 +396,7 @@ func TestFilestreamCloseAfterInterval(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.waitUntilHarvesterIsDone() cancelInput() @@ -397,8 +408,9 @@ func TestFilestreamCloseAfterIntervalRemoved(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "24h", "close.on_state_change.check_interval": "10ms", @@ -415,7 +427,7 @@ func TestFilestreamCloseAfterIntervalRemoved(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.mustRemoveFile(testlogName) @@ -429,8 +441,9 @@ func TestFilestreamCloseAfterIntervalRenamed(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "24h", "close.on_state_change.check_interval": "10ms", @@ -447,7 +460,7 @@ func TestFilestreamCloseAfterIntervalRenamed(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) newFileName := "test_rotated.log" env.mustRenameFile(testlogName, newFileName) @@ -463,8 +476,9 @@ func TestFilestreamCloseAfterIntervalRotatedAndRemoved(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "24h", "close.on_state_change.check_interval": "10ms", @@ -481,7 +495,7 @@ func TestFilestreamCloseAfterIntervalRotatedAndRemoved(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) newFileName := "test_rotated.log" env.mustRenameFile(testlogName, newFileName) @@ -498,8 +512,9 @@ func TestFilestreamCloseAfterIntervalRotatedAndNewRemoved(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "1ms", "close.on_state_change.check_interval": "10ms", @@ -516,7 +531,7 @@ func TestFilestreamCloseAfterIntervalRotatedAndNewRemoved(t *testing.T) { env.startInput(ctx, inp) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) newFileName := "test_rotated.log" env.mustRenameFile(testlogName, newFileName) @@ -541,8 +556,9 @@ func TestFilestreamTruncatedFileOpen(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "1ms", "prospector.scanner.resend_on_touch": "true", @@ -555,7 +571,7 @@ func TestFilestreamTruncatedFileOpen(t *testing.T) { env.mustWriteToFile(testlogName, testlines) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.mustTruncateFile(testlogName, 0) time.Sleep(5 * time.Millisecond) @@ -566,7 +582,7 @@ func TestFilestreamTruncatedFileOpen(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogName, "fake-ID", len(truncatedTestLines)) + env.requireOffsetInRegistry(testlogName, id, len(truncatedTestLines)) } // test_truncated_file_closed from test_harvester.py @@ -574,8 +590,9 @@ func TestFilestreamTruncatedFileClosed(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "1ms", "prospector.scanner.resend_on_touch": "true", @@ -589,7 +606,7 @@ func TestFilestreamTruncatedFileClosed(t *testing.T) { env.mustWriteToFile(testlogName, testlines) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.waitUntilHarvesterIsDone() @@ -602,7 +619,7 @@ func TestFilestreamTruncatedFileClosed(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogName, "fake-ID", len(truncatedTestLines)) + env.requireOffsetInRegistry(testlogName, id, len(truncatedTestLines)) } // test_truncate from test_harvester.py @@ -611,8 +628,9 @@ func TestFilestreamTruncateWithSymlink(t *testing.T) { testlogName := "test.log" symlinkName := "test.log.symlink" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{ env.abspath(testlogName), env.abspath(symlinkName), @@ -632,18 +650,18 @@ func TestFilestreamTruncateWithSymlink(t *testing.T) { env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(lines)) + env.requireOffsetInRegistry(testlogName, id, len(lines)) // remove symlink env.mustRemoveFile(symlinkName) env.mustTruncateFile(testlogName, 0) - env.waitUntilOffsetInRegistry(testlogName, "fake-ID", 0, 10*time.Second) + env.waitUntilOffsetInRegistry(testlogName, id, 0, 10*time.Second) moreLines := []byte("forth line\nfifth line\n") env.mustWriteToFile(testlogName, moreLines) env.waitUntilEventCount(5) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(moreLines)) + env.requireOffsetInRegistry(testlogName, id, len(moreLines)) cancelInput() env.waitUntilInputStops() @@ -655,8 +673,9 @@ func TestFilestreamTruncateBigScannerInterval(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "5s", "prospector.scanner.resend_on_touch": "true", @@ -669,7 +688,7 @@ func TestFilestreamTruncateBigScannerInterval(t *testing.T) { env.mustWriteToFile(testlogName, testlines) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.mustTruncateFile(testlogName, 0) @@ -686,8 +705,9 @@ func TestFilestreamTruncateCheckOffset(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "1ms", "prospector.scanner.resend_on_touch": "true", @@ -700,11 +720,11 @@ func TestFilestreamTruncateCheckOffset(t *testing.T) { env.mustWriteToFile(testlogName, testlines) env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) env.mustTruncateFile(testlogName, 0) - env.waitUntilOffsetInRegistry(testlogName, "fake-ID", 0, 10*time.Second) + env.waitUntilOffsetInRegistry(testlogName, id, 0, 10*time.Second) cancelInput() env.waitUntilInputStops() @@ -715,8 +735,9 @@ func TestFilestreamTruncateBlockedOutput(t *testing.T) { env.pipeline = &mockPipelineConnector{blocking: true} testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{env.abspath(testlogName)}, "prospector.scanner.check_interval": "200ms", }) @@ -734,7 +755,7 @@ func TestFilestreamTruncateBlockedOutput(t *testing.T) { env.pipeline.clients[0].canceler() env.waitUntilEventCount(2) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) // extra lines are appended after first line is processed // so it can interfere with the truncation of the file @@ -742,7 +763,7 @@ func TestFilestreamTruncateBlockedOutput(t *testing.T) { env.mustTruncateFile(testlogName, 0) - env.waitUntilOffsetInRegistry(testlogName, "fake-ID", 0, 10*time.Second) + env.waitUntilOffsetInRegistry(testlogName, id, 0, 10*time.Second) // all newly started client has to be cancelled so events can be processed env.pipeline.cancelAllClients() @@ -753,7 +774,7 @@ func TestFilestreamTruncateBlockedOutput(t *testing.T) { env.mustWriteToFile(testlogName, truncatedTestLines) env.waitUntilEventCount(3) - env.waitUntilOffsetInRegistry(testlogName, "fake-ID", len(truncatedTestLines), 10*time.Second) + env.waitUntilOffsetInRegistry(testlogName, id, len(truncatedTestLines), 10*time.Second) cancelInput() env.waitUntilInputStops() @@ -765,8 +786,9 @@ func TestFilestreamSymlinksEnabled(t *testing.T) { testlogName := "test.log" symlinkName := "test.log.symlink" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{ env.abspath(symlinkName), }, @@ -786,7 +808,7 @@ func TestFilestreamSymlinksEnabled(t *testing.T) { cancelInput() env.waitUntilInputStops() - env.requireOffsetInRegistry(testlogName, "fake-ID", len(testlines)) + env.requireOffsetInRegistry(testlogName, id, len(testlines)) } // test_symlink_rotated from test_harvester.py @@ -796,8 +818,9 @@ func TestFilestreamSymlinkRotated(t *testing.T) { firstTestlogName := "test1.log" secondTestlogName := "test2.log" symlinkName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{ env.abspath(symlinkName), }, @@ -820,7 +843,7 @@ func TestFilestreamSymlinkRotated(t *testing.T) { env.waitUntilEventCount(1) expectedOffset := len(commonLine) + 2 - env.requireOffsetInRegistry(firstTestlogName, "fake-ID", expectedOffset) + env.requireOffsetInRegistry(firstTestlogName, id, expectedOffset) // rotate symlink env.mustRemoveFile(symlinkName) @@ -830,8 +853,8 @@ func TestFilestreamSymlinkRotated(t *testing.T) { env.mustAppendToFile(secondTestlogName, []byte(moreLines)) env.waitUntilEventCount(4) - env.requireOffsetInRegistry(firstTestlogName, "fake-ID", expectedOffset) - env.requireOffsetInRegistry(secondTestlogName, "fake-ID", expectedOffset+len(moreLines)) + env.requireOffsetInRegistry(firstTestlogName, id, expectedOffset) + env.requireOffsetInRegistry(secondTestlogName, id, expectedOffset+len(moreLines)) cancelInput() env.waitUntilInputStops() @@ -845,8 +868,9 @@ func TestFilestreamSymlinkRemoved(t *testing.T) { testlogName := "test.log" symlinkName := "test.log.symlink" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{ env.abspath(symlinkName), }, @@ -866,7 +890,7 @@ func TestFilestreamSymlinkRemoved(t *testing.T) { env.waitUntilEventCount(1) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(line)) + env.requireOffsetInRegistry(testlogName, id, len(line)) // remove symlink env.mustRemoveFile(symlinkName) @@ -874,7 +898,7 @@ func TestFilestreamSymlinkRemoved(t *testing.T) { env.mustAppendToFile(testlogName, line) env.waitUntilEventCount(2) - env.requireOffsetInRegistry(testlogName, "fake-ID", 2*len(line)) + env.requireOffsetInRegistry(testlogName, id, 2*len(line)) cancelInput() env.waitUntilInputStops() @@ -888,8 +912,9 @@ func TestFilestreamTruncate(t *testing.T) { testlogName := "test.log" symlinkName := "test.log.symlink" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "fake-ID", + "id": id, "paths": []string{ env.abspath("*"), }, @@ -908,12 +933,12 @@ func TestFilestreamTruncate(t *testing.T) { env.waitUntilEventCount(3) - env.requireOffsetInRegistry(testlogName, "fake-ID", len(lines)) + env.requireOffsetInRegistry(testlogName, id, len(lines)) // remove symlink env.mustRemoveFile(symlinkName) env.mustTruncateFile(testlogName, 0) - env.waitUntilOffsetInRegistry(testlogName, "fake-ID", 0, 10*time.Second) + env.waitUntilOffsetInRegistry(testlogName, id, 0, 10*time.Second) // recreate symlink env.mustSymlink(testlogName, symlinkName) @@ -921,7 +946,7 @@ func TestFilestreamTruncate(t *testing.T) { moreLines := []byte("forth line\nfifth line\n") env.mustWriteToFile(testlogName, moreLines) - env.waitUntilOffsetInRegistry(testlogName, "fake-ID", len(moreLines), 10*time.Second) + env.waitUntilOffsetInRegistry(testlogName, id, len(moreLines), 10*time.Second) cancelInput() env.waitUntilInputStops() @@ -982,8 +1007,9 @@ func TestRotatingCloseInactiveLargerWriteRate(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "my-id", + "id": id, "paths": []string{ env.abspath("*"), }, @@ -1028,8 +1054,9 @@ func TestRotatingCloseInactiveLowWriteRate(t *testing.T) { env := newInputTestingEnvironment(t) testlogName := "test.log" + id := "fake-ID-" + uuid.Must(uuid.NewV4()).String() inp := env.mustCreateInput(map[string]interface{}{ - "id": "my-id", + "id": id, "paths": []string{ env.abspath("*"), }, diff --git a/filebeat/input/filestream/internal/input-logfile/harvester_test.go b/filebeat/input/filestream/internal/input-logfile/harvester_test.go index d8800c85996..6f5938e308a 100644 --- a/filebeat/input/filestream/internal/input-logfile/harvester_test.go +++ b/filebeat/input/filestream/internal/input-logfile/harvester_test.go @@ -23,6 +23,7 @@ import ( "fmt" "strings" "sync" + "sync/atomic" "testing" "time" @@ -32,7 +33,6 @@ import ( "github.com/elastic/beats/v7/filebeat/input/filestream/internal/task" input "github.com/elastic/beats/v7/filebeat/input/v2" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/tests/resources" "github.com/elastic/elastic-agent-libs/logp" ) @@ -128,7 +128,7 @@ func TestDefaultHarvesterGroup(t *testing.T) { t.Run("assert a harvester is only started if harvester limit haven't been reached", func(t *testing.T) { var wg sync.WaitGroup - var harvesterRunningCount atomic.Int + var harvesterRunningCount atomic.Int64 var harvester1Finished, harvester2Finished atomic.Bool done1, done2 := make(chan struct{}), make(chan struct{}) diff --git a/filebeat/input/filestream/internal/input-logfile/store.go b/filebeat/input/filestream/internal/input-logfile/store.go index 024ca5c9bfd..14c7869d087 100644 --- a/filebeat/input/filestream/internal/input-logfile/store.go +++ b/filebeat/input/filestream/internal/input-logfile/store.go @@ -21,9 +21,9 @@ import ( "fmt" "strings" "sync" + "sync/atomic" "time" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/common/cleanup" "github.com/elastic/beats/v7/libbeat/common/transform/typeconv" "github.com/elastic/beats/v7/libbeat/statestore" @@ -461,14 +461,14 @@ func (r *resource) isDeleted() bool { // Retain is used to indicate that 'resource' gets an additional 'owner'. // Owners of an resource can be active inputs or pending update operations // not yet written to disk. -func (r *resource) Retain() { r.pending.Inc() } +func (r *resource) Retain() { r.pending.Add(1) } // Release reduced the owner ship counter of the resource. -func (r *resource) Release() { r.pending.Dec() } +func (r *resource) Release() { r.pending.Add(^uint64(0)) } // UpdatesReleaseN is used to release ownership of N pending update operations. func (r *resource) UpdatesReleaseN(n uint) { - r.pending.Sub(uint64(n)) + r.pending.Add(^uint64(n - 1)) } // Finished returns true if the resource is not in use and if there are no pending updates diff --git a/filebeat/input/kafka/input.go b/filebeat/input/kafka/input.go index 6cc74514bef..3f330587f82 100644 --- a/filebeat/input/kafka/input.go +++ b/filebeat/input/kafka/input.go @@ -25,9 +25,9 @@ import ( "io" "strings" "sync" + "sync/atomic" "time" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/mapstr" input "github.com/elastic/beats/v7/filebeat/input/v2" @@ -391,9 +391,10 @@ func (l *listFromFieldReader) Next() (reader.Message, error) { timestamp, kafkaFields := composeEventMetadata(l.claim, l.groupHandler, msg) messages := l.parseMultipleMessages(msg.Value) - neededAcks := atomic.MakeInt(len(messages)) + neededAcks := atomic.Int64{} + neededAcks.Add(int64(len(messages))) ackHandler := func() { - if neededAcks.Dec() == 0 { + if neededAcks.Add(-1) == 0 { l.groupHandler.ack(msg) } } diff --git a/filebeat/input/log/input.go b/filebeat/input/log/input.go index ad93632b372..e2090f6ebb8 100644 --- a/filebeat/input/log/input.go +++ b/filebeat/input/log/input.go @@ -25,6 +25,7 @@ import ( "sort" "strings" "sync" + "sync/atomic" "time" "github.com/gofrs/uuid/v5" @@ -35,7 +36,6 @@ import ( "github.com/elastic/beats/v7/filebeat/input/file" "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/beats/v7/libbeat/common" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/common/cfgwarn" "github.com/elastic/beats/v7/libbeat/management/status" conf "github.com/elastic/elastic-agent-libs/config" @@ -737,8 +737,8 @@ func (p *Input) createHarvester(logger *logp.Logger, state file.State, onTermina // startHarvester starts a new harvester with the given offset // In case the HarvesterLimit is reached, an error is returned func (p *Input) startHarvester(logger *logp.Logger, state file.State, offset int64) error { - if p.numHarvesters.Inc() > p.config.HarvesterLimit && p.config.HarvesterLimit > 0 { - p.numHarvesters.Dec() + if p.numHarvesters.Add(1) > p.config.HarvesterLimit && p.config.HarvesterLimit > 0 { + p.numHarvesters.Add(^uint32(0)) harvesterSkipped.Add(1) return errHarvesterLimit } @@ -747,15 +747,15 @@ func (p *Input) startHarvester(logger *logp.Logger, state file.State, offset int state.Offset = offset // Create harvester with state - h, err := p.createHarvester(logger, state, func() { p.numHarvesters.Dec() }) + h, err := p.createHarvester(logger, state, func() { p.numHarvesters.Add(^uint32(0)) }) if err != nil { - p.numHarvesters.Dec() + p.numHarvesters.Add(^uint32(0)) return err } err = h.Setup() if err != nil { - p.numHarvesters.Dec() + p.numHarvesters.Add(^uint32(0)) return fmt.Errorf("error setting up harvester: %w", err) } @@ -765,7 +765,7 @@ func (p *Input) startHarvester(logger *logp.Logger, state file.State, offset int h.SendStateUpdate() if err = p.harvesters.Start(h); err != nil { - p.numHarvesters.Dec() + p.numHarvesters.Add(^uint32(0)) } return err } diff --git a/filebeat/input/v2/input-cursor/store.go b/filebeat/input/v2/input-cursor/store.go index cc755f046ca..a53bc77a79f 100644 --- a/filebeat/input/v2/input-cursor/store.go +++ b/filebeat/input/v2/input-cursor/store.go @@ -20,9 +20,9 @@ package cursor import ( "strings" "sync" + "sync/atomic" "time" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/common/cleanup" "github.com/elastic/beats/v7/libbeat/common/transform/typeconv" "github.com/elastic/beats/v7/libbeat/statestore" @@ -235,14 +235,14 @@ func (r *resource) IsNew() bool { // Retain is used to indicate that 'resource' gets an additional 'owner'. // Owners of an resource can be active inputs or pending update operations // not yet written to disk. -func (r *resource) Retain() { r.pending.Inc() } +func (r *resource) Retain() { r.pending.Add(1) } // Release reduced the owner ship counter of the resource. -func (r *resource) Release() { r.pending.Dec() } +func (r *resource) Release() { r.pending.Add(^uint64(0)) } // UpdatesReleaseN is used to release ownership of N pending update operations. func (r *resource) UpdatesReleaseN(n uint) { - r.pending.Sub(uint64(n)) + r.pending.Add(^uint64(n - 1)) } // Finished returns true if the resource is not in use and if there are no pending updates @@ -290,7 +290,7 @@ func readStates(log *logp.Logger, store *statestore.Store, prefix string) (*stat } err := store.Each(func(key string, dec statestore.ValueDecoder) (bool, error) { - if !strings.HasPrefix(string(key), keyPrefix) { + if !strings.HasPrefix(key, keyPrefix) { return true, nil } diff --git a/filebeat/input/v2/input-stateless/stateless_test.go b/filebeat/input/v2/input-stateless/stateless_test.go index 2febcb7e1b6..731577e76c3 100644 --- a/filebeat/input/v2/input-stateless/stateless_test.go +++ b/filebeat/input/v2/input-stateless/stateless_test.go @@ -22,6 +22,7 @@ import ( "errors" "runtime" "sync" + "sync/atomic" "testing" "github.com/stretchr/testify/require" @@ -29,7 +30,6 @@ import ( v2 "github.com/elastic/beats/v7/filebeat/input/v2" stateless "github.com/elastic/beats/v7/filebeat/input/v2/input-stateless" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" pubtest "github.com/elastic/beats/v7/libbeat/publisher/testing" conf "github.com/elastic/elastic-agent-libs/config" "github.com/elastic/elastic-agent-libs/mapstr" @@ -111,12 +111,12 @@ func TestStateless_Run(t *testing.T) { defer cancel() // connector creates a client the blocks forever until the shutdown signal is received - var publishCalls atomic.Int + var publishCalls atomic.Int64 connector := pubtest.FakeConnector{ ConnectFunc: func(config beat.ClientConfig) (beat.Client, error) { return &pubtest.FakeClient{ PublishFunc: func(event beat.Event) { - publishCalls.Inc() + publishCalls.Add(1) // Unlock Publish once the input has been cancelled <-ctx.Done() }, @@ -141,24 +141,24 @@ func TestStateless_Run(t *testing.T) { // validate require.Equal(t, context.Canceled, err) - require.Equal(t, 1, publishCalls.Load()) + require.Equal(t, int64(1), publishCalls.Load()) }) t.Run("do not start input of pipeline connection fails", func(t *testing.T) { errOpps := errors.New("oops") connector := pubtest.FailingConnector(errOpps) - var run atomic.Int + var run atomic.Int64 input := createConfiguredInput(t, constInputManager(&fakeStatelessInput{ OnRun: func(_ v2.Context, publisher stateless.Publisher) error { - run.Inc() + run.Add(1) return nil }, }), nil) err := input.Run(v2.Context{}, connector) require.True(t, errors.Is(err, errOpps)) - require.Equal(t, 0, run.Load()) + require.Equal(t, int64(0), run.Load()) }) } diff --git a/filebeat/inputsource/common/streaming/listener.go b/filebeat/inputsource/common/streaming/listener.go index ce0f69d030e..5816c1fa81c 100644 --- a/filebeat/inputsource/common/streaming/listener.go +++ b/filebeat/inputsource/common/streaming/listener.go @@ -26,9 +26,9 @@ import ( "net" "strings" "sync" + "sync/atomic" "github.com/elastic/beats/v7/filebeat/inputsource" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/go-concert/ctxtool" ) @@ -44,7 +44,7 @@ type Listener struct { wg sync.WaitGroup log *logp.Logger ctx ctxtool.CancelContext - clientsCount atomic.Int + clientsCount atomic.Int64 handlerFactory HandlerFactory listenerFactory ListenerFactory } @@ -190,10 +190,10 @@ func (l *Listener) handleConnection(conn net.Conn) { defer cancel() // Track number of clients. - l.clientsCount.Inc() + l.clientsCount.Add(1) log.Debugw("New client connection", "active_clients", l.clientsCount.Load()) defer func() { - l.clientsCount.Dec() + l.clientsCount.Add(-1) log.Debugw("Client disconnected", "active_clients", l.clientsCount.Load()) }() diff --git a/filebeat/processor/add_kubernetes_metadata/matchers_test.go b/filebeat/processor/add_kubernetes_metadata/matchers_test.go index 1b219127867..ccae66f8002 100644 --- a/filebeat/processor/add_kubernetes_metadata/matchers_test.go +++ b/filebeat/processor/add_kubernetes_metadata/matchers_test.go @@ -19,6 +19,7 @@ package add_kubernetes_metadata import ( "fmt" + "os" "runtime" "testing" @@ -36,6 +37,8 @@ const puid = "005f3b90-4b9d-12f8-acf0-31020a840133" func TestMain(m *testing.M) { InitializeModule() + + os.Exit(m.Run()) } func TestLogsPathMatcher_InvalidSource1(t *testing.T) { diff --git a/filebeat/tests/integration/translate_ldap_attribute_test.go b/filebeat/tests/integration/translate_ldap_attribute_test.go index e2b0f877efc..376be5e36a2 100644 --- a/filebeat/tests/integration/translate_ldap_attribute_test.go +++ b/filebeat/tests/integration/translate_ldap_attribute_test.go @@ -64,7 +64,7 @@ logging: processors: - add_fields: - fields: + fields: guid: '%s' - translate_ldap_attribute: field: fields.guid @@ -120,7 +120,7 @@ func TestTranslateGUIDWithLDAP(t *testing.T) { filebeat.WaitFileContains( outputFile, fmt.Sprintf(`"fields":{"guid":"%s","common_name":["User1","user01"]}`, entryUUID), - 10*time.Second, + 20*time.Second, ) } diff --git a/go.mod b/go.mod index 13794af08e3..f9c5b141027 100644 --- a/go.mod +++ b/go.mod @@ -82,7 +82,6 @@ require ( github.com/godror/godror v0.33.2 github.com/gofrs/flock v0.8.1 github.com/gogo/protobuf v1.3.2 - github.com/golang/mock v1.6.0 github.com/golang/snappy v0.0.4 github.com/gomodule/redigo v1.8.3 github.com/google/flatbuffers v24.3.25+incompatible @@ -182,7 +181,7 @@ require ( github.com/elastic/bayeux v1.0.5 github.com/elastic/ebpfevents v0.6.0 github.com/elastic/elastic-agent-autodiscover v0.9.0 - github.com/elastic/elastic-agent-libs v0.17.3 + github.com/elastic/elastic-agent-libs v0.17.4 github.com/elastic/elastic-agent-system-metrics v0.11.4 github.com/elastic/go-elasticsearch/v8 v8.14.0 github.com/elastic/go-quark v0.2.0 @@ -223,6 +222,7 @@ require ( go.opentelemetry.io/collector/consumer v0.109.0 go.opentelemetry.io/collector/pdata v1.15.0 go.opentelemetry.io/collector/receiver v0.109.0 + go.uber.org/mock v0.5.0 golang.org/x/term v0.25.0 google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f gopkg.in/natefinch/lumberjack.v2 v2.2.1 diff --git a/go.sum b/go.sum index 1104817faad..900849e696a 100644 --- a/go.sum +++ b/go.sum @@ -335,8 +335,8 @@ github.com/elastic/elastic-agent-autodiscover v0.9.0 h1:+iWIKh0u3e8I+CJa3FfWe9h0 github.com/elastic/elastic-agent-autodiscover v0.9.0/go.mod h1:5iUxLHhVdaGSWYTveSwfJEY4RqPXTG13LPiFoxcpFd4= github.com/elastic/elastic-agent-client/v7 v7.15.0 h1:nDB7v8TBoNuD6IIzC3z7Q0y+7bMgXoT2DsHfolO2CHE= github.com/elastic/elastic-agent-client/v7 v7.15.0/go.mod h1:6h+f9QdIr3GO2ODC0Y8+aEXRwzbA5W4eV4dd/67z7nI= -github.com/elastic/elastic-agent-libs v0.17.3 h1:q79P05dhQkc5REzieVkkD9oRKrnptKY4MC6Typ+d8bc= -github.com/elastic/elastic-agent-libs v0.17.3/go.mod h1:5CR02awPrBr+tfmjBBK+JI+dMmHNQjpVY24J0wjbC7M= +github.com/elastic/elastic-agent-libs v0.17.4 h1:kWK5Kn2EQjM97yHqbeXv+cFAIti4IiI9Qj8huM+lZzE= +github.com/elastic/elastic-agent-libs v0.17.4/go.mod h1:5CR02awPrBr+tfmjBBK+JI+dMmHNQjpVY24J0wjbC7M= github.com/elastic/elastic-agent-system-metrics v0.11.4 h1:Z/8CML5RKvGpi6/QUFok1K3EriBAv2kUAXnsk8hCifk= github.com/elastic/elastic-agent-system-metrics v0.11.4/go.mod h1:TTW2ysv78uHBQ68hG8TXiaX1m6f29ZHgGWb8XONYsU8= github.com/elastic/elastic-transport-go/v8 v8.6.0 h1:Y2S/FBjx1LlCv5m6pWAF2kDJAHoSjSRSJCApolgfthA= @@ -967,6 +967,8 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= @@ -1144,7 +1146,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200821192610-3366bbee4705/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= diff --git a/heartbeat/monitors/factory_test.go b/heartbeat/monitors/factory_test.go index e32ac671c8e..72db7cdf731 100644 --- a/heartbeat/monitors/factory_test.go +++ b/heartbeat/monitors/factory_test.go @@ -236,8 +236,8 @@ func TestDisabledMonitor(t *testing.T) { require.NoError(t, err) require.IsType(t, NoopRunner{}, runner) - require.Equal(t, 0, built.Load()) - require.Equal(t, 0, closed.Load()) + require.Equal(t, int64(0), built.Load()) + require.Equal(t, int64(0), closed.Load()) } } @@ -353,7 +353,7 @@ func TestDuplicateMonitorIDs(t *testing.T) { // Two are counted as built. The bad config is missing a stdfield so it // doesn't complete construction - require.Equal(t, 2, built.Load()) + require.Equal(t, int64(2), built.Load()) // Only 2 closes, because the bad config isn't closed - require.Equal(t, 2, closed.Load()) + require.Equal(t, int64(2), closed.Load()) } diff --git a/heartbeat/monitors/mocks.go b/heartbeat/monitors/mocks.go index c172d24464c..2c4e5e9d0b5 100644 --- a/heartbeat/monitors/mocks.go +++ b/heartbeat/monitors/mocks.go @@ -21,6 +21,7 @@ import ( "fmt" "regexp" "sync" + "sync/atomic" "testing" "time" @@ -42,7 +43,6 @@ import ( "github.com/elastic/beats/v7/heartbeat/monitors/wrappers/monitorstate" "github.com/elastic/beats/v7/heartbeat/scheduler" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" beatversion "github.com/elastic/beats/v7/libbeat/version" ) @@ -60,12 +60,8 @@ func makeMockFactory(pluginsReg *plugin.PluginsReg) (factory *RunnerFactory, sch EphemeralID: eid, FirstStart: time.Now(), StartTime: time.Now(), - Monitoring: struct { - DefaultUsername string - }{ - DefaultUsername: "test", - }, } + info.Monitoring.DefaultUsername = "test" sched = scheduler.Create( 1, @@ -216,17 +212,17 @@ func createMockJob() []jobs.Job { return []jobs.Job{j} } -func mockPluginBuilder() (plugin.PluginFactory, *atomic.Int, *atomic.Int) { +func mockPluginBuilder() (plugin.PluginFactory, *atomic.Int64, *atomic.Int64) { reg := monitoring.NewRegistry() - built := atomic.NewInt(0) - closed := atomic.NewInt(0) + built := &atomic.Int64{} + closed := &atomic.Int64{} return plugin.PluginFactory{ Name: "test", Aliases: []string{"testAlias"}, Make: func(s string, config *config.C) (plugin.Plugin, error) { - built.Inc() + built.Add(1) // Declare a real config block with a required attr so we can see what happens when it doesn't work unpacked := struct { URLs []string `config:"urls" validate:"required"` @@ -234,7 +230,7 @@ func mockPluginBuilder() (plugin.PluginFactory, *atomic.Int, *atomic.Int) { // track all closes, even on error closer := func() error { - closed.Inc() + closed.Add(1) return nil } @@ -246,12 +242,13 @@ func mockPluginBuilder() (plugin.PluginFactory, *atomic.Int, *atomic.Int) { return plugin.Plugin{Jobs: j, DoClose: closer, Endpoints: 1}, nil }, - Stats: plugin.NewPluginCountersRecorder("test", reg)}, + Stats: plugin.NewPluginCountersRecorder("test", reg), + }, built, closed } -func mockPluginsReg() (p *plugin.PluginsReg, built *atomic.Int, closed *atomic.Int) { +func mockPluginsReg() (p *plugin.PluginsReg, built *atomic.Int64, closed *atomic.Int64) { reg := plugin.NewPluginsReg() builder, built, closed := mockPluginBuilder() _ = reg.Add(builder) diff --git a/heartbeat/monitors/monitor_test.go b/heartbeat/monitors/monitor_test.go index 0890a1697be..7bb3b5d3fae 100644 --- a/heartbeat/monitors/monitor_test.go +++ b/heartbeat/monitors/monitor_test.go @@ -106,10 +106,10 @@ func testMonitorConfig(t *testing.T, conf *conf.C, eventValidator validator.Vali t.Fatalf("No publishes detected!") } - assert.Equal(t, 1, built.Load()) + assert.Equal(t, int64(1), built.Load()) mon.Stop() - assert.Equal(t, 1, closed.Load()) + assert.Equal(t, int64(1), closed.Load()) assert.Equal(t, true, pcClient.closed) } @@ -129,8 +129,8 @@ func TestCheckInvalidConfig(t *testing.T) { require.Nil(t, m, "For this test to work we need a nil value for the monitor.") // These counters are both zero since this fails at config parse time - require.Equal(t, 0, built.Load()) - require.Equal(t, 0, closed.Load()) + require.Equal(t, int64(0), built.Load()) + require.Equal(t, int64(0), closed.Load()) require.Error(t, checkMonitorConfig(serverMonConf, reg)) } diff --git a/heartbeat/scheduler/schedjob.go b/heartbeat/scheduler/schedjob.go index 2a8172b9564..50f94a895d0 100644 --- a/heartbeat/scheduler/schedjob.go +++ b/heartbeat/scheduler/schedjob.go @@ -20,11 +20,11 @@ package scheduler import ( "context" "sync" + "sync/atomic" "time" "golang.org/x/sync/semaphore" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/logp" ) @@ -35,7 +35,7 @@ type schedJob struct { wg *sync.WaitGroup entrypoint TaskFunc jobLimitSem *semaphore.Weighted - activeTasks atomic.Int + activeTasks atomic.Int64 } // runRecursiveJob runs the entry point for a job, blocking until all subtasks are completed. @@ -48,7 +48,6 @@ func newSchedJob(ctx context.Context, s *Scheduler, id string, jobType string, t scheduler: s, jobLimitSem: s.jobLimitSem[jobType], entrypoint: task, - activeTasks: atomic.MakeInt(0), wg: &sync.WaitGroup{}, } } @@ -59,7 +58,7 @@ func newSchedJob(ctx context.Context, s *Scheduler, id string, jobType string, t // The wait group passed into this function expects to already have its count incremented by one. func (sj *schedJob) run() (startedAt time.Time) { sj.wg.Add(1) - sj.activeTasks.Inc() + sj.activeTasks.Add(1) if sj.jobLimitSem != nil { err := sj.jobLimitSem.Acquire(sj.ctx, 1) // Defer release only if acquired @@ -82,7 +81,7 @@ func (sj *schedJob) run() (startedAt time.Time) { // The wait group passed into this function expects to already have its count incremented by one. func (sj *schedJob) runTask(task TaskFunc) time.Time { defer sj.wg.Done() - defer sj.activeTasks.Dec() + defer sj.activeTasks.Add(-1) // The accounting for waiting/active tasks is done using atomics. // Absolute accuracy is not critical here so the gap between modifying waitingTasks and activeJobs is acceptable. @@ -113,7 +112,7 @@ func (sj *schedJob) runTask(task TaskFunc) time.Time { sj.scheduler.stats.activeTasks.Dec() sj.wg.Add(len(continuations)) - sj.activeTasks.Add(len(continuations)) + sj.activeTasks.Add(int64(len(continuations))) for _, cont := range continuations { // Run continuations in parallel, note that these each will acquire their own slots // We can discard the started at times for continuations as those are diff --git a/heartbeat/scheduler/schedjob_test.go b/heartbeat/scheduler/schedjob_test.go index 24e6178e162..f989014f467 100644 --- a/heartbeat/scheduler/schedjob_test.go +++ b/heartbeat/scheduler/schedjob_test.go @@ -20,13 +20,13 @@ package scheduler import ( "context" "sync" + "sync/atomic" "testing" "time" "github.com/stretchr/testify/require" "github.com/elastic/beats/v7/heartbeat/config" - batomic "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/monitoring" ) @@ -72,7 +72,7 @@ func TestSchedJobRun(t *testing.T) { wg := &sync.WaitGroup{} wg.Add(1) - executed := batomic.MakeBool(false) + executed := &atomic.Bool{} tf := func(ctx context.Context) []TaskFunc { executed.Store(true) @@ -102,14 +102,14 @@ func TestRecursiveForkingJob(t *testing.T) { s := Create(1000, monitoring.NewRegistry(), tarawaTime(), map[string]*config.JobLimit{ "atype": {Limit: 1}, }, false) - ran := batomic.NewInt(0) + var ran atomic.Int64 var terminalTf TaskFunc = func(ctx context.Context) []TaskFunc { - ran.Inc() + ran.Add(1) return nil } var forkingTf TaskFunc = func(ctx context.Context) []TaskFunc { - ran.Inc() + ran.Add(1) return []TaskFunc{ terminalTf, terminalTf, terminalTf, } @@ -118,6 +118,6 @@ func TestRecursiveForkingJob(t *testing.T) { sj := newSchedJob(context.Background(), s, "myid", "atype", forkingTf) sj.run() - require.Equal(t, 4, ran.Load()) + require.Equal(t, int64(4), ran.Load()) } diff --git a/libbeat/beat/info.go b/libbeat/beat/info.go index 314597abbb5..7c3b5c0d90f 100644 --- a/libbeat/beat/info.go +++ b/libbeat/beat/info.go @@ -22,6 +22,8 @@ import ( "github.com/gofrs/uuid/v5" "go.opentelemetry.io/collector/consumer" + + "github.com/elastic/elastic-agent-libs/monitoring" ) // Info stores a beats instance meta data. @@ -41,9 +43,10 @@ type Info struct { // Monitoring-related fields Monitoring struct { - DefaultUsername string // The default username to be used to connect to Elasticsearch Monitoring + DefaultUsername string // The default username to be used to connect to Elasticsearch Monitoring + Namespace *monitoring.Namespace // a monitor namespace that is unique per beat instance } - LogConsumer consumer.Logs //otel log consumer + LogConsumer consumer.Logs // otel log consumer } diff --git a/libbeat/cmd/instance/beat.go b/libbeat/cmd/instance/beat.go index 6332ebac39b..2d1eb3a20f0 100644 --- a/libbeat/cmd/instance/beat.go +++ b/libbeat/cmd/instance/beat.go @@ -337,6 +337,8 @@ func NewBeatReceiver(settings Settings, receiverConfig map[string]interface{}, c config.OverwriteConfigOpts(configOpts(store)) } + b.Beat.Info.Monitoring.Namespace = monitoring.GetNamespace(b.Info.Beat + "-" + b.Info.ID.String()) + instrumentation, err := instrumentation.New(cfg, b.Info.Beat, b.Info.Version) if err != nil { return nil, fmt.Errorf("error setting up instrumentation: %w", err) @@ -469,11 +471,6 @@ func NewBeatReceiver(settings Settings, receiverConfig map[string]interface{}, c return nil, fmt.Errorf("error creating processors: %w", err) } - reg := monitoring.Default.GetRegistry(b.Info.Name) - if reg == nil { - reg = monitoring.Default.NewRegistry(b.Info.Name) - } - // This should be replaced with static config for otel consumer // but need to figure out if we want the Queue settings from here. outputEnabled := b.Config.Output.IsSet() && b.Config.Output.Config().Enabled() @@ -485,12 +482,14 @@ func NewBeatReceiver(settings Settings, receiverConfig map[string]interface{}, c } } - tel := reg.GetRegistry("state") + uniq_reg := b.Beat.Info.Monitoring.Namespace.GetRegistry() + + tel := uniq_reg.GetRegistry("state") if tel == nil { - tel = reg.NewRegistry("state") + tel = uniq_reg.NewRegistry("state") } monitors := pipeline.Monitors{ - Metrics: reg, + Metrics: uniq_reg, Telemetry: tel, Logger: logp.L().Named("publisher"), Tracer: b.Instrumentation.Tracer(), @@ -510,7 +509,6 @@ func NewBeatReceiver(settings Settings, receiverConfig map[string]interface{}, c b.Publisher = publisher return b, nil - } // InitWithSettings does initialization of things common to all actions (read confs, flags) @@ -831,11 +829,27 @@ func (b *Beat) RegisterHostname(useFQDN bool) { hostname := b.Info.FQDNAwareHostname(useFQDN) // info.hostname - infoRegistry := monitoring.GetNamespace("info").GetRegistry() + var infoRegistry *monitoring.Registry + if b.Info.Monitoring.Namespace != nil { + infoRegistry = b.Info.Monitoring.Namespace.GetRegistry().GetRegistry("info") + if infoRegistry == nil { + infoRegistry = b.Info.Monitoring.Namespace.GetRegistry().NewRegistry("info") + } + } else { + infoRegistry = monitoring.GetNamespace("info").GetRegistry() + } monitoring.NewString(infoRegistry, "hostname").Set(hostname) // state.host - stateRegistry := monitoring.GetNamespace("state").GetRegistry() + var stateRegistry *monitoring.Registry + if b.Info.Monitoring.Namespace != nil { + stateRegistry = b.Info.Monitoring.Namespace.GetRegistry().GetRegistry("state") + if stateRegistry == nil { + stateRegistry = b.Info.Monitoring.Namespace.GetRegistry().NewRegistry("state") + } + } else { + stateRegistry = monitoring.GetNamespace("state").GetRegistry() + } monitoring.NewFunc(stateRegistry, "host", host.ReportInfo(hostname), monitoring.Report) } diff --git a/libbeat/common/acker/acker.go b/libbeat/common/acker/acker.go index c75aec13564..47c019a576f 100644 --- a/libbeat/common/acker/acker.go +++ b/libbeat/common/acker/acker.go @@ -19,9 +19,9 @@ package acker import ( "sync" + "sync/atomic" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" ) // Nil creates an ACKer that does nothing. @@ -98,7 +98,7 @@ type gapInfo struct { } func (a *trackingACKer) AddEvent(_ beat.Event, published bool) { - a.events.Inc() + a.events.Add(1) if published { a.addPublishedEvent() } else { @@ -148,7 +148,7 @@ func (a *trackingACKer) addDropEvent() { a.lst.Unlock() current.Unlock() - a.events.Dec() + a.events.Add(^uint32(0)) return } @@ -202,7 +202,7 @@ func (a *trackingACKer) ACKEvents(n int) { current.Unlock() } - a.events.Sub(uint32(total)) + a.events.Add(^uint32(total - 1)) a.fn(acked, total) } diff --git a/libbeat/common/atomic/atomic.go b/libbeat/common/atomic/atomic.go deleted file mode 100644 index 09e83614184..00000000000 --- a/libbeat/common/atomic/atomic.go +++ /dev/null @@ -1,94 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -// Package atomic provides common primitive types with atomic accessors. -package atomic - -import a "sync/atomic" - -// Bool provides an atomic boolean type. -type Bool struct{ u Uint32 } - -// Int32 provides an atomic int32 type. -type Int32 struct{ value int32 } - -// Int64 provides an atomic int64 type. -type Int64 struct{ value int64 } - -// Uint32 provides an atomic uint32 type. -type Uint32 struct{ value uint32 } - -// Uint64 provides an atomic uint64 type. -type Uint64 struct{ value uint64 } - -func MakeBool(v bool) Bool { return Bool{MakeUint32(encBool(v))} } -func NewBool(v bool) *Bool { return &Bool{MakeUint32(encBool(v))} } -func (b *Bool) Load() bool { return b.u.Load() == 1 } -func (b *Bool) Store(v bool) { b.u.Store(encBool(v)) } -func (b *Bool) Swap(new bool) bool { return b.u.Swap(encBool(new)) == 1 } -func (b *Bool) CAS(old, new bool) bool { return b.u.CAS(encBool(old), encBool(new)) } - -func MakeInt32(v int32) Int32 { return Int32{v} } -func NewInt32(v int32) *Int32 { return &Int32{v} } -func (i *Int32) Load() int32 { return a.LoadInt32(&i.value) } -func (i *Int32) Store(v int32) { a.StoreInt32(&i.value, v) } -func (i *Int32) Swap(new int32) int32 { return a.SwapInt32(&i.value, new) } -func (i *Int32) Add(delta int32) int32 { return a.AddInt32(&i.value, delta) } -func (i *Int32) Sub(delta int32) int32 { return a.AddInt32(&i.value, -delta) } -func (i *Int32) Inc() int32 { return i.Add(1) } -func (i *Int32) Dec() int32 { return i.Add(-1) } -func (i *Int32) CAS(old, new int32) bool { return a.CompareAndSwapInt32(&i.value, old, new) } - -func MakeInt64(v int64) Int64 { return Int64{v} } -func NewInt64(v int64) *Int64 { return &Int64{v} } -func (i *Int64) Load() int64 { return a.LoadInt64(&i.value) } -func (i *Int64) Store(v int64) { a.StoreInt64(&i.value, v) } -func (i *Int64) Swap(new int64) int64 { return a.SwapInt64(&i.value, new) } -func (i *Int64) Add(delta int64) int64 { return a.AddInt64(&i.value, delta) } -func (i *Int64) Sub(delta int64) int64 { return a.AddInt64(&i.value, -delta) } -func (i *Int64) Inc() int64 { return i.Add(1) } -func (i *Int64) Dec() int64 { return i.Add(-1) } -func (i *Int64) CAS(old, new int64) bool { return a.CompareAndSwapInt64(&i.value, old, new) } - -func MakeUint32(v uint32) Uint32 { return Uint32{v} } -func NewUint32(v uint32) *Uint32 { return &Uint32{v} } -func (u *Uint32) Load() uint32 { return a.LoadUint32(&u.value) } -func (u *Uint32) Store(v uint32) { a.StoreUint32(&u.value, v) } -func (u *Uint32) Swap(new uint32) uint32 { return a.SwapUint32(&u.value, new) } -func (u *Uint32) Add(delta uint32) uint32 { return a.AddUint32(&u.value, delta) } -func (u *Uint32) Sub(delta uint32) uint32 { return a.AddUint32(&u.value, ^uint32(delta-1)) } -func (u *Uint32) Inc() uint32 { return u.Add(1) } -func (u *Uint32) Dec() uint32 { return u.Add(^uint32(0)) } -func (u *Uint32) CAS(old, new uint32) bool { return a.CompareAndSwapUint32(&u.value, old, new) } - -func MakeUint64(v uint64) Uint64 { return Uint64{v} } -func NewUint64(v uint64) *Uint64 { return &Uint64{v} } -func (u *Uint64) Load() uint64 { return a.LoadUint64(&u.value) } -func (u *Uint64) Store(v uint64) { a.StoreUint64(&u.value, v) } -func (u *Uint64) Swap(new uint64) uint64 { return a.SwapUint64(&u.value, new) } -func (u *Uint64) Add(delta uint64) uint64 { return a.AddUint64(&u.value, delta) } -func (u *Uint64) Sub(delta uint64) uint64 { return a.AddUint64(&u.value, ^uint64(delta-1)) } -func (u *Uint64) Inc() uint64 { return u.Add(1) } -func (u *Uint64) Dec() uint64 { return u.Add(^uint64(0)) } -func (u *Uint64) CAS(old, new uint64) bool { return a.CompareAndSwapUint64(&u.value, old, new) } - -func encBool(b bool) uint32 { - if b { - return 1 - } - return 0 -} diff --git a/libbeat/common/atomic/atomic32.go b/libbeat/common/atomic/atomic32.go deleted file mode 100644 index d811070a6cd..00000000000 --- a/libbeat/common/atomic/atomic32.go +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//go:build 386 || arm || mips || mipsle - -package atomic - -// atomic Uint/Int for 32bit systems - -// Uint provides an architecture specific atomic uint. -type Uint struct{ a Uint32 } - -// Int provides an architecture specific atomic uint. -type Int struct{ a Int32 } - -func MakeUint(v uint) Uint { return Uint{MakeUint32(uint32(v))} } -func NewUint(v uint) *Uint { return &Uint{MakeUint32(uint32(v))} } -func (u *Uint) Load() uint { return uint(u.a.Load()) } -func (u *Uint) Store(v uint) { u.a.Store(uint32(v)) } -func (u *Uint) Swap(new uint) uint { return uint(u.a.Swap(uint32(new))) } -func (u *Uint) Add(delta uint) uint { return uint(u.a.Add(uint32(delta))) } -func (u *Uint) Sub(delta uint) uint { return uint(u.a.Add(uint32(-delta))) } -func (u *Uint) Inc() uint { return uint(u.a.Inc()) } -func (u *Uint) Dec() uint { return uint(u.a.Dec()) } -func (u *Uint) CAS(old, new uint) bool { return u.a.CAS(uint32(old), uint32(new)) } - -func MakeInt(v int) Int { return Int{MakeInt32(int32(v))} } -func NewInt(v int) *Int { return &Int{MakeInt32(int32(v))} } -func (i *Int) Load() int { return int(i.a.Load()) } -func (i *Int) Store(v int) { i.a.Store(int32(v)) } -func (i *Int) Swap(new int) int { return int(i.a.Swap(int32(new))) } -func (i *Int) Add(delta int) int { return int(i.a.Add(int32(delta))) } -func (i *Int) Sub(delta int) int { return int(i.a.Add(int32(-delta))) } -func (i *Int) Inc() int { return int(i.a.Inc()) } -func (i *Int) Dec() int { return int(i.a.Dec()) } -func (i *Int) CAS(old, new int) bool { return i.a.CAS(int32(old), int32(new)) } diff --git a/libbeat/common/atomic/atomic64.go b/libbeat/common/atomic/atomic64.go deleted file mode 100644 index 3f3648e91ce..00000000000 --- a/libbeat/common/atomic/atomic64.go +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//go:build amd64 || arm64 || ppc64 || ppc64le || mips64 || mips64le || s390x - -package atomic - -// atomic Uint/Int for 64bit systems - -// Uint provides an architecture specific atomic uint. -type Uint struct{ a Uint64 } - -// Int provides an architecture specific atomic uint. -type Int struct{ a Int64 } - -func MakeUint(v uint) Uint { return Uint{MakeUint64(uint64(v))} } -func NewUint(v uint) *Uint { return &Uint{MakeUint64(uint64(v))} } -func (u *Uint) Load() uint { return uint(u.a.Load()) } -func (u *Uint) Store(v uint) { u.a.Store(uint64(v)) } -func (u *Uint) Swap(new uint) uint { return uint(u.a.Swap(uint64(new))) } -func (u *Uint) Add(delta uint) uint { return uint(u.a.Add(uint64(delta))) } -func (u *Uint) Sub(delta uint) uint { return uint(u.a.Add(uint64(-delta))) } -func (u *Uint) Inc() uint { return uint(u.a.Inc()) } -func (u *Uint) Dec() uint { return uint(u.a.Dec()) } -func (u *Uint) CAS(old, new uint) bool { return u.a.CAS(uint64(old), uint64(new)) } - -func MakeInt(v int) Int { return Int{MakeInt64(int64(v))} } -func NewInt(v int) *Int { return &Int{MakeInt64(int64(v))} } -func (i *Int) Load() int { return int(i.a.Load()) } -func (i *Int) Store(v int) { i.a.Store(int64(v)) } -func (i *Int) Swap(new int) int { return int(i.a.Swap(int64(new))) } -func (i *Int) Add(delta int) int { return int(i.a.Add(int64(delta))) } -func (i *Int) Sub(delta int) int { return int(i.a.Add(int64(-delta))) } -func (i *Int) Inc() int { return int(i.a.Inc()) } -func (i *Int) Dec() int { return int(i.a.Dec()) } -func (i *Int) CAS(old, new int) bool { return i.a.CAS(int64(old), int64(new)) } diff --git a/libbeat/common/atomic/atomic_test.go b/libbeat/common/atomic/atomic_test.go deleted file mode 100644 index f4df6fc4d89..00000000000 --- a/libbeat/common/atomic/atomic_test.go +++ /dev/null @@ -1,273 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package atomic - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestAtomicBool(t *testing.T) { - assert := assert.New(t) - - var b Bool - assert.False(b.Load(), "check zero value is false") - - b = MakeBool(true) - assert.True(b.Load(), "check value initializer with 'true' value") - - b.Store(false) - assert.False(b.Load(), "check store to false") - - old := b.Swap(true) - assert.False(old, "check old value of swap operation is 'false'") - assert.True(b.Load(), "check new value after swap is 'true'") - - old = b.Swap(false) - assert.True(old, "check old value of second swap operation is 'true'") - assert.False(b.Load(), "check new value after second swap is 'false'") - - ok := b.CAS(true, true) - assert.False(ok, "check CAS fails with wrong 'old' value") - assert.False(b.Load(), "check failed CAS did not change value 'false'") - - ok = b.CAS(false, true) - assert.True(ok, "check CAS succeeds with correct 'old' value") - assert.True(b.Load(), "check CAS did change value to 'true'") -} - -func TestAtomicInt32(t *testing.T) { - assert := assert.New(t) - check := func(expected, actual int32, msg string) { - assert.Equal(expected, actual, msg) - } - - var v Int32 - check(0, v.Load(), "check zero value") - - v = MakeInt32(23) - check(23, v.Load(), "check value initializer") - - v.Store(42) - check(42, v.Load(), "check store new value") - - new := v.Inc() - check(43, new, "check increment returns new value") - check(43, v.Load(), "check increment did store new value") - - new = v.Dec() - check(42, new, "check decrement returns new value") - check(42, v.Load(), "check decrement did store new value") - - new = v.Add(8) - check(50, new, "check add returns new value") - check(50, v.Load(), "check add did store new value") - - new = v.Sub(8) - check(42, new, "check sub returns new value") - check(42, v.Load(), "check sub did store new value") - - old := v.Swap(101) - check(42, old, "check swap returns old value") - check(101, v.Load(), "check swap stores new value") - - ok := v.CAS(0, 23) - assert.False(ok, "check CAS with wrong old value fails") - check(101, v.Load(), "check failed CAS did not change value") - - ok = v.CAS(101, 23) - assert.True(ok, "check CAS succeeds") - check(23, v.Load(), "check CAS did store new value") -} - -func TestAtomicInt64(t *testing.T) { - assert := assert.New(t) - check := func(expected, actual int64, msg string) { - assert.Equal(expected, actual, msg) - } - - var v Int64 - check(0, v.Load(), "check zero value") - - v = MakeInt64(23) - check(23, v.Load(), "check value initializer") - - v.Store(42) - check(42, v.Load(), "check store new value") - - new := v.Inc() - check(43, new, "check increment returns new value") - check(43, v.Load(), "check increment did store new value") - - new = v.Dec() - check(42, new, "check decrement returns new value") - check(42, v.Load(), "check decrement did store new value") - - new = v.Add(8) - check(50, new, "check add returns new value") - check(50, v.Load(), "check add did store new value") - - new = v.Sub(8) - check(42, new, "check sub returns new value") - check(42, v.Load(), "check sub did store new value") - - old := v.Swap(101) - check(42, old, "check swap returns old value") - check(101, v.Load(), "check swap stores new value") - - ok := v.CAS(0, 23) - assert.False(ok, "check CAS with wrong old value fails") - check(101, v.Load(), "check failed CAS did not change value") - - ok = v.CAS(101, 23) - assert.True(ok, "check CAS succeeds") - check(23, v.Load(), "check CAS did store new value") -} - -func TestAtomicUint32(t *testing.T) { - assert := assert.New(t) - check := func(expected, actual uint32, msg string) { - assert.Equal(expected, actual, msg) - } - - var v Uint32 - check(0, v.Load(), "check zero value") - - v = MakeUint32(23) - check(23, v.Load(), "check value initializer") - - v.Store(42) - check(42, v.Load(), "check store new value") - - new := v.Inc() - check(43, new, "check increment returns new value") - check(43, v.Load(), "check increment did store new value") - - new = v.Dec() - check(42, new, "check decrement returns new value") - check(42, v.Load(), "check decrement did store new value") - - new = v.Add(8) - check(50, new, "check add returns new value") - check(50, v.Load(), "check add did store new value") - - new = v.Sub(8) - check(42, new, "check sub returns new value") - check(42, v.Load(), "check sub did store new value") - - old := v.Swap(101) - check(42, old, "check swap returns old value") - check(101, v.Load(), "check swap stores new value") - - ok := v.CAS(0, 23) - assert.False(ok, "check CAS with wrong old value fails") - check(101, v.Load(), "check failed CAS did not change value") - - ok = v.CAS(101, 23) - assert.True(ok, "check CAS succeeds") - check(23, v.Load(), "check CAS did store new value") -} - -func TestAtomicUint64(t *testing.T) { - assert := assert.New(t) - check := func(expected, actual uint64, msg string) { - assert.Equal(expected, actual, msg) - } - - var v Uint64 - check(0, v.Load(), "check zero value") - - v = MakeUint64(23) - check(23, v.Load(), "check value initializer") - - v.Store(42) - check(42, v.Load(), "check store new value") - - new := v.Inc() - check(43, new, "check increment returns new value") - check(43, v.Load(), "check increment did store new value") - - new = v.Dec() - check(42, new, "check decrement returns new value") - check(42, v.Load(), "check decrement did store new value") - - new = v.Add(8) - check(50, new, "check add returns new value") - check(50, v.Load(), "check add did store new value") - - new = v.Sub(8) - check(42, new, "check sub returns new value") - check(42, v.Load(), "check sub did store new value") - - old := v.Swap(101) - check(42, old, "check swap returns old value") - check(101, v.Load(), "check swap stores new value") - - ok := v.CAS(0, 23) - assert.False(ok, "check CAS with wrong old value fails") - check(101, v.Load(), "check failed CAS did not change value") - - ok = v.CAS(101, 23) - assert.True(ok, "check CAS succeeds") - check(23, v.Load(), "check CAS did store new value") -} - -func TestAtomicUint(t *testing.T) { - assert := assert.New(t) - check := func(expected, actual uint, msg string) { - assert.Equal(expected, actual, msg) - } - - var v Uint - check(0, v.Load(), "check zero value") - - v = MakeUint(23) - check(23, v.Load(), "check value initializer") - - v.Store(42) - check(42, v.Load(), "check store new value") - - new := v.Inc() - check(43, new, "check increment returns new value") - check(43, v.Load(), "check increment did store new value") - - new = v.Dec() - check(42, new, "check decrement returns new value") - check(42, v.Load(), "check decrement did store new value") - - new = v.Add(8) - check(50, new, "check add returns new value") - check(50, v.Load(), "check add did store new value") - - new = v.Sub(8) - check(42, new, "check sub returns new value") - check(42, v.Load(), "check sub did store new value") - - old := v.Swap(101) - check(42, old, "check swap returns old value") - check(101, v.Load(), "check swap stores new value") - - ok := v.CAS(0, 23) - assert.False(ok, "check CAS with wrong old value fails") - check(101, v.Load(), "check failed CAS did not change value") - - ok = v.CAS(101, 23) - assert.True(ok, "check CAS succeeds") - check(23, v.Load(), "check CAS did store new value") -} diff --git a/libbeat/conditions/conditions_test.go b/libbeat/conditions/conditions_test.go index be74f0a7caa..8abbb0694e7 100644 --- a/libbeat/conditions/conditions_test.go +++ b/libbeat/conditions/conditions_test.go @@ -105,6 +105,29 @@ var httpResponseTestEvent = &beat.Event{ }, } +var httpResponseEventIPList = &beat.Event{ + Timestamp: time.Now(), + Fields: mapstr.M{ + "@timestamp": "2024-12-05T09:51:23.642Z", + "ecs": mapstr.M{ + "version": "8.11.0", + }, + "host": mapstr.M{ + "hostname": "testhost", + "os": mapstr.M{ + "type": "linux", + "family": "debian", + "version": "11 (bullseye)", + "platform": "debian", + }, + "ip": []string{ + "10.1.0.55", + "fe80::4001:aff:fe9a:55", + }, + }, + }, +} + func testConfig(t *testing.T, expected bool, event *beat.Event, config *Config) { t.Helper() logp.TestingSetup() diff --git a/libbeat/conditions/network.go b/libbeat/conditions/network.go index 76ab2d08423..25c5fb86e70 100644 --- a/libbeat/conditions/network.go +++ b/libbeat/conditions/network.go @@ -20,6 +20,7 @@ package conditions import ( "fmt" "net" + "slices" "strings" "github.com/elastic/elastic-agent-libs/logp" @@ -94,6 +95,24 @@ func (m multiNetworkMatcher) String() string { return strings.Join(names, " OR ") } +func makeMatcher(network string) (networkMatcher, error) { + m := singleNetworkMatcher{name: network, netContainsFunc: namedNetworks[network]} + if m.netContainsFunc == nil { + subnet, err := parseCIDR(network) + if err != nil { + return nil, err + } + m.netContainsFunc = subnet.Contains + } + return m, nil +} + +func invalidTypeError(field string, value interface{}) error { + return fmt.Errorf("network condition attempted to set "+ + "'%v' -> '%v' and encountered unexpected type '%T', only "+ + "strings or []strings are allowed", field, value, value) +} + // NewNetworkCondition builds a new Network using the given configuration. func NewNetworkCondition(fields map[string]interface{}) (*Network, error) { cond := &Network{ @@ -101,24 +120,6 @@ func NewNetworkCondition(fields map[string]interface{}) (*Network, error) { log: logp.NewLogger(logName), } - makeMatcher := func(network string) (networkMatcher, error) { - m := singleNetworkMatcher{name: network, netContainsFunc: namedNetworks[network]} - if m.netContainsFunc == nil { - subnet, err := parseCIDR(network) - if err != nil { - return nil, err - } - m.netContainsFunc = subnet.Contains - } - return m, nil - } - - invalidTypeError := func(field string, value interface{}) error { - return fmt.Errorf("network condition attempted to set "+ - "'%v' -> '%v' and encountered unexpected type '%T', only "+ - "strings or []strings are allowed", field, value, value) - } - for field, value := range mapstr.M(fields).Flatten() { switch v := value.(type) { case string: @@ -157,15 +158,17 @@ func (c *Network) Check(event ValuesMap) bool { return false } - ip := extractIP(value) - if ip == nil { + ipList := extractIP(value) + if len(ipList) == 0 { c.log.Debugf("Invalid IP address in field=%v for network condition", field) return false } - - if !network.Contains(ip) { + // match on an "any" basis when we find multiple IPs in the event; + // if the network matcher returns true for any seen IP, consider it a match + if !slices.ContainsFunc(ipList, network.Contains) { return false } + } return true @@ -202,12 +205,20 @@ func parseCIDR(value string) (*net.IPNet, error) { // extractIP return an IP address if unk is an IP address string or a net.IP. // Otherwise it returns nil. -func extractIP(unk interface{}) net.IP { +func extractIP(unk interface{}) []net.IP { switch v := unk.(type) { case string: - return net.ParseIP(v) - case net.IP: + return []net.IP{net.ParseIP(v)} + case []net.IP: return v + case net.IP: + return []net.IP{v} + case []string: + parsed := make([]net.IP, len(v)) + for i, rawIP := range v { + parsed[i] = net.ParseIP(rawIP) + } + return parsed default: return nil } diff --git a/libbeat/conditions/network_test.go b/libbeat/conditions/network_test.go index b79568098e4..71ab0918e67 100644 --- a/libbeat/conditions/network_test.go +++ b/libbeat/conditions/network_test.go @@ -79,6 +79,26 @@ network: testYAMLConfig(t, true, evt, yaml) }) + + t.Run("IP list", func(t *testing.T) { + const yaml = ` +network: + ip: + client: [loopback] + server: [loopback] + host: 10.10.0.0/8 +` + + evt := &beat.Event{Fields: mapstr.M{ + "ip": mapstr.M{ + "client": "127.0.0.1", + "server": "127.0.0.1", + "host": []string{"10.10.0.83", "fe80::4001:aff:fe9a:53"}, + }, + }} + + testYAMLConfig(t, true, evt, yaml) + }) } func TestNetworkCreate(t *testing.T) { @@ -166,6 +186,22 @@ func TestNetworkCheck(t *testing.T) { }) }) + t.Run("multiple IPs field single match", func(t *testing.T) { + testConfig(t, true, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": "10.1.0.0/24", + }, + }) + }) + + t.Run("multiple IPs field negative match", func(t *testing.T) { + testConfig(t, false, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": "127.0.0.0/24", + }, + }) + }) + // Multiple conditions are treated as an implicit AND. t.Run("multiple fields negative match", func(t *testing.T) { testConfig(t, false, httpResponseTestEvent, &Config{ @@ -191,6 +227,22 @@ func TestNetworkCheck(t *testing.T) { }, }) }) + + t.Run("multiple values multiple IPs match", func(t *testing.T) { + testConfig(t, true, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": []interface{}{"10.1.0.0/24", "127.0.0.0/24"}, + }, + }) + }) + + t.Run("multiple values multiple IPs no match", func(t *testing.T) { + testConfig(t, false, httpResponseEventIPList, &Config{ + Network: map[string]interface{}{ + "host.ip": []interface{}{"12.1.0.0/24", "127.0.0.0/24"}, + }, + }) + }) } func TestNetworkPrivate(t *testing.T) { diff --git a/libbeat/docs/processors-using.asciidoc b/libbeat/docs/processors-using.asciidoc index dd91ea8d5db..a029f5f2ea8 100644 --- a/libbeat/docs/processors-using.asciidoc +++ b/libbeat/docs/processors-using.asciidoc @@ -311,10 +311,15 @@ range: [[condition-network]] ===== `network` -The `network` condition checks if the field is in a certain IP network range. -Both IPv4 and IPv6 addresses are supported. The network range may be specified -using CIDR notation, like "192.0.2.0/24" or "2001:db8::/32", or by using one of -these named ranges: +The `network` condition checks whether a field's value falls within a specified +IP network range. If multiple fields are provided, each field value must match +its corresponding network range. You can specify multiple network ranges for a +single field, and a match occurs if any one of the ranges matches. If the field +value is an array of IPs, it will match if any of the IPs fall within any of the +given ranges. Both IPv4 and IPv6 addresses are supported. + +The network range may be specified using CIDR notation, like "192.0.2.0/24" or +"2001:db8::/32", or by using one of these named ranges: - `loopback` - Matches loopback addresses in the range of `127.0.0.0/8` or `::1/128`. diff --git a/libbeat/idxmgmt/index_support.go b/libbeat/idxmgmt/index_support.go index 4526d916c35..57816b3625c 100644 --- a/libbeat/idxmgmt/index_support.go +++ b/libbeat/idxmgmt/index_support.go @@ -21,10 +21,10 @@ import ( "errors" "fmt" "strings" + "sync/atomic" "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/beats/v7/libbeat/beat/events" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/idxmgmt/lifecycle" "github.com/elastic/beats/v7/libbeat/outputs" "github.com/elastic/beats/v7/libbeat/outputs/outil" @@ -314,7 +314,7 @@ func (m *indexManager) setupWithILM() (bool, error) { } if withILM { // mark ILM as enabled in indexState - m.support.st.withILM.CAS(false, true) + m.support.st.withILM.CompareAndSwap(false, true) } } return withILM, nil diff --git a/libbeat/management/agent.go b/libbeat/management/agent.go index 84f5a1e9f6b..cd14f8cb969 100644 --- a/libbeat/management/agent.go +++ b/libbeat/management/agent.go @@ -17,17 +17,15 @@ package management -import ( - "github.com/elastic/beats/v7/libbeat/common/atomic" -) +import "sync/atomic" var ( // underAgent is set to true with this beat is being ran under the elastic-agent - underAgent = atomic.MakeBool(false) + underAgent atomic.Bool // underAgentTrace is set to true when the elastic-agent has placed this beat into // trace mode (which enables logging of published events) - underAgentTrace = atomic.MakeBool(false) + underAgentTrace atomic.Bool ) // SetUnderAgent sets that the processing pipeline is being ran under the elastic-agent. diff --git a/libbeat/outputs/elasticsearch/client_proxy_test.go b/libbeat/outputs/elasticsearch/client_proxy_test.go index bd6739c3bf0..b0cef282487 100644 --- a/libbeat/outputs/elasticsearch/client_proxy_test.go +++ b/libbeat/outputs/elasticsearch/client_proxy_test.go @@ -29,12 +29,12 @@ import ( "net/url" "os" "os/exec" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/esleg/eslegclient" "github.com/elastic/beats/v7/libbeat/outputs/outil" "github.com/elastic/elastic-agent-libs/transport/httpcommon" @@ -224,17 +224,17 @@ type serverState struct { serverURL string proxyURL string - _serverRequestCount atomic.Int // Requests directly to the server - _proxyRequestCount atomic.Int // Requests via the proxy + _serverRequestCount atomic.Int64 // Requests directly to the server + _proxyRequestCount atomic.Int64 // Requests via the proxy } // Convenience functions to unwrap the atomic primitives -func (s serverState) serverRequestCount() int { - return s._serverRequestCount.Load() +func (s *serverState) serverRequestCount() int { + return int(s._serverRequestCount.Load()) } -func (s serverState) proxyRequestCount() int { - return s._proxyRequestCount.Load() +func (s *serverState) proxyRequestCount() int { + return int(s._proxyRequestCount.Load()) } // startServers starts endpoints representing a backend server and a proxy, @@ -246,13 +246,13 @@ func startServers(t *testing.T) (*serverState, func()) { http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, headerTestValue, r.Header.Get(headerTestField)) fmt.Fprintln(w, "Hello, client") - state._serverRequestCount.Inc() + state._serverRequestCount.Add(1) })) proxy := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, headerTestValue, r.Header.Get(headerTestField)) fmt.Fprintln(w, "Hello, client") - state._proxyRequestCount.Inc() + state._proxyRequestCount.Add(1) })) state.serverURL = server.URL state.proxyURL = proxy.URL diff --git a/libbeat/outputs/logstash/async.go b/libbeat/outputs/logstash/async.go index a980d1cef32..f0da5e45943 100644 --- a/libbeat/outputs/logstash/async.go +++ b/libbeat/outputs/logstash/async.go @@ -22,10 +22,10 @@ import ( "errors" "net" "sync" + "sync/atomic" "time" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/outputs" "github.com/elastic/beats/v7/libbeat/publisher" "github.com/elastic/elastic-agent-libs/logp" @@ -147,13 +147,13 @@ func (c *asyncClient) Publish(_ context.Context, batch publisher.Batch) error { ref := &msgRef{ client: c, - count: atomic.MakeUint32(1), batch: batch, slice: events, batchSize: len(events), win: c.win, err: nil, } + ref.count.Store(1) defer ref.dec() for len(events) > 0 { @@ -218,7 +218,7 @@ func (c *asyncClient) sendEvents(ref *msgRef, events []publisher.Event) error { for i := range events { window[i] = &events[i].Content } - ref.count.Inc() + ref.count.Add(1) return client.Send(ref.callback, window) } @@ -261,7 +261,7 @@ func (r *msgRef) fail(n uint32, err error) { } func (r *msgRef) dec() { - i := r.count.Dec() + i := r.count.Add(^uint32(0)) if i > 0 { return } diff --git a/libbeat/outputs/otelconsumer/otelconsumer.go b/libbeat/outputs/otelconsumer/otelconsumer.go index ca5da5308e5..7b2f6fe93e5 100644 --- a/libbeat/outputs/otelconsumer/otelconsumer.go +++ b/libbeat/outputs/otelconsumer/otelconsumer.go @@ -20,7 +20,6 @@ package otelconsumer import ( "context" "fmt" - "strings" "time" "github.com/elastic/beats/v7/libbeat/beat" @@ -103,30 +102,6 @@ func (out *otelConsumer) logsPublish(ctx context.Context, batch publisher.Batch) err := out.logsConsumer.ConsumeLogs(ctx, pLogs) if err != nil { - // If the batch is too large, the elasticsearchexporter will - // return a 413 error. - // - // At the moment, the exporter does not support batch splitting - // on error so we do it here. - // - // See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/36163. - if strings.Contains(err.Error(), "Request Entity Too Large") { - // Try and split the batch into smaller batches and retry - if batch.SplitRetry() { - st.BatchSplit() - st.RetryableErrors(len(events)) - } else { - // If the batch could not be split, there is no option left but - // to drop it and log the error state. - batch.Drop() - st.PermanentErrors(len(events)) - out.log.Errorf("the batch is too large to be sent: %v", err) - } - - // Don't propagate the error, the batch was split and retried. - return nil - } - // Permanent errors shouldn't be retried. This tipically means // the data cannot be serialized by the exporter that is attached // to the pipeline or when the destination refuses the data because diff --git a/libbeat/outputs/otelconsumer/otelconsumer_test.go b/libbeat/outputs/otelconsumer/otelconsumer_test.go index 2751ce7f721..6a197d806c1 100644 --- a/libbeat/outputs/otelconsumer/otelconsumer_test.go +++ b/libbeat/outputs/otelconsumer/otelconsumer_test.go @@ -90,33 +90,6 @@ func TestPublish(t *testing.T) { assert.Equal(t, outest.BatchRetry, batch.Signals[0].Tag) }) - t.Run("split batch on entity too large error", func(t *testing.T) { - batch := outest.NewBatch(event1, event2, event3) - - otelConsumer := makeOtelConsumer(t, func(ctx context.Context, ld plog.Logs) error { - return errors.New("Request Entity Too Large") - }) - - err := otelConsumer.Publish(ctx, batch) - assert.NoError(t, err) - assert.Len(t, batch.Signals, 1) - assert.Equal(t, outest.BatchSplitRetry, batch.Signals[0].Tag) - }) - - t.Run("drop batch if can't split on entity too large error", func(t *testing.T) { - batch := outest.NewBatch(event1) - - otelConsumer := makeOtelConsumer(t, func(ctx context.Context, ld plog.Logs) error { - return errors.New("Request Entity Too Large") - }) - - err := otelConsumer.Publish(ctx, batch) - assert.NoError(t, err) - assert.Len(t, batch.Signals, 2) - assert.Equal(t, outest.BatchSplitRetry, batch.Signals[0].Tag) - assert.Equal(t, outest.BatchDrop, batch.Signals[1].Tag) - }) - t.Run("drop batch on permanent consumer error", func(t *testing.T) { batch := outest.NewBatch(event1, event2, event3) diff --git a/libbeat/processors/add_process_metadata/add_process_metadata.go b/libbeat/processors/add_process_metadata/add_process_metadata.go index 2385e5f99de..78e67f8e4d7 100644 --- a/libbeat/processors/add_process_metadata/add_process_metadata.go +++ b/libbeat/processors/add_process_metadata/add_process_metadata.go @@ -22,11 +22,11 @@ import ( "fmt" "reflect" "strconv" + "sync/atomic" "time" "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/beats/v7/libbeat/common" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/processors" jsprocessor "github.com/elastic/beats/v7/libbeat/processors/script/javascript/module/processor" conf "github.com/elastic/elastic-agent-libs/config" @@ -131,7 +131,7 @@ func NewWithConfig(opts ...ConfigOption) (beat.Processor, error) { func newProcessMetadataProcessorWithProvider(config config, provider processMetadataProvider, withCache bool) (proc beat.Processor, err error) { // Logging (each processor instance has a unique ID). var ( - id = int(instanceID.Inc()) + id = int(instanceID.Add(1)) log = logp.NewLogger(processorName).With("instance_id", id) ) diff --git a/libbeat/processors/cache/cache.go b/libbeat/processors/cache/cache.go index a7ce4876f50..45c1e41872e 100644 --- a/libbeat/processors/cache/cache.go +++ b/libbeat/processors/cache/cache.go @@ -22,10 +22,10 @@ import ( "errors" "fmt" "os" + "sync/atomic" "time" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/processors" conf "github.com/elastic/elastic-agent-libs/config" "github.com/elastic/elastic-agent-libs/logp" @@ -67,7 +67,7 @@ func New(cfg *conf.C) (beat.Processor, error) { return nil, fmt.Errorf("failed to unpack the %s configuration: %w", name, err) } // Logging (each processor instance has a unique ID). - id := int(instanceID.Inc()) + id := int(instanceID.Add(1)) log := logp.NewLogger(name).With("instance_id", id) src, cancel, err := getStoreFor(config, log) diff --git a/libbeat/processors/dns/dns.go b/libbeat/processors/dns/dns.go index d4f3d2ba57b..48b08ab671c 100644 --- a/libbeat/processors/dns/dns.go +++ b/libbeat/processors/dns/dns.go @@ -22,9 +22,9 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/processors" jsprocessor "github.com/elastic/beats/v7/libbeat/processors/script/javascript/module/processor" conf "github.com/elastic/elastic-agent-libs/config" @@ -36,7 +36,7 @@ import ( const logName = "processor.dns" // instanceID is used to assign each instance a unique monitoring namespace. -var instanceID = atomic.MakeUint32(0) +var instanceID atomic.Uint32 func init() { processors.RegisterPlugin("dns", New) @@ -58,7 +58,7 @@ func New(cfg *conf.C) (beat.Processor, error) { // Logging and metrics (each processor instance has a unique ID). var ( - id = int(instanceID.Inc()) + id = int(instanceID.Add(1)) log = logp.NewLogger(logName).With("instance_id", id) metrics = monitoring.Default.NewRegistry(logName+"."+strconv.Itoa(id), monitoring.DoNotReport) ) diff --git a/libbeat/processors/ratelimit/rate_limit.go b/libbeat/processors/ratelimit/rate_limit.go index f558b076e2b..92d9b4c37a0 100644 --- a/libbeat/processors/ratelimit/rate_limit.go +++ b/libbeat/processors/ratelimit/rate_limit.go @@ -22,12 +22,12 @@ import ( "fmt" "sort" "strconv" + "sync/atomic" "github.com/jonboulle/clockwork" "github.com/mitchellh/hashstructure" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/processors" c "github.com/elastic/elastic-agent-libs/config" "github.com/elastic/elastic-agent-libs/logp" @@ -36,7 +36,7 @@ import ( ) // instanceID is used to assign each instance a unique monitoring namespace. -var instanceID = atomic.MakeUint32(0) +var instanceID atomic.Uint32 const processorName = "rate_limit" const logName = "processor." + processorName @@ -79,7 +79,7 @@ func new(cfg *c.C) (beat.Processor, error) { // Logging and metrics (each processor instance has a unique ID). var ( - id = int(instanceID.Inc()) + id = int(instanceID.Add(1)) log = logp.NewLogger(logName).With("instance_id", id) reg = monitoring.Default.NewRegistry(logName+"."+strconv.Itoa(id), monitoring.DoNotReport) ) diff --git a/libbeat/processors/ratelimit/token_bucket.go b/libbeat/processors/ratelimit/token_bucket.go index 1f1381fd8df..1e84f799b98 100644 --- a/libbeat/processors/ratelimit/token_bucket.go +++ b/libbeat/processors/ratelimit/token_bucket.go @@ -20,13 +20,13 @@ package ratelimit import ( "fmt" "sync" + "sync/atomic" "time" "github.com/jonboulle/clockwork" "github.com/elastic/go-concert/unison" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/logp" ) @@ -50,7 +50,7 @@ type tokenBucket struct { gc struct { thresholds tokenBucketGCConfig metrics struct { - numCalls atomic.Uint + numCalls atomic.Uint64 } } @@ -93,7 +93,7 @@ func newTokenBucket(config algoConfig) (algorithm, error) { gc: struct { thresholds tokenBucketGCConfig metrics struct { - numCalls atomic.Uint + numCalls atomic.Uint64 } }{ thresholds: tokenBucketGCConfig{ @@ -112,7 +112,7 @@ func (t *tokenBucket) IsAllowed(key uint64) bool { b := t.getBucket(key) allowed := b.withdraw() - t.gc.metrics.numCalls.Inc() + t.gc.metrics.numCalls.Add(1) return allowed } @@ -126,6 +126,7 @@ func (t *tokenBucket) getBucket(key uint64) *bucket { tokens: t.depth, lastReplenish: t.clock.Now(), }) + //nolint:errcheck // ignore b := v.(*bucket) if exists { @@ -154,7 +155,7 @@ func (b *bucket) replenish(rate rate, clock clockwork.Clock) { func (t *tokenBucket) runGC() { // Don't run GC if thresholds haven't been crossed. - if t.gc.metrics.numCalls.Load() < t.gc.thresholds.NumCalls { + if t.gc.metrics.numCalls.Load() < uint64(t.gc.thresholds.NumCalls) { return } @@ -171,7 +172,9 @@ func (t *tokenBucket) runGC() { toDelete := make([]uint64, 0) numBucketsBefore := 0 t.buckets.Range(func(k, v interface{}) bool { + //nolint:errcheck // ignore key := k.(uint64) + //nolint:errcheck // ignore b := v.(*bucket) b.replenish(t.limit, t.clock) @@ -190,9 +193,9 @@ func (t *tokenBucket) runGC() { } // Reset GC metrics - t.gc.metrics.numCalls = atomic.MakeUint(0) + t.gc.metrics.numCalls = atomic.Uint64{} - gcDuration := time.Now().Sub(gcStartTime) + gcDuration := time.Since(gcStartTime) numBucketsDeleted := len(toDelete) numBucketsAfter := numBucketsBefore - numBucketsDeleted t.logger.Debugf("gc duration: %v, buckets: (before: %v, deleted: %v, after: %v)", diff --git a/libbeat/processors/syslog/syslog.go b/libbeat/processors/syslog/syslog.go index 96c21d3d773..9a6e71fcca3 100644 --- a/libbeat/processors/syslog/syslog.go +++ b/libbeat/processors/syslog/syslog.go @@ -22,9 +22,9 @@ import ( "errors" "fmt" "strconv" + "sync/atomic" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/common/cfgtype" "github.com/elastic/beats/v7/libbeat/common/jsontransform" "github.com/elastic/beats/v7/libbeat/processors" @@ -43,7 +43,7 @@ const ( ) // instanceID is used to assign each instance a unique monitoring namespace. -var instanceID = atomic.MakeUint32(0) +var instanceID atomic.Uint32 // config defines the configuration for this processor. type config struct { @@ -114,7 +114,7 @@ func New(c *conf.C) (beat.Processor, error) { return nil, fmt.Errorf("fail to unpack the "+procName+" processor configuration: %w", err) } - id := int(instanceID.Inc()) + id := int(instanceID.Add(1)) log := logp.NewLogger(logName).With("instance_id", id) registryName := logName + "." + strconv.Itoa(id) diff --git a/libbeat/publisher/pipeline/client.go b/libbeat/publisher/pipeline/client.go index af756213a63..3048ec4a001 100644 --- a/libbeat/publisher/pipeline/client.go +++ b/libbeat/publisher/pipeline/client.go @@ -19,10 +19,10 @@ package pipeline import ( "sync" + "sync/atomic" "time" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/processors" "github.com/elastic/beats/v7/libbeat/publisher" "github.com/elastic/beats/v7/libbeat/publisher/queue" @@ -41,8 +41,7 @@ type client struct { canDrop bool // Open state, signaling, and sync primitives for coordinating client Close. - isOpen atomic.Bool // set to false during shutdown, such that no new events will be accepted anymore. - closeOnce sync.Once // closeOnce ensure that the client shutdown sequence is only executed once + isOpen atomic.Bool // set to false during shutdown, such that no new events will be accepted anymore. observer observer eventListener beat.EventListener @@ -204,12 +203,12 @@ func newClientCloseWaiter(timeout time.Duration) *clientCloseWaiter { func (w *clientCloseWaiter) AddEvent(_ beat.Event, published bool) { if published { - w.events.Inc() + w.events.Add(1) } } func (w *clientCloseWaiter) ACKEvents(n int) { - value := w.events.Sub(uint32(n)) + value := w.events.Add(^uint32(n - 1)) if value != 0 { return } diff --git a/libbeat/publisher/pipeline/client_worker_test.go b/libbeat/publisher/pipeline/client_worker_test.go index 97692b2aada..ed97cd11ac8 100644 --- a/libbeat/publisher/pipeline/client_worker_test.go +++ b/libbeat/publisher/pipeline/client_worker_test.go @@ -22,6 +22,7 @@ import ( "math" "strings" "sync" + "sync/atomic" "testing" "testing/quick" "time" @@ -30,7 +31,6 @@ import ( "github.com/stretchr/testify/require" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/internal/testutil" "github.com/elastic/beats/v7/libbeat/outputs" "github.com/elastic/beats/v7/libbeat/publisher" @@ -48,7 +48,7 @@ func TestMakeClientWorker(t *testing.T) { err := quick.Check(func(i uint) bool { numBatches := 300 + (i % 100) // between 300 and 399 - var numEvents uint + var numEvents uint64 logger := makeBufLogger(t) @@ -56,9 +56,9 @@ func TestMakeClientWorker(t *testing.T) { retryer := newStandaloneRetryer(workQueue) defer retryer.close() - var published atomic.Uint + var published atomic.Uint64 publishFn := func(batch publisher.Batch) error { - published.Add(uint(len(batch.Events()))) + published.Add(uint64(len(batch.Events()))) return nil } @@ -69,7 +69,7 @@ func TestMakeClientWorker(t *testing.T) { for i := uint(0); i < numBatches; i++ { batch := randomBatch(50, 150).withRetryer(retryer) - numEvents += uint(len(batch.Events())) + numEvents += uint64(len(batch.Events())) workQueue <- batch } @@ -82,7 +82,7 @@ func TestMakeClientWorker(t *testing.T) { }) if !success { logger.Flush() - t.Logf("numBatches = %v, numEvents = %v, published = %v", numBatches, numEvents, published) + t.Logf("numBatches = %v, numEvents = %v, published = %v", numBatches, numEvents, published.Load()) } return success }, nil) @@ -140,9 +140,9 @@ func TestReplaceClientWorker(t *testing.T) { }() // Publish at least 1 batch worth of events but no more than 20% events - publishLimit := uint(math.Max(minEventsInBatch, float64(numEvents)*0.2)) + publishLimit := uint64(math.Max(minEventsInBatch, float64(numEvents)*0.2)) - var publishedFirst atomic.Uint + var publishedFirst atomic.Uint64 blockCtrl := make(chan struct{}) blockingPublishFn := func(batch publisher.Batch) error { // Emulate blocking. Upon unblocking the in-flight batch that was @@ -152,7 +152,7 @@ func TestReplaceClientWorker(t *testing.T) { } count := len(batch.Events()) - publishedFirst.Add(uint(count)) + publishedFirst.Add(uint64(count)) t.Logf("#1 processed batch: %v (%v)", batch.(*mockBatch).events[0].Content.Private, count) return nil } @@ -176,10 +176,10 @@ func TestReplaceClientWorker(t *testing.T) { close(blockCtrl) // Start new worker to drain work queue - var publishedLater atomic.Uint + var publishedLater atomic.Uint64 countingPublishFn := func(batch publisher.Batch) error { count := len(batch.Events()) - publishedLater.Add(uint(count)) + publishedLater.Add(uint64(count)) t.Logf("#2 processed batch: %v (%v)", batch.(*mockBatch).events[0].Content.Private, count) return nil } @@ -212,7 +212,7 @@ func TestMakeClientTracer(t *testing.T) { testutil.SeedPRNG(t) numBatches := 10 - var numEvents uint + var numEvents uint64 logger := makeBufLogger(t) @@ -220,9 +220,9 @@ func TestMakeClientTracer(t *testing.T) { retryer := newStandaloneRetryer(workQueue) defer retryer.close() - var published atomic.Uint + var published atomic.Uint64 publishFn := func(batch publisher.Batch) error { - published.Add(uint(len(batch.Events()))) + published.Add(uint64(len(batch.Events()))) return nil } @@ -236,7 +236,7 @@ func TestMakeClientTracer(t *testing.T) { for i := 0; i < numBatches; i++ { batch := randomBatch(10, 15).withRetryer(retryer) - numEvents += uint(len(batch.Events())) + numEvents += uint64(len(batch.Events())) workQueue <- batch } @@ -248,7 +248,7 @@ func TestMakeClientTracer(t *testing.T) { return numEvents == published.Load() }) if !matches { - t.Errorf("expected %d events, got %d", numEvents, published) + t.Errorf("expected %d events, got %d", numEvents, published.Load()) } recorder.Flush(nil) diff --git a/libbeat/publisher/pipeline/controller_test.go b/libbeat/publisher/pipeline/controller_test.go index 706c159e3d4..5e48fbb79b8 100644 --- a/libbeat/publisher/pipeline/controller_test.go +++ b/libbeat/publisher/pipeline/controller_test.go @@ -20,12 +20,12 @@ package pipeline import ( "fmt" "sync" + "sync/atomic" "testing" "testing/quick" "time" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/internal/testutil" "github.com/elastic/beats/v7/libbeat/outputs" "github.com/elastic/beats/v7/libbeat/publisher" @@ -64,9 +64,9 @@ func TestOutputReload(t *testing.T) { fmt.Sprintf("mem.events: %v", numEventsToPublish)) _ = queueConfig.Unpack(conf) - var publishedCount atomic.Uint + var publishedCount atomic.Uint64 countingPublishFn := func(batch publisher.Batch) error { - publishedCount.Add(uint(len(batch.Events()))) + publishedCount.Add(uint64(len(batch.Events()))) return nil } @@ -108,7 +108,7 @@ func TestOutputReload(t *testing.T) { timeout := 20 * time.Second return waitUntilTrue(timeout, func() bool { - return numEventsToPublish == publishedCount.Load() + return uint64(numEventsToPublish) == publishedCount.Load() }) }, &quick.Config{MaxCount: 25}) @@ -222,18 +222,19 @@ func TestQueueProducerBlocksUntilOutputIsSet(t *testing.T) { // block, because there is no queue, but they should become unblocked // once we set a nonempty output. const producerCount = 10 - remaining := atomic.MakeInt(producerCount) + var remaining atomic.Int64 + remaining.Store(producerCount) for i := 0; i < producerCount; i++ { go func() { controller.queueProducer(queue.ProducerConfig{}) - remaining.Dec() + remaining.Add(-1) }() } allStarted := waitUntilTrue(time.Second, func() bool { return len(controller.pendingRequests) == producerCount }) assert.True(t, allStarted, "All queueProducer requests should be saved as pending requests by outputController") - assert.Equal(t, producerCount, remaining.Load(), "No queueProducer request should return before an output is set") + assert.Equal(t, int64(producerCount), remaining.Load(), "No queueProducer request should return before an output is set") // Set the output, then ensure that it unblocks all the waiting goroutines. controller.Set(outputs.Group{ diff --git a/libbeat/publisher/pipeline/pipeline.go b/libbeat/publisher/pipeline/pipeline.go index a5a13a0584e..6297f7b7ee6 100644 --- a/libbeat/publisher/pipeline/pipeline.go +++ b/libbeat/publisher/pipeline/pipeline.go @@ -26,7 +26,6 @@ import ( "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/beats/v7/libbeat/common/acker" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/common/reload" "github.com/elastic/beats/v7/libbeat/outputs" "github.com/elastic/beats/v7/libbeat/publisher" @@ -211,13 +210,13 @@ func (p *Pipeline) ConnectWith(cfg beat.ClientConfig) (beat.Client, error) { client := &client{ logger: p.monitors.Logger, - isOpen: atomic.MakeBool(true), clientListener: cfg.ClientListener, processors: processors, eventFlags: eventFlags, canDrop: canDrop, observer: p.observer, } + client.isOpen.Store(true) ackHandler := cfg.EventListener diff --git a/libbeat/publisher/pipeline/pipeline_test.go b/libbeat/publisher/pipeline/pipeline_test.go index a8cf34b895a..bd374c3c87b 100644 --- a/libbeat/publisher/pipeline/pipeline_test.go +++ b/libbeat/publisher/pipeline/pipeline_test.go @@ -20,10 +20,10 @@ package pipeline import ( "runtime" "sync" + "sync/atomic" "testing" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/publisher/queue" "github.com/elastic/beats/v7/libbeat/tests/resources" "github.com/elastic/elastic-agent-libs/mapstr" @@ -79,7 +79,7 @@ func TestPipelineAcceptsAnyNumberOfClients(t *testing.T) { // close method is called, this ID is returned func makeDiscardQueue() queue.Queue { var wg sync.WaitGroup - producerID := atomic.NewInt(0) + var producerID atomic.Int64 return &testQueue{ close: func() error { @@ -92,7 +92,7 @@ func makeDiscardQueue() queue.Queue { }, producer: func(cfg queue.ProducerConfig) queue.Producer { - producerID.Inc() + producerID.Add(1) // count is a counter that increments on every published event // it's also the returned Event ID @@ -231,11 +231,11 @@ func makeTestQueue() queue.Queue { func blockingProducer(_ queue.ProducerConfig) queue.Producer { sig := make(chan struct{}) - waiting := atomic.MakeInt(0) + var waiting atomic.Int64 return &testProducer{ publish: func(_ bool, _ queue.Entry) (queue.EntryID, bool) { - waiting.Inc() + waiting.Add(1) <-sig return 0, false }, diff --git a/libbeat/publisher/pipeline/stress/gen.go b/libbeat/publisher/pipeline/stress/gen.go index 2a4d8c72ef0..7fdf17bd27b 100644 --- a/libbeat/publisher/pipeline/stress/gen.go +++ b/libbeat/publisher/pipeline/stress/gen.go @@ -22,6 +22,7 @@ import ( "fmt" "runtime/pprof" "sync" + "sync/atomic" "time" "github.com/elastic/elastic-agent-libs/logp" @@ -29,7 +30,6 @@ import ( "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/beats/v7/libbeat/common/acker" - "github.com/elastic/beats/v7/libbeat/common/atomic" ) type generateConfig struct { @@ -97,7 +97,7 @@ func generate( done := make(chan struct{}) defer close(done) - count := atomic.MakeUint64(0) + var count atomic.Uint64 var wg sync.WaitGroup defer wg.Wait() @@ -148,7 +148,7 @@ func generate( Fields: mapstr.M{ "id": id, "hello": "world", - "count": count, + "count": count.Load(), // TODO: more custom event generation? }, @@ -156,7 +156,7 @@ func generate( client.Publish(event) - total := count.Inc() + total := count.Add(1) if config.MaxEvents > 0 && total == config.MaxEvents { break } diff --git a/libbeat/publisher/pipeline/stress/sig.go b/libbeat/publisher/pipeline/stress/sig.go index 537fc9c633b..d6d03f0c3d1 100644 --- a/libbeat/publisher/pipeline/stress/sig.go +++ b/libbeat/publisher/pipeline/stress/sig.go @@ -17,7 +17,7 @@ package stress -import "github.com/elastic/beats/v7/libbeat/common/atomic" +import "sync/atomic" type closeSignaler struct { active atomic.Bool @@ -25,14 +25,15 @@ type closeSignaler struct { } func newCloseSignaler() *closeSignaler { - return &closeSignaler{ - active: atomic.MakeBool(true), - done: make(chan struct{}), + cs := &closeSignaler{ + done: make(chan struct{}), } + cs.active.Store(true) + return cs } func (s *closeSignaler) Close() { - if act := s.active.Swap(false); act { + if s.active.Swap(false) { close(s.done) } } diff --git a/libbeat/publisher/testing/connector.go b/libbeat/publisher/testing/connector.go index ddf48cc126f..967f86ebe28 100644 --- a/libbeat/publisher/testing/connector.go +++ b/libbeat/publisher/testing/connector.go @@ -18,15 +18,16 @@ package testing import ( + "sync/atomic" + "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" ) // ClientCounter can be used to create a beat.PipelineConnector that count // pipeline connects and disconnects. type ClientCounter struct { - total atomic.Int - active atomic.Int + total atomic.Int64 + active atomic.Int64 } // FakeConnector implements the beat.PipelineConnector interface. @@ -111,21 +112,21 @@ func ChClient(ch chan beat.Event) beat.Client { } // Active returns the number of currently active connections. -func (c *ClientCounter) Active() int { return c.active.Load() } +func (c *ClientCounter) Active() int { return int(c.active.Load()) } // Total returns the total number of calls to Connect. -func (c *ClientCounter) Total() int { return c.total.Load() } +func (c *ClientCounter) Total() int { return int(c.total.Load()) } // BuildConnector create a pipeline that updates the active and tocal // connection counters on Connect and Close calls. func (c *ClientCounter) BuildConnector() beat.PipelineConnector { return FakeConnector{ ConnectFunc: func(_ beat.ClientConfig) (beat.Client, error) { - c.total.Inc() - c.active.Inc() + c.total.Add(1) + c.active.Add(1) return &FakeClient{ CloseFunc: func() error { - c.active.Dec() + c.active.Add(-1) return nil }, }, nil diff --git a/libbeat/tests/integration/framework.go b/libbeat/tests/integration/framework.go index 904fc1e302a..aa05d6ff0e6 100644 --- a/libbeat/tests/integration/framework.go +++ b/libbeat/tests/integration/framework.go @@ -37,13 +37,12 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "testing" "time" "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/require" - - "github.com/elastic/beats/v7/libbeat/common/atomic" ) type BeatProc struct { @@ -189,7 +188,7 @@ func (b *BeatProc) Start(args ...string) { b.fullPath = fullPath b.Args = append(b.baseArgs, args...) - done := atomic.MakeBool(false) + var done atomic.Bool wg := sync.WaitGroup{} if b.RestartOnBeatOnExit { wg.Add(1) diff --git a/metricbeat/autodiscover/appender/kubernetes/token/token_test.go b/metricbeat/autodiscover/appender/kubernetes/token/token_test.go index aacf10b6bd0..1fd50066799 100644 --- a/metricbeat/autodiscover/appender/kubernetes/token/token_test.go +++ b/metricbeat/autodiscover/appender/kubernetes/token/token_test.go @@ -31,6 +31,8 @@ import ( func TestMain(m *testing.M) { InitializeModule() + + os.Exit(m.Run()) } func TestTokenAppender(t *testing.T) { diff --git a/metricbeat/autodiscover/builder/hints/metrics_test.go b/metricbeat/autodiscover/builder/hints/metrics_test.go index fb0980450bd..4cede2536eb 100644 --- a/metricbeat/autodiscover/builder/hints/metrics_test.go +++ b/metricbeat/autodiscover/builder/hints/metrics_test.go @@ -34,6 +34,8 @@ import ( func TestMain(m *testing.M) { InitializeModule() + + os.Exit(m.Run()) } func TestGenerateHints(t *testing.T) { diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index 09fd5e53245..5f9ee24aa8c 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -23,6 +23,7 @@ grouped in the following categories: * <> * <> * <> +* <> * <> * <> * <> @@ -10472,6 +10473,36 @@ type: long -- +[[exported-fields-benchmark]] +== Benchmark fields + +benchmark module + + + +[float] +=== benchmark + + + + +[float] +=== info + +info + + + +*`benchmark.info.counter`*:: ++ +-- +The nth info metric emitted by the benchmark module + + +type: keyword + +-- + [[exported-fields-ceph]] == Ceph fields diff --git a/metricbeat/docs/modules/benchmark.asciidoc b/metricbeat/docs/modules/benchmark.asciidoc new file mode 100644 index 00000000000..01c950c84d4 --- /dev/null +++ b/metricbeat/docs/modules/benchmark.asciidoc @@ -0,0 +1,76 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// + +:modulename: benchmark +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/benchmark/_meta/docs.asciidoc + + +[[metricbeat-module-benchmark]] +[role="xpack"] +== Benchmark module + +beta[] + +include::{libbeat-dir}/shared/integration-link.asciidoc[] + +:modulename!: + +The `benchmark` module is used to generate synthetic metrics at a predictable rate. This can be useful when you want to test output settings or test system sizing without using real data. + +The `benchmark` module metricset is `info`. + +[source,yaml] +---- +- module: benchmark + metricsets: + - info + enabled: true + period: 10s +---- + +[float] +== Metricsets + +[float] +=== `info` +A metric that includes a `counter` field which is used to keep the metric unique. + +[float] +=== Module-specific configuration notes + +`count`:: number, the number of metrics to emit per fetch. + + + + + +:edit_url: + +[float] +=== Example configuration + +The Benchmark module supports the standard configuration options that are described +in <>. Here is an example configuration: + +[source,yaml] +---- +metricbeat.modules: +- module: benchmark + metricsets: + - info + enabled: false + period: 10s + +---- + +[float] +=== Metricsets + +The following metricsets are available: + +* <> + +include::benchmark/info.asciidoc[] + +:edit_url!: diff --git a/metricbeat/docs/modules/benchmark/info.asciidoc b/metricbeat/docs/modules/benchmark/info.asciidoc new file mode 100644 index 00000000000..4f6cee9874f --- /dev/null +++ b/metricbeat/docs/modules/benchmark/info.asciidoc @@ -0,0 +1,29 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/benchmark/info/_meta/docs.asciidoc + + +[[metricbeat-metricset-benchmark-info]] +[role="xpack"] +=== Benchmark info metricset + +beta[] + +include::../../../../x-pack/metricbeat/module/benchmark/info/_meta/docs.asciidoc[] + + +:edit_url: + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../../x-pack/metricbeat/module/benchmark/info/_meta/data.json[] +---- +:edit_url!: \ No newline at end of file diff --git a/metricbeat/docs/modules/docker.asciidoc b/metricbeat/docs/modules/docker.asciidoc index cd5e2a207a8..a8ae9cbf488 100644 --- a/metricbeat/docs/modules/docker.asciidoc +++ b/metricbeat/docs/modules/docker.asciidoc @@ -22,6 +22,9 @@ The Docker module is currently tested on Linux and Mac with the community edition engine, versions 1.11 and 17.09.0-ce. It is not tested on Windows, but it should also work there. +The Docker module supports collection of metrics from Podman's Docker-compatible API. +It has been tested on Linux and Mac with Podman Rest API v2.0.0 and above. + [float] === Module-specific configuration notes @@ -30,6 +33,9 @@ It is strongly recommended that you run Docker metricsets with a Docker API already takes up to 2 seconds. Specifying less than 3 seconds will result in requests that timeout, and no data will be reported for those requests. +In the case of Podman, the configuration parameter `podman` should be set to `true`. +This enables streaming of container stats output, which allows for more accurate +CPU percentage calculations when using Podman. :edit_url: @@ -62,6 +68,9 @@ metricbeat.modules: # If set to true, replace dots in labels with `_`. #labels.dedot: false + # Docker module supports metrics collection from podman's docker compatible API. In case of podman set to true. + # podman: false + # Skip metrics for certain device major numbers in docker/diskio. # Necessary on systems with software RAID, device mappers, # or other configurations where virtual disks will sum metrics from other disks. diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index f68dc8e1e65..abf04650f2c 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -51,6 +51,8 @@ This file is generated! See scripts/mage/docs_collector.go |<> |image:./images/icon-no.png[No prebuilt dashboards] | .2+| .2+| |<> |<> +|<> beta[] |image:./images/icon-no.png[No prebuilt dashboards] | +.1+| .1+| |<> beta[] |<> |image:./images/icon-yes.png[Prebuilt dashboards are available] | .13+| .13+| |<> |<> @@ -344,6 +346,7 @@ include::modules/aws.asciidoc[] include::modules/awsfargate.asciidoc[] include::modules/azure.asciidoc[] include::modules/beat.asciidoc[] +include::modules/benchmark.asciidoc[] include::modules/ceph.asciidoc[] include::modules/cloudfoundry.asciidoc[] include::modules/cockroachdb.asciidoc[] diff --git a/metricbeat/mb/module/runner_group_test.go b/metricbeat/mb/module/runner_group_test.go index 1d462359968..2cd6e6cc8b5 100644 --- a/metricbeat/mb/module/runner_group_test.go +++ b/metricbeat/mb/module/runner_group_test.go @@ -19,13 +19,13 @@ package module import ( "fmt" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/beats/v7/libbeat/cfgfile" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/common/diagnostics" ) @@ -55,19 +55,19 @@ func (fr *fakeRunnerDiag) Diagnostics() []diagnostics.DiagnosticSetup { type fakeRunner struct { id int - startCounter *atomic.Int - stopCounter *atomic.Int + startCounter *atomic.Int64 + stopCounter *atomic.Int64 } func (fr *fakeRunner) Start() { if fr.startCounter != nil { - fr.startCounter.Inc() + fr.startCounter.Add(1) } } func (fr *fakeRunner) Stop() { if fr.stopCounter != nil { - fr.stopCounter.Inc() + fr.stopCounter.Add(1) } } @@ -76,15 +76,14 @@ func (fr *fakeRunner) String() string { } func TestStartStop(t *testing.T) { - startCounter := atomic.NewInt(0) - stopCounter := atomic.NewInt(0) + var startCounter, stopCounter atomic.Int64 runners := make([]cfgfile.Runner, 0, fakeRunnersNum) for i := 0; i < fakeRunnersNum; i++ { runners = append(runners, &fakeRunner{ id: i, - startCounter: startCounter, - stopCounter: stopCounter, + startCounter: &startCounter, + stopCounter: &stopCounter, }) } @@ -93,8 +92,8 @@ func TestStartStop(t *testing.T) { runnerGroup.Stop() - assert.Equal(t, fakeRunnersNum, startCounter.Load()) - assert.Equal(t, fakeRunnersNum, stopCounter.Load()) + assert.Equal(t, int64(fakeRunnersNum), startCounter.Load()) + assert.Equal(t, int64(fakeRunnersNum), stopCounter.Load()) } func TestDiagnosticsUnsupported(t *testing.T) { @@ -102,8 +101,8 @@ func TestDiagnosticsUnsupported(t *testing.T) { for i := 0; i < fakeRunnersNum; i++ { runners = append(runners, &fakeRunner{ id: i, - startCounter: atomic.NewInt(0), - stopCounter: atomic.NewInt(0), + startCounter: &atomic.Int64{}, + stopCounter: &atomic.Int64{}, }) } diff --git a/metricbeat/metricbeat.reference.yml b/metricbeat/metricbeat.reference.yml index 0a05e0c6008..f1412be4b6f 100644 --- a/metricbeat/metricbeat.reference.yml +++ b/metricbeat/metricbeat.reference.yml @@ -268,6 +268,9 @@ metricbeat.modules: # If set to true, replace dots in labels with `_`. #labels.dedot: false + # Docker module supports metrics collection from podman's docker compatible API. In case of podman set to true. + # podman: false + # Skip metrics for certain device major numbers in docker/diskio. # Necessary on systems with software RAID, device mappers, # or other configurations where virtual disks will sum metrics from other disks. diff --git a/metricbeat/module/docker/_meta/config.reference.yml b/metricbeat/module/docker/_meta/config.reference.yml index 8d11201983c..184d6592bf1 100644 --- a/metricbeat/module/docker/_meta/config.reference.yml +++ b/metricbeat/module/docker/_meta/config.reference.yml @@ -17,6 +17,9 @@ # If set to true, replace dots in labels with `_`. #labels.dedot: false + # Docker module supports metrics collection from podman's docker compatible API. In case of podman set to true. + # podman: false + # Skip metrics for certain device major numbers in docker/diskio. # Necessary on systems with software RAID, device mappers, # or other configurations where virtual disks will sum metrics from other disks. diff --git a/metricbeat/module/docker/_meta/config.yml b/metricbeat/module/docker/_meta/config.yml index da3c1e02a06..a7b9b9196fc 100644 --- a/metricbeat/module/docker/_meta/config.yml +++ b/metricbeat/module/docker/_meta/config.yml @@ -15,6 +15,9 @@ # If set to true, replace dots in labels with `_`. #labels.dedot: false + # Docker module supports metrics collection from podman's Docker-compatible API. In case of podman set to true. + # podman: false + # Skip metrics for certain device major numbers in docker/diskio. # Necessary on systems with software RAID, device mappers, # or other configurations where virtual disks will sum metrics from other disks. diff --git a/metricbeat/module/docker/_meta/docs.asciidoc b/metricbeat/module/docker/_meta/docs.asciidoc index e1d5437572a..ca2da0ea26d 100644 --- a/metricbeat/module/docker/_meta/docs.asciidoc +++ b/metricbeat/module/docker/_meta/docs.asciidoc @@ -11,6 +11,9 @@ The Docker module is currently tested on Linux and Mac with the community edition engine, versions 1.11 and 17.09.0-ce. It is not tested on Windows, but it should also work there. +The Docker module supports collection of metrics from Podman's Docker-compatible API. +It has been tested on Linux and Mac with Podman Rest API v2.0.0 and above. + [float] === Module-specific configuration notes @@ -19,3 +22,6 @@ It is strongly recommended that you run Docker metricsets with a Docker API already takes up to 2 seconds. Specifying less than 3 seconds will result in requests that timeout, and no data will be reported for those requests. +In the case of Podman, the configuration parameter `podman` should be set to `true`. +This enables streaming of container stats output, which allows for more accurate +CPU percentage calculations when using Podman. diff --git a/metricbeat/module/docker/config.go b/metricbeat/module/docker/config.go index 40698cb0baf..b9bee9b35e9 100644 --- a/metricbeat/module/docker/config.go +++ b/metricbeat/module/docker/config.go @@ -19,14 +19,16 @@ package docker // Config contains the config needed for the docker type Config struct { - TLS *TLSConfig `config:"ssl"` - DeDot bool `config:"labels.dedot"` + TLS *TLSConfig `config:"ssl"` + DeDot bool `config:"labels.dedot"` + Podman bool `config:"podman"` } // DefaultConfig returns default module config func DefaultConfig() Config { return Config{ - DeDot: true, + DeDot: true, + Podman: false, } } diff --git a/metricbeat/module/docker/cpu/cpu.go b/metricbeat/module/docker/cpu/cpu.go index a29ee8a00cc..6869dd30a46 100644 --- a/metricbeat/module/docker/cpu/cpu.go +++ b/metricbeat/module/docker/cpu/cpu.go @@ -40,6 +40,7 @@ type MetricSet struct { cpuService *CPUService dockerClient *client.Client dedot bool + podman bool } // New creates a new instance of the docker cpu MetricSet. @@ -68,12 +69,13 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { dockerClient: client, cpuService: &CPUService{Cores: cpuConfig.Cores}, dedot: config.DeDot, + podman: config.Podman, }, nil } // Fetch returns a list of docker CPU stats. func (m *MetricSet) Fetch(r mb.ReporterV2) error { - stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout) + stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout, m.podman, m.Logger()) if err != nil { return fmt.Errorf("failed to get docker stats: %w", err) } diff --git a/metricbeat/module/docker/diskio/diskio.go b/metricbeat/module/docker/diskio/diskio.go index df5a0f2dff5..a59de2d5268 100644 --- a/metricbeat/module/docker/diskio/diskio.go +++ b/metricbeat/module/docker/diskio/diskio.go @@ -89,7 +89,7 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { // Fetch creates list of events with diskio stats for all containers. func (m *MetricSet) Fetch(r mb.ReporterV2) error { - stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout) + stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout, false, m.Logger()) if err != nil { return fmt.Errorf("failed to get docker stats: %w", err) } diff --git a/metricbeat/module/docker/docker.go b/metricbeat/module/docker/docker.go index 2020df91975..d4595ef8e5e 100644 --- a/metricbeat/module/docker/docker.go +++ b/metricbeat/module/docker/docker.go @@ -34,6 +34,7 @@ import ( "github.com/elastic/beats/v7/metricbeat/mb" "github.com/elastic/beats/v7/metricbeat/mb/parse" "github.com/elastic/elastic-agent-autodiscover/docker" + "github.com/elastic/elastic-agent-libs/logp" ) // HostParser is a TCP host parser function for docker tcp host addresses @@ -91,7 +92,7 @@ func NewDockerClient(endpoint string, config Config) (*client.Client, error) { } // FetchStats returns a list of running containers with all related stats inside -func FetchStats(client *client.Client, timeout time.Duration) ([]Stat, error) { +func FetchStats(client *client.Client, timeout time.Duration, stream bool, logger *logp.Logger) ([]Stat, error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() containers, err := client.ContainerList(ctx, container.ListOptions{}) @@ -108,7 +109,7 @@ func FetchStats(client *client.Client, timeout time.Duration) ([]Stat, error) { for _, container := range containers { go func(container types.Container) { defer wg.Done() - statsQueue <- exportContainerStats(ctx, client, &container) + statsQueue <- exportContainerStats(ctx, client, &container, stream, logger) }(container) } @@ -133,18 +134,41 @@ func FetchStats(client *client.Client, timeout time.Duration) ([]Stat, error) { // This is currently very inefficient as docker calculates the average for each request, // means each request will take at least 2s: https://github.com/docker/docker/blob/master/cli/command/container/stats_helpers.go#L148 // Getting all stats at once is implemented here: https://github.com/docker/docker/pull/25361 -func exportContainerStats(ctx context.Context, client *client.Client, container *types.Container) Stat { +// In case stream is true, we use get a stream of results for container stats. From the stream we keep the second result. +// This is needed for podman use case where in case stream is false, no precpu stats are returned. The precpu stats +// are required for the cpu percentage calculation. We keep the second result as in the first result, the stats are not correct. +func exportContainerStats(ctx context.Context, client *client.Client, container *types.Container, stream bool, logger *logp.Logger) Stat { var event Stat event.Container = container - - containerStats, err := client.ContainerStats(ctx, container.ID, false) + containerStats, err := client.ContainerStats(ctx, container.ID, stream) if err != nil { + logger.Debugf("Failed fetching container stats: %v", err) return event } - defer containerStats.Body.Close() - decoder := json.NewDecoder(containerStats.Body) - decoder.Decode(&event.Stats) + // JSON decoder + decoder := json.NewDecoder(containerStats.Body) + if !stream { + if err := decoder.Decode(&event.Stats); err != nil { + logger.Debugf("Failed decoding event: %v", err) + return event + } + } else { + // handle stream. Take the second result. + count := 0 + for decoder.More() { + if err := decoder.Decode(&event.Stats); err != nil { + logger.Debugf("Failed decoding event: %v", err) + return event + } + + count++ + // Exit after the second result + if count == 2 { + break + } + } + } return event } diff --git a/metricbeat/module/docker/memory/memory.go b/metricbeat/module/docker/memory/memory.go index 140383de833..5c90c09d39c 100644 --- a/metricbeat/module/docker/memory/memory.go +++ b/metricbeat/module/docker/memory/memory.go @@ -43,6 +43,7 @@ type MetricSet struct { memoryService *MemoryService dockerClient *client.Client dedot bool + podman bool logger *logp.Logger } @@ -64,13 +65,14 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { memoryService: &MemoryService{}, dockerClient: dockerClient, dedot: config.DeDot, + podman: config.Podman, logger: logger, }, nil } // Fetch creates a list of memory events for each container. func (m *MetricSet) Fetch(r mb.ReporterV2) error { - stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout) + stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout, m.podman, m.Logger()) if err != nil { return fmt.Errorf("failed to get docker stats: %w", err) } diff --git a/metricbeat/module/docker/network/network.go b/metricbeat/module/docker/network/network.go index 8a70fd12446..34487aa4983 100644 --- a/metricbeat/module/docker/network/network.go +++ b/metricbeat/module/docker/network/network.go @@ -66,7 +66,7 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { // Fetch methods creates a list of network events for each container. func (m *MetricSet) Fetch(r mb.ReporterV2) error { - stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout) + stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout, false, m.Logger()) if err != nil { return fmt.Errorf("failed to get docker stats: %w", err) } diff --git a/metricbeat/module/docker/network_summary/network_summary.go b/metricbeat/module/docker/network_summary/network_summary.go index 9753b449add..9052e38580b 100644 --- a/metricbeat/module/docker/network_summary/network_summary.go +++ b/metricbeat/module/docker/network_summary/network_summary.go @@ -84,7 +84,7 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) { // of an error set the Error field of mb.Event or simply call report.Error(). func (m *MetricSet) Fetch(ctx context.Context, report mb.ReporterV2) error { - stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout) + stats, err := docker.FetchStats(m.dockerClient, m.Module().Config().Timeout, false, m.Logger()) if err != nil { return fmt.Errorf("failed to get docker stats: %w", err) } diff --git a/metricbeat/module/linux/conntrack/conntrack.go b/metricbeat/module/linux/conntrack/conntrack.go index 602a786b574..42c2114f92c 100644 --- a/metricbeat/module/linux/conntrack/conntrack.go +++ b/metricbeat/module/linux/conntrack/conntrack.go @@ -19,6 +19,7 @@ package conntrack import ( "fmt" + "os" "github.com/prometheus/procfs" @@ -50,7 +51,10 @@ type MetricSet struct { func New(base mb.BaseMetricSet) (mb.MetricSet, error) { cfgwarn.Beta("The linux conntrack metricset is beta.") - sys := base.Module().(resolve.Resolver) + sys, ok := base.Module().(resolve.Resolver) + if !ok { + return nil, fmt.Errorf("unexpected module type: %T", base.Module()) + } return &MetricSet{ BaseMetricSet: base, @@ -68,6 +72,9 @@ func (m *MetricSet) Fetch(report mb.ReporterV2) error { } conntrackStats, err := newFS.ConntrackStat() if err != nil { + if os.IsNotExist(err) { + err = mb.PartialMetricsError{Err: fmt.Errorf("nf_conntrack kernel module not loaded: %w", err)} + } return fmt.Errorf("error fetching conntrack stats: %w", err) } diff --git a/metricbeat/module/linux/conntrack/conntrack_test.go b/metricbeat/module/linux/conntrack/conntrack_test.go index cc3b2a05230..0d1ad2a4677 100644 --- a/metricbeat/module/linux/conntrack/conntrack_test.go +++ b/metricbeat/module/linux/conntrack/conntrack_test.go @@ -18,10 +18,15 @@ package conntrack import ( + "errors" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/elastic/beats/v7/metricbeat/mb" mbtest "github.com/elastic/beats/v7/metricbeat/mb/testing" _ "github.com/elastic/beats/v7/metricbeat/module/linux" "github.com/elastic/elastic-agent-libs/mapstr" @@ -60,6 +65,23 @@ func TestFetch(t *testing.T) { assert.Equal(t, testConn, rawEvent) } +func TestFetchConntrackModuleNotLoaded(t *testing.T) { + // Create a temporary directory to simulate a missing /proc/net/stat/nf_conntrack file + tmpDir := t.TempDir() + assert.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "proc/net/stat"), 0755)) + c := getConfig() + c["hostfs"] = tmpDir + + f := mbtest.NewReportingMetricSetV2Error(t, c) + events, errs := mbtest.ReportingFetchV2Error(f) + + require.Len(t, errs, 1) + err := errors.Join(errs...) + assert.ErrorAs(t, err, &mb.PartialMetricsError{}) + assert.Contains(t, err.Error(), "error fetching conntrack stats: nf_conntrack kernel module not loaded") + require.Empty(t, events) +} + func getConfig() map[string]interface{} { return map[string]interface{}{ "module": "linux", diff --git a/metricbeat/modules.d/docker.yml.disabled b/metricbeat/modules.d/docker.yml.disabled index 88af5d21288..726c535af1a 100644 --- a/metricbeat/modules.d/docker.yml.disabled +++ b/metricbeat/modules.d/docker.yml.disabled @@ -18,6 +18,9 @@ # If set to true, replace dots in labels with `_`. #labels.dedot: false + # Docker module supports metrics collection from podman's Docker-compatible API. In case of podman set to true. + # podman: false + # Skip metrics for certain device major numbers in docker/diskio. # Necessary on systems with software RAID, device mappers, # or other configurations where virtual disks will sum metrics from other disks. diff --git a/packetbeat/sniffer/sniffer.go b/packetbeat/sniffer/sniffer.go index d8043032a64..24c77180b46 100644 --- a/packetbeat/sniffer/sniffer.go +++ b/packetbeat/sniffer/sniffer.go @@ -25,6 +25,7 @@ import ( "os" "runtime" "strings" + "sync/atomic" "time" "github.com/google/gopacket" @@ -33,7 +34,6 @@ import ( "github.com/google/gopacket/pcapgo" "golang.org/x/sync/errgroup" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/beats/v7/packetbeat/config" @@ -51,7 +51,7 @@ type Sniffer struct { type sniffer struct { config config.InterfaceConfig - state atomic.Int32 // store snifferState + state *atomic.Int32 // store snifferState // device is the first active device after calling New. // It is not updated by default route polling. @@ -103,13 +103,14 @@ func New(id string, testMode bool, _ string, decoders map[string]Decoders, inter return nil, fmt.Errorf("no decoder for %s", iface.Device) } child := sniffer{ - state: atomic.MakeInt32(snifferInactive), + state: &atomic.Int32{}, followDefault: iface.PollDefaultRoute > 0 && strings.HasPrefix(iface.Device, "default_route"), id: id, idx: i, decoders: dec, log: s.log, } + child.state.Store(snifferInactive) s.log.Debugf("interface: %d, BPF filter: '%s'", i, iface.BpfFilter) @@ -373,7 +374,7 @@ func (s *sniffer) sniffHandle(ctx context.Context, handle snifferHandle, dec *de // Mark inactive sniffer as active. In case of the sniffer/packetbeat closing // before/while Run is executed, the state will be snifferClosing. // => return if state is already snifferClosing. - if !s.state.CAS(snifferInactive, snifferActive) { + if !s.state.CompareAndSwap(snifferInactive, snifferActive) { return nil } defer s.state.Store(snifferInactive) diff --git a/winlogbeat/_meta/fields.common.yml b/winlogbeat/_meta/fields.common.yml index ba2af74bb14..c1bef43568e 100644 --- a/winlogbeat/_meta/fields.common.yml +++ b/winlogbeat/_meta/fields.common.yml @@ -27,8 +27,8 @@ required: true description: > The event log API type used to read the record. The possible values are - "wineventlog" for the Windows Event Log API or "wineventlog-experimental" for its - experimental implementation. + "wineventlog" for the Windows Event Log XML reader or "wineventlog-raw" for its + more performant implementation. - name: activity_id type: keyword diff --git a/winlogbeat/beater/acker.go b/winlogbeat/beater/acker.go index e9cc2e77c33..d35a0166484 100644 --- a/winlogbeat/beater/acker.go +++ b/winlogbeat/beater/acker.go @@ -20,20 +20,20 @@ package beater import ( "context" "sync" + "sync/atomic" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/winlogbeat/checkpoint" ) type eventACKer struct { - active *atomic.Int + active *atomic.Int64 wg *sync.WaitGroup checkpoint *checkpoint.Checkpoint } func newEventACKer(checkpoint *checkpoint.Checkpoint) *eventACKer { return &eventACKer{ - active: atomic.NewInt(0), + active: &atomic.Int64{}, wg: &sync.WaitGroup{}, checkpoint: checkpoint, } @@ -55,7 +55,7 @@ func (a *eventACKer) ACKEvents(data []interface{}) { } // Mark events as done (subtract). - a.active.Add(-1 * len(data)) + a.active.Add(-1 * int64(len(data))) a.wg.Add(-1 * len(data)) } @@ -71,11 +71,11 @@ func (a *eventACKer) Wait(ctx context.Context) { // Add adds to the number of active events. func (a *eventACKer) Add(delta int) { - a.active.Add(delta) + a.active.Add(int64(delta)) a.wg.Add(delta) } // Active returns the number of active events (published but not yet ACKed). func (a *eventACKer) Active() int { - return a.active.Load() + return int(a.active.Load()) } diff --git a/winlogbeat/docs/fields.asciidoc b/winlogbeat/docs/fields.asciidoc index 567a78e4a91..c669e55bd3e 100644 --- a/winlogbeat/docs/fields.asciidoc +++ b/winlogbeat/docs/fields.asciidoc @@ -16282,7 +16282,7 @@ All fields specific to the Windows Event Log are defined here. *`winlog.api`*:: + -- -The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log API or "wineventlog-experimental" for its experimental implementation. +The event log API type used to read the record. The possible values are "wineventlog" for the Windows Event Log XML reader or "wineventlog-raw" for its more performant implementation. required: True diff --git a/winlogbeat/docs/winlogbeat-options.asciidoc b/winlogbeat/docs/winlogbeat-options.asciidoc index 1a68787dade..1702561a7a6 100644 --- a/winlogbeat/docs/winlogbeat-options.asciidoc +++ b/winlogbeat/docs/winlogbeat-options.asciidoc @@ -500,21 +500,17 @@ example of how to read from an `.evtx` file in the <>. ==== `event_logs.api` This selects the event log reader implementation that is used to read events -from the Windows APIs. You should only set this option when testing experimental -features. When the value is set to `wineventlog-experimental` Winlogbeat will -replace the default event log reader with the **experimental** implementation. -We are evaluating this implementation to see if it can provide increased -performance and reduce CPU usage. *{vista_and_newer}* +from the Windows APIs. When the value is set to `wineventlog-raw` Winlogbeat will +replace the default XML event log reader with a more performant implementation. +*{vista_and_newer}* [source,yaml] -------------------------------------------------------------------------------- winlogbeat.event_logs: - name: ForwardedEvents - api: wineventlog-experimental + api: wineventlog-raw -------------------------------------------------------------------------------- -There are a few notable differences in the events: - * If `include_xml` is `true` the performance will be the same as the default API, as performance improvements are lost when parsing the XML. diff --git a/winlogbeat/eventlog/wineventlog_experimental.go b/winlogbeat/eventlog/wineventlog_raw.go similarity index 88% rename from winlogbeat/eventlog/wineventlog_experimental.go rename to winlogbeat/eventlog/wineventlog_raw.go index 580adb1cfe2..7a6917e9b7a 100644 --- a/winlogbeat/eventlog/wineventlog_experimental.go +++ b/winlogbeat/eventlog/wineventlog_raw.go @@ -28,7 +28,6 @@ import ( "go.uber.org/multierr" "golang.org/x/sys/windows" - "github.com/elastic/beats/v7/libbeat/common/cfgwarn" "github.com/elastic/beats/v7/winlogbeat/checkpoint" win "github.com/elastic/beats/v7/winlogbeat/sys/wineventlog" conf "github.com/elastic/elastic-agent-libs/config" @@ -39,19 +38,23 @@ const ( // winEventLogExpApiName is the name used to identify the Windows Event Log API // as both an event type and an API. winEventLogExpAPIName = "wineventlog-experimental" + // winEventLogRawAPIName is the name used to identify the Windows Event Log API + // as both an event type and an API. + winEventLogRawAPIName = "wineventlog-raw" ) func init() { // Register wineventlog API if it is available. available, _ := win.IsAvailable() if available { - Register(winEventLogExpAPIName, 10, newWinEventLogExp, win.Channels) + Register(winEventLogExpAPIName, 10, newWinEventLogRaw, win.Channels) + Register(winEventLogRawAPIName, 11, newWinEventLogRaw, win.Channels) } } -// winEventLogExp implements the EventLog interface for reading from the Windows +// winEventLogRaw implements the EventLog interface for reading from the Windows // Event Log API. -type winEventLogExp struct { +type winEventLogRaw struct { config winEventLogConfig query string id string // Identifier of this event log. @@ -67,16 +70,14 @@ type winEventLogExp struct { metrics *inputMetrics } -// newWinEventLogExp creates and returns a new EventLog for reading event logs +// newWinEventLogRaw creates and returns a new EventLog for reading event logs // using the Windows Event Log. -func newWinEventLogExp(options *conf.C) (EventLog, error) { +func newWinEventLogRaw(options *conf.C) (EventLog, error) { var xmlQuery string var err error var isFile bool var log *logp.Logger - cfgwarn.Experimental("The %s event log reader is experimental.", winEventLogExpAPIName) - c := winEventLogConfig{BatchReadSize: 512} if err := readConfig(options, &c); err != nil { return nil, err @@ -115,7 +116,7 @@ func newWinEventLogExp(options *conf.C) (EventLog, error) { log = logp.NewLogger("wineventlog").With("id", id).With("channel", c.Name) } - l := &winEventLogExp{ + l := &winEventLogRaw{ config: c, query: xmlQuery, id: id, @@ -148,27 +149,27 @@ func newWinEventLogExp(options *conf.C) (EventLog, error) { return l, nil } -func (l *winEventLogExp) isForwarded() bool { +func (l *winEventLogRaw) isForwarded() bool { c := l.config return (c.Forwarded != nil && *c.Forwarded) || (c.Forwarded == nil && c.Name == "ForwardedEvents") } // Name returns the name of the event log (i.e. Application, Security, etc.). -func (l *winEventLogExp) Name() string { +func (l *winEventLogRaw) Name() string { return l.id } // Channel returns the event log's channel name. -func (l *winEventLogExp) Channel() string { +func (l *winEventLogRaw) Channel() string { return l.channelName } // IsFile returns true if the event log is an evtx file. -func (l *winEventLogExp) IsFile() bool { +func (l *winEventLogRaw) IsFile() bool { return l.file } -func (l *winEventLogExp) Open(state checkpoint.EventLogState) error { +func (l *winEventLogRaw) Open(state checkpoint.EventLogState) error { l.lastRead = state // we need to defer metrics initialization since when the event log // is used from winlog input it would register it twice due to CheckConfig calls @@ -185,7 +186,7 @@ func (l *winEventLogExp) Open(state checkpoint.EventLogState) error { return err } -func (l *winEventLogExp) open(state checkpoint.EventLogState) (win.EvtHandle, error) { +func (l *winEventLogRaw) open(state checkpoint.EventLogState) (win.EvtHandle, error) { var bookmark win.Bookmark if len(state.Bookmark) > 0 { var err error @@ -202,7 +203,7 @@ func (l *winEventLogExp) open(state checkpoint.EventLogState) (win.EvtHandle, er return l.openChannel(bookmark) } -func (l *winEventLogExp) openFile(state checkpoint.EventLogState, bookmark win.Bookmark) (win.EvtHandle, error) { +func (l *winEventLogRaw) openFile(state checkpoint.EventLogState, bookmark win.Bookmark) (win.EvtHandle, error) { path := l.channelName h, err := win.EvtQuery(0, path, l.query, win.EvtQueryFilePath|win.EvtQueryForwardDirection) @@ -239,7 +240,7 @@ func (l *winEventLogExp) openFile(state checkpoint.EventLogState, bookmark win.B return h, err } -func (l *winEventLogExp) openChannel(bookmark win.Bookmark) (win.EvtHandle, error) { +func (l *winEventLogRaw) openChannel(bookmark win.Bookmark) (win.EvtHandle, error) { // Using a pull subscription to receive events. See: // https://msdn.microsoft.com/en-us/library/windows/desktop/aa385771(v=vs.85).aspx#pull signalEvent, err := windows.CreateEvent(nil, 0, 0, nil) @@ -281,7 +282,7 @@ func (l *winEventLogExp) openChannel(bookmark win.Bookmark) (win.EvtHandle, erro } } -func (l *winEventLogExp) Read() ([]Record, error) { +func (l *winEventLogRaw) Read() ([]Record, error) { //nolint:prealloc // Avoid unnecessary preallocation for each reader every second when event log is inactive. var records []Record defer func() { @@ -319,7 +320,7 @@ func (l *winEventLogExp) Read() ([]Record, error) { return records, nil } -func (l *winEventLogExp) processHandle(h win.EvtHandle) (*Record, error) { +func (l *winEventLogRaw) processHandle(h win.EvtHandle) (*Record, error) { defer h.Close() // NOTE: Render can return an error and a partial event. @@ -357,7 +358,7 @@ func (l *winEventLogExp) processHandle(h win.EvtHandle) (*Record, error) { return r, nil } -func (l *winEventLogExp) createBookmarkFromEvent(evtHandle win.EvtHandle) (string, error) { +func (l *winEventLogRaw) createBookmarkFromEvent(evtHandle win.EvtHandle) (string, error) { bookmark, err := win.NewBookmarkFromEvent(evtHandle) if err != nil { return "", fmt.Errorf("failed to create new bookmark from event handle: %w", err) @@ -367,18 +368,18 @@ func (l *winEventLogExp) createBookmarkFromEvent(evtHandle win.EvtHandle) (strin return bookmark.XML() } -func (l *winEventLogExp) Reset() error { +func (l *winEventLogRaw) Reset() error { l.log.Debug("Closing event log reader handles for reset.") return l.close() } -func (l *winEventLogExp) Close() error { +func (l *winEventLogRaw) Close() error { l.log.Debug("Closing event log reader handles.") l.metrics.close() return l.close() } -func (l *winEventLogExp) close() error { +func (l *winEventLogRaw) close() error { if l.iterator == nil { return l.renderer.Close() } diff --git a/winlogbeat/eventlog/wineventlog_test.go b/winlogbeat/eventlog/wineventlog_test.go index 3277bb5d7f1..9aafe31a258 100644 --- a/winlogbeat/eventlog/wineventlog_test.go +++ b/winlogbeat/eventlog/wineventlog_test.go @@ -170,11 +170,11 @@ func TestWindowsEventLogAPI(t *testing.T) { testWindowsEventLog(t, winEventLogAPIName, false) } -func TestWindowsEventLogAPIExperimental(t *testing.T) { - // for the experimental api using include xml behave differently than not +func TestWindowsEventLogAPIRaw(t *testing.T) { + // for the raw api using include xml behave differently than not // so we must test both settings - testWindowsEventLog(t, winEventLogExpAPIName, true) - testWindowsEventLog(t, winEventLogExpAPIName, false) + testWindowsEventLog(t, winEventLogRawAPIName, true) + testWindowsEventLog(t, winEventLogRawAPIName, false) } func testWindowsEventLog(t *testing.T, api string, includeXML bool) { @@ -407,8 +407,8 @@ func openLog(t testing.TB, api string, state *checkpoint.EventLogState, config m switch api { case winEventLogAPIName: log, err = newWinEventLog(cfg) - case winEventLogExpAPIName: - log, err = newWinEventLogExp(cfg) + case winEventLogRawAPIName: + log, err = newWinEventLogRaw(cfg) default: t.Fatalf("Unknown API name: '%s'", api) } diff --git a/winlogbeat/include/fields.go b/winlogbeat/include/fields.go index b40185bdf54..e372ca95ed2 100644 --- a/winlogbeat/include/fields.go +++ b/winlogbeat/include/fields.go @@ -32,5 +32,5 @@ func init() { // AssetBuildFieldsFieldsCommonYml returns asset data. // This is the base64 encoded zlib format compressed contents of build/fields/fields.common.yml. func AssetBuildFieldsFieldsCommonYml() string { - return "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" + return "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" } diff --git a/winlogbeat/sys/wineventlog/renderer_test.go b/winlogbeat/sys/wineventlog/renderer_test.go index 339122d8a29..01089b5a112 100644 --- a/winlogbeat/sys/wineventlog/renderer_test.go +++ b/winlogbeat/sys/wineventlog/renderer_test.go @@ -26,13 +26,13 @@ import ( "runtime" "strconv" "strings" + "sync/atomic" "testing" "text/template" "time" "github.com/stretchr/testify/assert" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/winlogbeat/sys/winevent" "github.com/elastic/elastic-agent-libs/logp" ) @@ -297,7 +297,7 @@ func BenchmarkRenderer(b *testing.B) { defer itr.Close() defer r.Close() - count := atomic.NewUint64(0) + count := atomic.Uint64{} start := time.Now() b.ResetTimer() @@ -314,7 +314,7 @@ func BenchmarkRenderer(b *testing.B) { b.Fatal(err) } - count.Inc() + count.Add(1) } elapsed := time.Since(start) @@ -326,7 +326,7 @@ func BenchmarkRenderer(b *testing.B) { defer itr.Close() defer r.Close() - count := atomic.NewUint64(0) + var count atomic.Uint64 start := time.Now() b.ResetTimer() @@ -343,7 +343,7 @@ func BenchmarkRenderer(b *testing.B) { if err != nil { b.Fatal(err) } - count.Inc() + count.Add(1) } }) diff --git a/winlogbeat/sys/wineventlog/testdata/experimental.evtx b/winlogbeat/sys/wineventlog/testdata/raw.evtx similarity index 100% rename from winlogbeat/sys/wineventlog/testdata/experimental.evtx rename to winlogbeat/sys/wineventlog/testdata/raw.evtx diff --git a/winlogbeat/sys/wineventlog/testdata/experimental.xml b/winlogbeat/sys/wineventlog/testdata/raw.xml similarity index 100% rename from winlogbeat/sys/wineventlog/testdata/experimental.xml rename to winlogbeat/sys/wineventlog/testdata/raw.xml diff --git a/winlogbeat/sys/wineventlog/wineventlog_windows_test.go b/winlogbeat/sys/wineventlog/wineventlog_windows_test.go index e6b494e3b24..8c23098cd3a 100644 --- a/winlogbeat/sys/wineventlog/wineventlog_windows_test.go +++ b/winlogbeat/sys/wineventlog/wineventlog_windows_test.go @@ -44,13 +44,13 @@ func TestWinEventLog(t *testing.T) { }{ {path: "application-windows-error-reporting.evtx", events: 1}, {path: "sysmon-9.01.evtx", events: 32}, - {path: "ec1.evtx", events: 1}, // eventcreate /id 1000 /t error /l application /d "My custom error event for the application log" - {path: "ec2.evtx", events: 1}, // eventcreate /id 999 /t error /l application /so WinWord /d "Winword event 999 happened due to low diskspace" - {path: "ec3.evtx", events: 1}, // eventcreate /id 5 /t error /l system /d "Catastrophe!" - {path: "ec4.evtx", events: 1}, // eventcreate /id 5 /t error /l system /so Backup /d "Backup failure" - {path: "ec3and4.evtx", events: 2}, // ec3 and ec3 exported as a single evtx. - {path: "original.evtx", events: 5}, // a capture from a short generation of the eventlog WindowsEventLogAPI test. - {path: "experimental.evtx", events: 5}, // a capture from a short generation of the eventlog WindowsEventLogAPIExperimental test. + {path: "ec1.evtx", events: 1}, // eventcreate /id 1000 /t error /l application /d "My custom error event for the application log" + {path: "ec2.evtx", events: 1}, // eventcreate /id 999 /t error /l application /so WinWord /d "Winword event 999 happened due to low diskspace" + {path: "ec3.evtx", events: 1}, // eventcreate /id 5 /t error /l system /d "Catastrophe!" + {path: "ec4.evtx", events: 1}, // eventcreate /id 5 /t error /l system /so Backup /d "Backup failure" + {path: "ec3and4.evtx", events: 2}, // ec3 and ec3 exported as a single evtx. + {path: "original.evtx", events: 5}, // a capture from a short generation of the eventlog WindowsEventLogAPI test. + {path: "raw.evtx", events: 5}, // a capture from a short generation of the eventlog WindowsEventLogAPIRaw test. } { t.Run(test.path, func(t *testing.T) { evtx, err := filepath.Abs(filepath.Join("testdata", test.path)) diff --git a/x-pack/filebeat/docs/inputs/input-azure-eventhub.asciidoc b/x-pack/filebeat/docs/inputs/input-azure-eventhub.asciidoc index 81cfdc66d52..808c46b8333 100644 --- a/x-pack/filebeat/docs/inputs/input-azure-eventhub.asciidoc +++ b/x-pack/filebeat/docs/inputs/input-azure-eventhub.asciidoc @@ -13,7 +13,10 @@ Users can make use of the `azure-eventhub` input in order to read messages from The azure-eventhub input implementation is based on the the event processor host (EPH is intended to be run across multiple processes and machines while load balancing message consumers more on this here https://github.com/Azure/azure-event-hubs-go#event-processor-host, https://docs.microsoft.com/en-us/azure/event-hubs/event-hubs-event-processor-host). State such as leases on partitions and checkpoints in the event stream are shared between receivers using an Azure Storage container. For this reason, as a prerequisite to using this input, users will have to create or use an existing storage account. - +Users can enable internal logs tracing for this input by setting the environment +variable `BEATS_AZURE_EVENTHUB_INPUT_TRACING_ENABLED: true`. When enabled, +this input will log additional information to the logs. Additional information +includes partition ownership, blob lease information, and other internal state. Example configuration: diff --git a/x-pack/filebeat/docs/inputs/input-cel.asciidoc b/x-pack/filebeat/docs/inputs/input-cel.asciidoc index 8e062025b24..c18a687653c 100644 --- a/x-pack/filebeat/docs/inputs/input-cel.asciidoc +++ b/x-pack/filebeat/docs/inputs/input-cel.asciidoc @@ -815,6 +815,13 @@ dump is configured, it is recommended that data input sizes be reduced to avoid making dumps that are intractable to analysis. To delete existing failure dumps, set `failure_dump.enabled` to false without unsetting the filename option. +[[cel-record-coverage]] +[float] +==== `record_coverage` + +This specifies that CEL code evaluation coverage should be recorded and logged in debug logs. This is a +developer-only option. + [float] === Metrics diff --git a/x-pack/filebeat/docs/inputs/input-gcs.asciidoc b/x-pack/filebeat/docs/inputs/input-gcs.asciidoc index 2a762ddec18..d85a7393440 100644 --- a/x-pack/filebeat/docs/inputs/input-gcs.asciidoc +++ b/x-pack/filebeat/docs/inputs/input-gcs.asciidoc @@ -164,6 +164,7 @@ Now let's explore the configuration attributes a bit more elaborately. 11. <> 12. <> 13. <> + 14. <> [id="attrib-project-id"] @@ -397,6 +398,45 @@ filebeat.inputs: The GCS APIs don't provide a direct way to filter files based on the timestamp, so the input will download all the files and then filter them based on the timestamp. This can cause a bottleneck in processing if the number of files are very high. It is recommended to use this attribute only when the number of files are limited or ample resources are available. This option scales vertically and not horizontally. +[id="attrib-retry-gcs"] +[float] +==== `retry` + +This attribute can be used to configure a list of sub attributes that directly control how the input should behave when a download for a file/object fails or gets interrupted. + + - `max_attempts`: This attribute defines the maximum number of retry attempts(including the initial api call) that should be attempted for a retryable error. The default value for this is `3`. + - `initial_backoff_duration`: This attribute defines the initial backoff time. The default value for this is `1s`. + - `max_backoff_duration`: This attribute defines the maximum backoff time. The default value for this is `30s`. + - `backoff_multiplier`: This attribute defines the backoff multiplication factor. The default value for this is `2`. + +NOTE: The `initial_backoff_duration` and `max_backoff_duration` attributes must have time units. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. + +By configuring these attributes, the user is given the flexibility to control how the input should behave when a download fails or gets interrupted. This attribute can only be +specified at the root level of the configuration and not at the bucket level. It applies uniformly to all the buckets. + +An example configuration is given below :- + +[source, yml] +---- +filebeat.inputs: +- type: gcs + project_id: my_project_id + auth.credentials_file.path: {{file_path}}/{{creds_file_name}}.json + retry: + max_attempts: 3 + initial_backoff_duration: 2s + max_backoff_duration: 60s + backoff_multiplier: 2 + buckets: + - name: obs-bucket + max_workers: 3 + poll: true + poll_interval: 11m + bucket_timeout: 10m +---- + +When configuring the `retry` attribute, the user should consider the `bucket_timeout` value. The `retry` attribute should be configured in such a way that the retries are completed within the `bucket_timeout` window. If the `retry` attribute is configured in such a way that the retries are not completed successfully within the `bucket_timeout` window, the input will suffer a `context timeout` for that specific object/file which it was retrying. This can cause gaps in ingested data to pile up over time. + [id="bucket-overrides"] *The sample configs below will explain the bucket level overriding of attributes a bit further :-* diff --git a/x-pack/filebeat/input/awss3/interfaces.go b/x-pack/filebeat/input/awss3/interfaces.go index 6a3b119303b..512e95893b4 100644 --- a/x-pack/filebeat/input/awss3/interfaces.go +++ b/x-pack/filebeat/input/awss3/interfaces.go @@ -27,11 +27,10 @@ import ( ) // Run 'go generate' to create mocks that are used in tests. -//go:generate go install github.com/golang/mock/mockgen@v1.6.0 -//go:generate mockgen -source=interfaces.go -destination=mock_interfaces_test.go -package awss3 -mock_names=sqsAPI=MockSQSAPI,sqsProcessor=MockSQSProcessor,s3API=MockS3API,s3Pager=MockS3Pager,s3ObjectHandlerFactory=MockS3ObjectHandlerFactory,s3ObjectHandler=MockS3ObjectHandler -//go:generate mockgen -destination=mock_publisher_test.go -package=awss3 -mock_names=Client=MockBeatClient,Pipeline=MockBeatPipeline github.com/elastic/beats/v7/libbeat/beat Client,Pipeline -//go:generate go-licenser -license Elastic . -//go:generate goimports -w -local github.com/elastic . +//go:generate go run go.uber.org/mock/mockgen -source=interfaces.go -destination=mock_interfaces_test.go -package awss3 -mock_names=sqsAPI=MockSQSAPI,sqsProcessor=MockSQSProcessor,s3API=MockS3API,s3Pager=MockS3Pager,s3ObjectHandlerFactory=MockS3ObjectHandlerFactory,s3ObjectHandler=MockS3ObjectHandler +//go:generate go run go.uber.org/mock/mockgen -destination=mock_publisher_test.go -package=awss3 -mock_names=Client=MockBeatClient,Pipeline=MockBeatPipeline github.com/elastic/beats/v7/libbeat/beat Client,Pipeline +//go:generate go run github.com/elastic/go-licenser -license Elastic . +//go:generate go run golang.org/x/tools/cmd/goimports -w -local github.com/elastic . // ------ // SQS interfaces diff --git a/x-pack/filebeat/input/awss3/metrics_test.go b/x-pack/filebeat/input/awss3/metrics_test.go index e153d321e9f..33d3b9a513b 100644 --- a/x-pack/filebeat/input/awss3/metrics_test.go +++ b/x-pack/filebeat/input/awss3/metrics_test.go @@ -5,12 +5,12 @@ package awss3 import ( + "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/elastic-agent-libs/monitoring" ) diff --git a/x-pack/filebeat/input/awss3/mock_interfaces_test.go b/x-pack/filebeat/input/awss3/mock_interfaces_test.go index 086ca34136f..b611802dbad 100644 --- a/x-pack/filebeat/input/awss3/mock_interfaces_test.go +++ b/x-pack/filebeat/input/awss3/mock_interfaces_test.go @@ -4,6 +4,11 @@ // Code generated by MockGen. DO NOT EDIT. // Source: interfaces.go +// +// Generated by this command: +// +// mockgen -source=interfaces.go -destination=mock_interfaces_test.go -package awss3 -mock_names=sqsAPI=MockSQSAPI,sqsProcessor=MockSQSProcessor,s3API=MockS3API,s3Pager=MockS3Pager,s3ObjectHandlerFactory=MockS3ObjectHandlerFactory,s3ObjectHandler=MockS3ObjectHandler +// // Package awss3 is a generated GoMock package. package awss3 @@ -15,7 +20,7 @@ import ( s3 "github.com/aws/aws-sdk-go-v2/service/s3" types "github.com/aws/aws-sdk-go-v2/service/sqs/types" - gomock "github.com/golang/mock/gomock" + gomock "go.uber.org/mock/gomock" beat "github.com/elastic/beats/v7/libbeat/beat" logp "github.com/elastic/elastic-agent-libs/logp" @@ -25,6 +30,7 @@ import ( type MockSQSAPI struct { ctrl *gomock.Controller recorder *MockSQSAPIMockRecorder + isgomock struct{} } // MockSQSAPIMockRecorder is the mock recorder for MockSQSAPI. @@ -53,7 +59,7 @@ func (m *MockSQSAPI) ChangeMessageVisibility(ctx context.Context, msg *types.Mes } // ChangeMessageVisibility indicates an expected call of ChangeMessageVisibility. -func (mr *MockSQSAPIMockRecorder) ChangeMessageVisibility(ctx, msg, timeout interface{}) *gomock.Call { +func (mr *MockSQSAPIMockRecorder) ChangeMessageVisibility(ctx, msg, timeout any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeMessageVisibility", reflect.TypeOf((*MockSQSAPI)(nil).ChangeMessageVisibility), ctx, msg, timeout) } @@ -67,7 +73,7 @@ func (m *MockSQSAPI) DeleteMessage(ctx context.Context, msg *types.Message) erro } // DeleteMessage indicates an expected call of DeleteMessage. -func (mr *MockSQSAPIMockRecorder) DeleteMessage(ctx, msg interface{}) *gomock.Call { +func (mr *MockSQSAPIMockRecorder) DeleteMessage(ctx, msg any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMessage", reflect.TypeOf((*MockSQSAPI)(nil).DeleteMessage), ctx, msg) } @@ -82,7 +88,7 @@ func (m *MockSQSAPI) GetQueueAttributes(ctx context.Context, attr []types.QueueA } // GetQueueAttributes indicates an expected call of GetQueueAttributes. -func (mr *MockSQSAPIMockRecorder) GetQueueAttributes(ctx, attr interface{}) *gomock.Call { +func (mr *MockSQSAPIMockRecorder) GetQueueAttributes(ctx, attr any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueueAttributes", reflect.TypeOf((*MockSQSAPI)(nil).GetQueueAttributes), ctx, attr) } @@ -97,7 +103,7 @@ func (m *MockSQSAPI) ReceiveMessage(ctx context.Context, maxMessages int) ([]typ } // ReceiveMessage indicates an expected call of ReceiveMessage. -func (mr *MockSQSAPIMockRecorder) ReceiveMessage(ctx, maxMessages interface{}) *gomock.Call { +func (mr *MockSQSAPIMockRecorder) ReceiveMessage(ctx, maxMessages any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveMessage", reflect.TypeOf((*MockSQSAPI)(nil).ReceiveMessage), ctx, maxMessages) } @@ -106,6 +112,7 @@ func (mr *MockSQSAPIMockRecorder) ReceiveMessage(ctx, maxMessages interface{}) * type MockSQSProcessor struct { ctrl *gomock.Controller recorder *MockSQSProcessorMockRecorder + isgomock struct{} } // MockSQSProcessorMockRecorder is the mock recorder for MockSQSProcessor. @@ -134,7 +141,7 @@ func (m *MockSQSProcessor) ProcessSQS(ctx context.Context, msg *types.Message, e } // ProcessSQS indicates an expected call of ProcessSQS. -func (mr *MockSQSProcessorMockRecorder) ProcessSQS(ctx, msg, eventCallback interface{}) *gomock.Call { +func (mr *MockSQSProcessorMockRecorder) ProcessSQS(ctx, msg, eventCallback any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessSQS", reflect.TypeOf((*MockSQSProcessor)(nil).ProcessSQS), ctx, msg, eventCallback) } @@ -143,6 +150,7 @@ func (mr *MockSQSProcessorMockRecorder) ProcessSQS(ctx, msg, eventCallback inter type MockS3API struct { ctrl *gomock.Controller recorder *MockS3APIMockRecorder + isgomock struct{} } // MockS3APIMockRecorder is the mock recorder for MockS3API. @@ -172,7 +180,7 @@ func (m *MockS3API) CopyObject(ctx context.Context, region, from_bucket, to_buck } // CopyObject indicates an expected call of CopyObject. -func (mr *MockS3APIMockRecorder) CopyObject(ctx, region, from_bucket, to_bucket, from_key, to_key interface{}) *gomock.Call { +func (mr *MockS3APIMockRecorder) CopyObject(ctx, region, from_bucket, to_bucket, from_key, to_key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObject", reflect.TypeOf((*MockS3API)(nil).CopyObject), ctx, region, from_bucket, to_bucket, from_key, to_key) } @@ -187,7 +195,7 @@ func (m *MockS3API) DeleteObject(ctx context.Context, region, bucket, key string } // DeleteObject indicates an expected call of DeleteObject. -func (mr *MockS3APIMockRecorder) DeleteObject(ctx, region, bucket, key interface{}) *gomock.Call { +func (mr *MockS3APIMockRecorder) DeleteObject(ctx, region, bucket, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockS3API)(nil).DeleteObject), ctx, region, bucket, key) } @@ -202,7 +210,7 @@ func (m *MockS3API) GetObject(ctx context.Context, region, bucket, key string) ( } // GetObject indicates an expected call of GetObject. -func (mr *MockS3APIMockRecorder) GetObject(ctx, region, bucket, key interface{}) *gomock.Call { +func (mr *MockS3APIMockRecorder) GetObject(ctx, region, bucket, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockS3API)(nil).GetObject), ctx, region, bucket, key) } @@ -216,7 +224,7 @@ func (m *MockS3API) ListObjectsPaginator(bucket, prefix string) s3Pager { } // ListObjectsPaginator indicates an expected call of ListObjectsPaginator. -func (mr *MockS3APIMockRecorder) ListObjectsPaginator(bucket, prefix interface{}) *gomock.Call { +func (mr *MockS3APIMockRecorder) ListObjectsPaginator(bucket, prefix any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsPaginator", reflect.TypeOf((*MockS3API)(nil).ListObjectsPaginator), bucket, prefix) } @@ -225,6 +233,7 @@ func (mr *MockS3APIMockRecorder) ListObjectsPaginator(bucket, prefix interface{} type Mocks3Getter struct { ctrl *gomock.Controller recorder *Mocks3GetterMockRecorder + isgomock struct{} } // Mocks3GetterMockRecorder is the mock recorder for Mocks3Getter. @@ -254,7 +263,7 @@ func (m *Mocks3Getter) GetObject(ctx context.Context, region, bucket, key string } // GetObject indicates an expected call of GetObject. -func (mr *Mocks3GetterMockRecorder) GetObject(ctx, region, bucket, key interface{}) *gomock.Call { +func (mr *Mocks3GetterMockRecorder) GetObject(ctx, region, bucket, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*Mocks3Getter)(nil).GetObject), ctx, region, bucket, key) } @@ -263,6 +272,7 @@ func (mr *Mocks3GetterMockRecorder) GetObject(ctx, region, bucket, key interface type Mocks3Mover struct { ctrl *gomock.Controller recorder *Mocks3MoverMockRecorder + isgomock struct{} } // Mocks3MoverMockRecorder is the mock recorder for Mocks3Mover. @@ -292,7 +302,7 @@ func (m *Mocks3Mover) CopyObject(ctx context.Context, region, from_bucket, to_bu } // CopyObject indicates an expected call of CopyObject. -func (mr *Mocks3MoverMockRecorder) CopyObject(ctx, region, from_bucket, to_bucket, from_key, to_key interface{}) *gomock.Call { +func (mr *Mocks3MoverMockRecorder) CopyObject(ctx, region, from_bucket, to_bucket, from_key, to_key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObject", reflect.TypeOf((*Mocks3Mover)(nil).CopyObject), ctx, region, from_bucket, to_bucket, from_key, to_key) } @@ -307,7 +317,7 @@ func (m *Mocks3Mover) DeleteObject(ctx context.Context, region, bucket, key stri } // DeleteObject indicates an expected call of DeleteObject. -func (mr *Mocks3MoverMockRecorder) DeleteObject(ctx, region, bucket, key interface{}) *gomock.Call { +func (mr *Mocks3MoverMockRecorder) DeleteObject(ctx, region, bucket, key any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*Mocks3Mover)(nil).DeleteObject), ctx, region, bucket, key) } @@ -316,6 +326,7 @@ func (mr *Mocks3MoverMockRecorder) DeleteObject(ctx, region, bucket, key interfa type Mocks3Lister struct { ctrl *gomock.Controller recorder *Mocks3ListerMockRecorder + isgomock struct{} } // Mocks3ListerMockRecorder is the mock recorder for Mocks3Lister. @@ -344,7 +355,7 @@ func (m *Mocks3Lister) ListObjectsPaginator(bucket, prefix string) s3Pager { } // ListObjectsPaginator indicates an expected call of ListObjectsPaginator. -func (mr *Mocks3ListerMockRecorder) ListObjectsPaginator(bucket, prefix interface{}) *gomock.Call { +func (mr *Mocks3ListerMockRecorder) ListObjectsPaginator(bucket, prefix any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsPaginator", reflect.TypeOf((*Mocks3Lister)(nil).ListObjectsPaginator), bucket, prefix) } @@ -353,6 +364,7 @@ func (mr *Mocks3ListerMockRecorder) ListObjectsPaginator(bucket, prefix interfac type MockS3Pager struct { ctrl *gomock.Controller recorder *MockS3PagerMockRecorder + isgomock struct{} } // MockS3PagerMockRecorder is the mock recorder for MockS3Pager. @@ -389,7 +401,7 @@ func (mr *MockS3PagerMockRecorder) HasMorePages() *gomock.Call { // NextPage mocks base method. func (m *MockS3Pager) NextPage(ctx context.Context, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) { m.ctrl.T.Helper() - varargs := []interface{}{ctx} + varargs := []any{ctx} for _, a := range optFns { varargs = append(varargs, a) } @@ -400,9 +412,9 @@ func (m *MockS3Pager) NextPage(ctx context.Context, optFns ...func(*s3.Options)) } // NextPage indicates an expected call of NextPage. -func (mr *MockS3PagerMockRecorder) NextPage(ctx interface{}, optFns ...interface{}) *gomock.Call { +func (mr *MockS3PagerMockRecorder) NextPage(ctx any, optFns ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{ctx}, optFns...) + varargs := append([]any{ctx}, optFns...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockS3Pager)(nil).NextPage), varargs...) } @@ -410,6 +422,7 @@ func (mr *MockS3PagerMockRecorder) NextPage(ctx interface{}, optFns ...interface type MockS3ObjectHandlerFactory struct { ctrl *gomock.Controller recorder *MockS3ObjectHandlerFactoryMockRecorder + isgomock struct{} } // MockS3ObjectHandlerFactoryMockRecorder is the mock recorder for MockS3ObjectHandlerFactory. @@ -438,7 +451,7 @@ func (m *MockS3ObjectHandlerFactory) Create(ctx context.Context, obj s3EventV2) } // Create indicates an expected call of Create. -func (mr *MockS3ObjectHandlerFactoryMockRecorder) Create(ctx, obj interface{}) *gomock.Call { +func (mr *MockS3ObjectHandlerFactoryMockRecorder) Create(ctx, obj any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockS3ObjectHandlerFactory)(nil).Create), ctx, obj) } @@ -447,6 +460,7 @@ func (mr *MockS3ObjectHandlerFactoryMockRecorder) Create(ctx, obj interface{}) * type MockS3ObjectHandler struct { ctrl *gomock.Controller recorder *MockS3ObjectHandlerMockRecorder + isgomock struct{} } // MockS3ObjectHandlerMockRecorder is the mock recorder for MockS3ObjectHandler. @@ -489,7 +503,7 @@ func (m *MockS3ObjectHandler) ProcessS3Object(log *logp.Logger, eventCallback fu } // ProcessS3Object indicates an expected call of ProcessS3Object. -func (mr *MockS3ObjectHandlerMockRecorder) ProcessS3Object(log, eventCallback interface{}) *gomock.Call { +func (mr *MockS3ObjectHandlerMockRecorder) ProcessS3Object(log, eventCallback any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessS3Object", reflect.TypeOf((*MockS3ObjectHandler)(nil).ProcessS3Object), log, eventCallback) } diff --git a/x-pack/filebeat/input/awss3/mock_publisher_test.go b/x-pack/filebeat/input/awss3/mock_publisher_test.go index efbd5bcef97..3bffc9142b3 100644 --- a/x-pack/filebeat/input/awss3/mock_publisher_test.go +++ b/x-pack/filebeat/input/awss3/mock_publisher_test.go @@ -4,6 +4,11 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/elastic/beats/v7/libbeat/beat (interfaces: Client,Pipeline) +// +// Generated by this command: +// +// mockgen -destination=mock_publisher_test.go -package=awss3 -mock_names=Client=MockBeatClient,Pipeline=MockBeatPipeline github.com/elastic/beats/v7/libbeat/beat Client,Pipeline +// // Package awss3 is a generated GoMock package. package awss3 @@ -11,7 +16,7 @@ package awss3 import ( reflect "reflect" - gomock "github.com/golang/mock/gomock" + gomock "go.uber.org/mock/gomock" beat "github.com/elastic/beats/v7/libbeat/beat" ) @@ -20,6 +25,7 @@ import ( type MockBeatClient struct { ctrl *gomock.Controller recorder *MockBeatClientMockRecorder + isgomock struct{} } // MockBeatClientMockRecorder is the mock recorder for MockBeatClient. @@ -60,7 +66,7 @@ func (m *MockBeatClient) Publish(arg0 beat.Event) { } // Publish indicates an expected call of Publish. -func (mr *MockBeatClientMockRecorder) Publish(arg0 interface{}) *gomock.Call { +func (mr *MockBeatClientMockRecorder) Publish(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockBeatClient)(nil).Publish), arg0) } @@ -72,7 +78,7 @@ func (m *MockBeatClient) PublishAll(arg0 []beat.Event) { } // PublishAll indicates an expected call of PublishAll. -func (mr *MockBeatClientMockRecorder) PublishAll(arg0 interface{}) *gomock.Call { +func (mr *MockBeatClientMockRecorder) PublishAll(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishAll", reflect.TypeOf((*MockBeatClient)(nil).PublishAll), arg0) } @@ -81,6 +87,7 @@ func (mr *MockBeatClientMockRecorder) PublishAll(arg0 interface{}) *gomock.Call type MockBeatPipeline struct { ctrl *gomock.Controller recorder *MockBeatPipelineMockRecorder + isgomock struct{} } // MockBeatPipelineMockRecorder is the mock recorder for MockBeatPipeline. @@ -125,7 +132,7 @@ func (m *MockBeatPipeline) ConnectWith(arg0 beat.ClientConfig) (beat.Client, err } // ConnectWith indicates an expected call of ConnectWith. -func (mr *MockBeatPipelineMockRecorder) ConnectWith(arg0 interface{}) *gomock.Call { +func (mr *MockBeatPipelineMockRecorder) ConnectWith(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectWith", reflect.TypeOf((*MockBeatPipeline)(nil).ConnectWith), arg0) } diff --git a/x-pack/filebeat/input/awss3/s3_input.go b/x-pack/filebeat/input/awss3/s3_input.go index bc8a21495e5..88f28e39e83 100644 --- a/x-pack/filebeat/input/awss3/s3_input.go +++ b/x-pack/filebeat/input/awss3/s3_input.go @@ -115,8 +115,19 @@ func (in *s3PollerInput) runPoll(ctx context.Context) { } // Start reading data and wait for its processing to be done - in.readerLoop(ctx, workChan) + ids, ok := in.readerLoop(ctx, workChan) workerWg.Wait() + + if !ok { + in.log.Warn("skipping state registry cleanup as object reading ended with a non-ok return") + return + } + + // Perform state cleanup operation + err := in.states.CleanUp(ids) + if err != nil { + in.log.Errorf("failed to cleanup states: %v", err.Error()) + } } func (in *s3PollerInput) workerLoop(ctx context.Context, workChan <-chan state) { @@ -183,7 +194,10 @@ func (in *s3PollerInput) workerLoop(ctx context.Context, workChan <-chan state) } } -func (in *s3PollerInput) readerLoop(ctx context.Context, workChan chan<- state) { +// readerLoop performs the S3 object listing and emit state to work listeners if object needs to be processed. +// Returns all tracked state IDs correlates to all tracked S3 objects iff listing is successful. +// These IDs are intended to be used for state clean-up. +func (in *s3PollerInput) readerLoop(ctx context.Context, workChan chan<- state) (knownStateIDSlice []string, ok bool) { defer close(workChan) bucketName := getBucketNameFromARN(in.config.getBucketARN()) @@ -202,7 +216,7 @@ func (in *s3PollerInput) readerLoop(ctx context.Context, workChan chan<- state) circuitBreaker++ if circuitBreaker >= readerLoopMaxCircuitBreaker { in.log.Warnw(fmt.Sprintf("%d consecutive error when paginating listing, breaking the circuit.", circuitBreaker), "error", err) - break + return nil, false } } // add a backoff delay and try again @@ -219,6 +233,8 @@ func (in *s3PollerInput) readerLoop(ctx context.Context, workChan chan<- state) in.metrics.s3ObjectsListedTotal.Add(uint64(totListedObjects)) for _, object := range page.Contents { state := newState(bucketName, *object.Key, *object.ETag, *object.LastModified) + knownStateIDSlice = append(knownStateIDSlice, state.ID()) + if in.states.IsProcessed(state) { in.log.Debugw("skipping state.", "state", state) continue @@ -229,6 +245,8 @@ func (in *s3PollerInput) readerLoop(ctx context.Context, workChan chan<- state) in.metrics.s3ObjectsProcessedTotal.Inc() } } + + return knownStateIDSlice, true } func (in *s3PollerInput) s3EventForState(state state) s3EventV2 { diff --git a/x-pack/filebeat/input/awss3/s3_objects_test.go b/x-pack/filebeat/input/awss3/s3_objects_test.go index 432c5209d25..e6178c33a87 100644 --- a/x-pack/filebeat/input/awss3/s3_objects_test.go +++ b/x-pack/filebeat/input/awss3/s3_objects_test.go @@ -17,9 +17,9 @@ import ( awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/elastic/beats/v7/libbeat/beat" conf "github.com/elastic/elastic-agent-libs/config" diff --git a/x-pack/filebeat/input/awss3/s3_test.go b/x-pack/filebeat/input/awss3/s3_test.go index a1e9e02b426..2f79cb44a48 100644 --- a/x-pack/filebeat/input/awss3/s3_test.go +++ b/x-pack/filebeat/input/awss3/s3_test.go @@ -12,8 +12,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/elastic/elastic-agent-libs/logp" ) diff --git a/x-pack/filebeat/input/awss3/sqs_s3_event_test.go b/x-pack/filebeat/input/awss3/sqs_s3_event_test.go index c7962bb2f0f..ae4400dc0f2 100644 --- a/x-pack/filebeat/input/awss3/sqs_s3_event_test.go +++ b/x-pack/filebeat/input/awss3/sqs_s3_event_test.go @@ -16,9 +16,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sqs/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/elastic-agent-libs/logp" diff --git a/x-pack/filebeat/input/awss3/sqs_test.go b/x-pack/filebeat/input/awss3/sqs_test.go index 8bc25397eae..34575a47daf 100644 --- a/x-pack/filebeat/input/awss3/sqs_test.go +++ b/x-pack/filebeat/input/awss3/sqs_test.go @@ -15,9 +15,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/gofrs/uuid/v5" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/elastic-agent-libs/logp" diff --git a/x-pack/filebeat/input/awss3/states.go b/x-pack/filebeat/input/awss3/states.go index 8aef3de1c99..2bfb9f29cd8 100644 --- a/x-pack/filebeat/input/awss3/states.go +++ b/x-pack/filebeat/input/awss3/states.go @@ -21,7 +21,7 @@ const awsS3ObjectStatePrefix = "filebeat::aws-s3::state::" type states struct { // Completed S3 object states, indexed by state ID. // statesLock must be held to access states. - states map[string]state + states map[string]*state statesLock sync.Mutex // The store used to persist state changes to the registry. @@ -70,29 +70,61 @@ func (s *states) AddState(state state) error { id := state.ID() // Update in-memory copy s.statesLock.Lock() - s.states[id] = state + s.states[id] = &state s.statesLock.Unlock() // Persist to the registry s.storeLock.Lock() defer s.storeLock.Unlock() - key := awsS3ObjectStatePrefix + id - if err := s.store.Set(key, state); err != nil { + if err := s.store.Set(getStoreKey(id), state); err != nil { return err } return nil } +// CleanUp performs state and store cleanup based on provided knownIDs. +// knownIDs must contain valid currently tracked state IDs that must be known by this state registry. +// State and underlying storage will be cleaned if ID is no longer present in knownIDs set. +func (s *states) CleanUp(knownIDs []string) error { + knownIDHashSet := map[string]struct{}{} + for _, id := range knownIDs { + knownIDHashSet[id] = struct{}{} + } + + s.storeLock.Lock() + defer s.storeLock.Unlock() + s.statesLock.Lock() + defer s.statesLock.Unlock() + + for id := range s.states { + if _, contains := knownIDHashSet[id]; !contains { + // remove from sate & store as ID is no longer seen in known ID set + delete(s.states, id) + err := s.store.Remove(getStoreKey(id)) + if err != nil { + return fmt.Errorf("error while removing the state for ID %s: %w", id, err) + } + } + } + + return nil +} + func (s *states) Close() { s.storeLock.Lock() s.store.Close() s.storeLock.Unlock() } +// getStoreKey is a helper to generate the key used by underlying persistent storage +func getStoreKey(stateID string) string { + return awsS3ObjectStatePrefix + stateID +} + // loadS3StatesFromRegistry loads a copy of the registry states. // If prefix is set, entries will match the provided prefix(including empty prefix) -func loadS3StatesFromRegistry(log *logp.Logger, store *statestore.Store, prefix string) (map[string]state, error) { - stateTable := map[string]state{} +func loadS3StatesFromRegistry(log *logp.Logger, store *statestore.Store, prefix string) (map[string]*state, error) { + stateTable := map[string]*state{} err := store.Each(func(key string, dec statestore.ValueDecoder) (bool, error) { if !strings.HasPrefix(key, awsS3ObjectStatePrefix) { return true, nil @@ -117,9 +149,8 @@ func loadS3StatesFromRegistry(log *logp.Logger, store *statestore.Store, prefix // filter based on prefix and add entry to local copy if strings.HasPrefix(st.Key, prefix) { - stateTable[st.ID()] = st + stateTable[st.ID()] = &st } - return true, nil }) if err != nil { diff --git a/x-pack/filebeat/input/awss3/states_test.go b/x-pack/filebeat/input/awss3/states_test.go index 328b57003cd..fa604ed08d9 100644 --- a/x-pack/filebeat/input/awss3/states_test.go +++ b/x-pack/filebeat/input/awss3/states_test.go @@ -5,6 +5,7 @@ package awss3 import ( + "fmt" "testing" "time" @@ -42,7 +43,7 @@ func (s *testInputStore) CleanupInterval() time.Duration { func TestStatesAddStateAndIsProcessed(t *testing.T) { type stateTestCase struct { // An initialization callback to invoke on the (initially empty) states. - statesEdit func(states *states) + statesEdit func(states *states) error // The state to call IsProcessed on and the expected result state state @@ -62,42 +63,42 @@ func TestStatesAddStateAndIsProcessed(t *testing.T) { expectedIsProcessed: false, }, "not existing state": { - statesEdit: func(states *states) { - _ = states.AddState(testState2) + statesEdit: func(states *states) error { + return states.AddState(testState2) }, state: testState1, expectedIsProcessed: false, }, "existing state": { - statesEdit: func(states *states) { - _ = states.AddState(testState1) + statesEdit: func(states *states) error { + return states.AddState(testState1) }, state: testState1, expectedIsProcessed: true, }, "existing stored state is persisted": { - statesEdit: func(states *states) { + statesEdit: func(states *states) error { state := testState1 state.Stored = true - _ = states.AddState(state) + return states.AddState(state) }, state: testState1, shouldReload: true, expectedIsProcessed: true, }, "existing failed state is persisted": { - statesEdit: func(states *states) { + statesEdit: func(states *states) error { state := testState1 state.Failed = true - _ = states.AddState(state) + return states.AddState(state) }, state: testState1, shouldReload: true, expectedIsProcessed: true, }, "existing unprocessed state is not persisted": { - statesEdit: func(states *states) { - _ = states.AddState(testState1) + statesEdit: func(states *states) error { + return states.AddState(testState1) }, state: testState1, shouldReload: true, @@ -112,7 +113,8 @@ func TestStatesAddStateAndIsProcessed(t *testing.T) { states, err := newStates(nil, store, "") require.NoError(t, err, "states creation must succeed") if test.statesEdit != nil { - test.statesEdit(states) + err = test.statesEdit(states) + require.NoError(t, err, "states edit must succeed") } if test.shouldReload { states, err = newStates(nil, store, "") @@ -125,6 +127,76 @@ func TestStatesAddStateAndIsProcessed(t *testing.T) { } } +func TestStatesCleanUp(t *testing.T) { + bucketName := "test-bucket" + lModifiedTime := time.Unix(0, 0) + stateA := newState(bucketName, "a", "a-etag", lModifiedTime) + stateB := newState(bucketName, "b", "b-etag", lModifiedTime) + stateC := newState(bucketName, "c", "c-etag", lModifiedTime) + + tests := []struct { + name string + initStates []state + knownIDs []string + expectIDs []string + }{ + { + name: "No cleanup if not missing from known list", + initStates: []state{stateA, stateB, stateC}, + knownIDs: []string{stateA.ID(), stateB.ID(), stateC.ID()}, + expectIDs: []string{stateA.ID(), stateB.ID(), stateC.ID()}, + }, + { + name: "Clean up if missing from known list", + initStates: []state{stateA, stateB, stateC}, + knownIDs: []string{stateA.ID()}, + expectIDs: []string{stateA.ID()}, + }, + { + name: "Clean up everything", + initStates: []state{stateA, stateC}, // given A, C + knownIDs: []string{stateB.ID()}, // but known B + expectIDs: []string{}, // empty state & store + }, + { + name: "Empty known IDs are valid", + initStates: []state{stateA}, // given A + knownIDs: []string{}, // Known nothing + expectIDs: []string{}, // empty state & store + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + store := openTestStatestore() + statesInstance, err := newStates(nil, store, "") + require.NoError(t, err, "states creation must succeed") + + for _, s := range test.initStates { + err := statesInstance.AddState(s) + require.NoError(t, err, "state initialization must succeed") + } + + // perform cleanup + err = statesInstance.CleanUp(test.knownIDs) + require.NoError(t, err, "state cleanup must succeed") + + // validate + for _, id := range test.expectIDs { + // must be in local state + _, ok := statesInstance.states[id] + require.True(t, ok, fmt.Errorf("expected id %s in state, but got missing", id)) + + // must be in store + ok, err := statesInstance.store.Has(getStoreKey(id)) + require.NoError(t, err, "state has must succeed") + require.True(t, ok, fmt.Errorf("expected id %s in store, but got missing", id)) + } + }) + } + +} + func TestStatesPrefixHandling(t *testing.T) { logger := logp.NewLogger("state-prefix-testing") diff --git a/x-pack/filebeat/input/azureeventhub/tracer.go b/x-pack/filebeat/input/azureeventhub/tracer.go index f998a548e37..8ba77af3b4d 100644 --- a/x-pack/filebeat/input/azureeventhub/tracer.go +++ b/x-pack/filebeat/input/azureeventhub/tracer.go @@ -8,6 +8,7 @@ package azureeventhub import ( "context" + "os" "github.com/devigned/tab" @@ -15,7 +16,12 @@ import ( ) func init() { - tab.Register(new(logsOnlyTracer)) + // Register the logs tracer only if the environment variable is + // set to avoid the overhead of the tracer in environments where + // it's not needed. + if os.Getenv("BEATS_AZURE_EVENTHUB_INPUT_TRACING_ENABLED") == "true" { + tab.Register(new(logsOnlyTracer)) + } } // logsOnlyTracer manages the creation of the required diff --git a/x-pack/filebeat/input/benchmark/input.go b/x-pack/filebeat/input/benchmark/input.go index dd6d198cc40..e098d3e746b 100644 --- a/x-pack/filebeat/input/benchmark/input.go +++ b/x-pack/filebeat/input/benchmark/input.go @@ -60,7 +60,7 @@ func (bi *benchmarkInput) Test(ctx v2.TestContext) error { // Run starts the data generation. func (bi *benchmarkInput) Run(ctx v2.Context, publisher stateless.Publisher) error { var wg sync.WaitGroup - metrics := newInputMetrics(ctx.ID) + metrics := newInputMetrics(ctx) for i := uint8(0); i < bi.cfg.Threads; i++ { wg.Add(1) @@ -103,8 +103,8 @@ func runThread(ctx v2.Context, publisher stateless.Publisher, thread uint8, cfg ticker.Stop() return case <-ticker.C: - //don't want to block on filling doPublish channel - //so only send as many as it can hold right now + // don't want to block on filling doPublish channel + // so only send as many as it can hold right now numToSend := cap(pubChan) - len(pubChan) for i := 0; i < numToSend; i++ { pubChan <- true @@ -157,8 +157,8 @@ type inputMetrics struct { } // newInputMetrics returns an input metric for the benchmark processor. -func newInputMetrics(id string) *inputMetrics { - reg, unreg := inputmon.NewInputRegistry(inputName, id, nil) +func newInputMetrics(ctx v2.Context) *inputMetrics { + reg, unreg := inputmon.NewInputRegistry(inputName, ctx.ID, ctx.Agent.Monitoring.Namespace.GetRegistry()) out := &inputMetrics{ unregister: unreg, eventsPublished: monitoring.NewUint(reg, "events_published_total"), diff --git a/x-pack/filebeat/input/cel/config.go b/x-pack/filebeat/input/cel/config.go index aee095b199b..1906a309bdb 100644 --- a/x-pack/filebeat/input/cel/config.go +++ b/x-pack/filebeat/input/cel/config.go @@ -61,6 +61,10 @@ type config struct { // FailureDump configures failure dump behaviour. FailureDump *dumpConfig `config:"failure_dump"` + + // RecordCoverage indicates whether a program should + // record and log execution coverage. + RecordCoverage bool `config:"record_coverage"` } type redact struct { @@ -86,9 +90,13 @@ func (t *dumpConfig) enabled() bool { } func (c config) Validate() error { + if c.RecordCoverage { + logp.L().Named("input.cel").Warn("execution coverage enabled: " + + "see documentation for details: https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-cel.html#cel-record-coverage") + } if c.Redact == nil { logp.L().Named("input.cel").Warn("missing recommended 'redact' configuration: " + - "see documentation for details: https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-cel.html#_redact") + "see documentation for details: https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-input-cel.html#cel-state-redact") } if c.Interval <= 0 { return errors.New("interval must be greater than 0") @@ -106,7 +114,7 @@ func (c config) Validate() error { patterns = map[string]*regexp.Regexp{".": nil} } wantDump := c.FailureDump.enabled() && c.FailureDump.Filename != "" - _, _, err = newProgram(context.Background(), c.Program, root, nil, &http.Client{}, nil, nil, patterns, c.XSDs, logp.L().Named("input.cel"), nil, wantDump) + _, _, _, err = newProgram(context.Background(), c.Program, root, nil, &http.Client{}, nil, nil, patterns, c.XSDs, logp.L().Named("input.cel"), nil, wantDump, false) if err != nil { return fmt.Errorf("failed to check program: %w", err) } diff --git a/x-pack/filebeat/input/cel/input.go b/x-pack/filebeat/input/cel/input.go index 97ab8c9bee0..4a04836bf59 100644 --- a/x-pack/filebeat/input/cel/input.go +++ b/x-pack/filebeat/input/cel/input.go @@ -168,7 +168,8 @@ func (i input) run(env v2.Context, src *source, cursor map[string]interface{}, p } } wantDump := cfg.FailureDump.enabled() && cfg.FailureDump.Filename != "" - prg, ast, err := newProgram(ctx, cfg.Program, root, getEnv(cfg.AllowedEnvironment), client, limiter, auth, patterns, cfg.XSDs, log, trace, wantDump) + doCov := cfg.RecordCoverage && log.IsDebug() + prg, ast, cov, err := newProgram(ctx, cfg.Program, root, getEnv(cfg.AllowedEnvironment), client, limiter, auth, patterns, cfg.XSDs, log, trace, wantDump, doCov) if err != nil { return err } @@ -228,6 +229,14 @@ func (i input) run(env v2.Context, src *source, cursor map[string]interface{}, p ) // Keep track of whether CEL is degraded for this periodic run. var isDegraded bool + if doCov { + defer func() { + // If doCov is true, log the updated coverage details. + // Updates are a running aggregate for each call to run + // as cov is shared via the program compilation. + log.Debugw("coverage", "details", cov.Details()) + }() + } for { if wait := time.Until(waitUntil); wait > 0 { // We have a special-case wait for when we have a zero limit. @@ -1039,10 +1048,10 @@ func getEnv(allowed []string) map[string]string { return env } -func newProgram(ctx context.Context, src, root string, vars map[string]string, client *http.Client, limiter *rate.Limiter, auth *lib.BasicAuth, patterns map[string]*regexp.Regexp, xsd map[string]string, log *logp.Logger, trace *httplog.LoggingRoundTripper, details bool) (cel.Program, *cel.Ast, error) { +func newProgram(ctx context.Context, src, root string, vars map[string]string, client *http.Client, limiter *rate.Limiter, auth *lib.BasicAuth, patterns map[string]*regexp.Regexp, xsd map[string]string, log *logp.Logger, trace *httplog.LoggingRoundTripper, details, coverage bool) (cel.Program, *cel.Ast, *lib.Coverage, error) { xml, err := lib.XML(nil, xsd) if err != nil { - return nil, nil, fmt.Errorf("failed to build xml type hints: %w", err) + return nil, nil, nil, fmt.Errorf("failed to build xml type hints: %w", err) } opts := []cel.EnvOption{ cel.Declarations(decls.NewVar(root, decls.Dyn)), @@ -1070,23 +1079,30 @@ func newProgram(ctx context.Context, src, root string, vars map[string]string, c } env, err := cel.NewEnv(opts...) if err != nil { - return nil, nil, fmt.Errorf("failed to create env: %w", err) + return nil, nil, nil, fmt.Errorf("failed to create env: %w", err) } ast, iss := env.Compile(src) if iss.Err() != nil { - return nil, nil, fmt.Errorf("failed compilation: %w", iss.Err()) + return nil, nil, nil, fmt.Errorf("failed compilation: %w", iss.Err()) } - var progOpts []cel.ProgramOption + var ( + progOpts []cel.ProgramOption + cov *lib.Coverage + ) + if coverage { + cov = lib.NewCoverage(ast) + progOpts = []cel.ProgramOption{cov.ProgramOption()} + } if details { progOpts = []cel.ProgramOption{cel.EvalOptions(cel.OptTrackState)} } prg, err := env.Program(ast, progOpts...) if err != nil { - return nil, nil, fmt.Errorf("failed program instantiation: %w", err) + return nil, nil, nil, fmt.Errorf("failed program instantiation: %w", err) } - return prg, ast, nil + return prg, ast, cov, nil } func debug(log *logp.Logger, trace *httplog.LoggingRoundTripper) func(string, any) { diff --git a/x-pack/filebeat/input/cel/input_test.go b/x-pack/filebeat/input/cel/input_test.go index 143402d9834..7ccfb9ccbee 100644 --- a/x-pack/filebeat/input/cel/input_test.go +++ b/x-pack/filebeat/input/cel/input_test.go @@ -1769,6 +1769,49 @@ var inputTests = []struct { }, }, + // Coverage + { + name: "coverage", + config: map[string]interface{}{ + "interval": 1, + "program": `int(state.n).as(n, { + "events": [{"n": n+1}], + "n": n+1, + "want_more": n+1 < 5, + "probe": n < 2 ? + "little" + : + "big", + "fail_probe": n < 0 ? + "negative" + : + "non-negative", + })`, + "record_coverage": true, + "state": map[string]any{"n": 0}, + "resource": map[string]interface{}{ + "url": "", + }, + }, + time: func() time.Time { return time.Date(2010, 2, 9, 0, 0, 0, 0, time.UTC) }, + // The program will be evaluated five times in the first periodic + // run and then once for all subsequent runs. We depend here on + // the test construction that asks that we get at least as many + // results from the input as there are elements in the want slice + // and then stop. + want: []map[string]interface{}{ + // First periodic run. + {"n": float64(1)}, + {"n": float64(2)}, + {"n": float64(3)}, + {"n": float64(4)}, + {"n": float64(5)}, + // Second and subsequent periodic runs. + {"n": float64(6)}, + {"n": float64(7)}, + }, + }, + // not yet done from httpjson (some are redundant since they are compositional products). // // cursor/pagination (place above auth test block) diff --git a/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker.go b/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker.go index b18229b7795..71abb10d137 100644 --- a/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker.go +++ b/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker.go @@ -6,10 +6,10 @@ package kvstore import ( "context" + "sync/atomic" "github.com/elastic/beats/v7/libbeat/beat" "github.com/elastic/beats/v7/libbeat/common/acker" - "github.com/elastic/beats/v7/libbeat/common/atomic" ) // TxTracker implements a transaction tracker. Individual beat events which @@ -17,20 +17,20 @@ import ( // ACK-ed, the pending count is decremented (see NewTxACKHandler). Calling Wait // will block until all events are ACK-ed or the parent context is cancelled. type TxTracker struct { - pending atomic.Int + pending atomic.Int64 ctx context.Context cancel context.CancelFunc } // Add increments the pending count. func (t *TxTracker) Add() { - t.pending.Inc() + t.pending.Add(1) } // Ack decrements the pending count. If pending goes to zero, then the // context on TxTracker is cancelled. func (t *TxTracker) Ack() { - if t.pending.Dec() == 0 { + if t.pending.Add(-1) == 0 { t.cancel() } } diff --git a/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker_test.go b/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker_test.go index 7b452e21ae8..7741fa1e595 100644 --- a/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker_test.go +++ b/x-pack/filebeat/input/entityanalytics/internal/kvstore/tracker_test.go @@ -15,7 +15,7 @@ import ( func TestTxTracker_Ack(t *testing.T) { txTracker := NewTxTracker(context.Background()) - txTracker.pending.Inc() + txTracker.pending.Add(1) txTracker.Ack() @@ -25,9 +25,9 @@ func TestTxTracker_Ack(t *testing.T) { func TestTxTracker_Add(t *testing.T) { txTracker := NewTxTracker(context.Background()) - require.Equal(t, 0, txTracker.pending.Load()) + require.Equal(t, int64(0), txTracker.pending.Load()) txTracker.Add() - require.Equal(t, 1, txTracker.pending.Load()) + require.Equal(t, int64(1), txTracker.pending.Load()) } func TestTxTracker_Wait(t *testing.T) { @@ -43,7 +43,7 @@ func TestTxACKHandler(t *testing.T) { handler := NewTxACKHandler() txTracker.Add() - require.Equal(t, 1, txTracker.pending.Load()) + require.Equal(t, int64(1), txTracker.pending.Load()) handler.AddEvent(beat.Event{ Private: txTracker, @@ -63,7 +63,7 @@ func TestTxACKHandler(t *testing.T) { handler := NewTxACKHandler() txTracker.Add() - require.Equal(t, 1, txTracker.pending.Load()) + require.Equal(t, int64(1), txTracker.pending.Load()) handler.AddEvent(beat.Event{ Private: txTracker, @@ -71,6 +71,6 @@ func TestTxACKHandler(t *testing.T) { txTracker.Wait() - require.Equal(t, 1, txTracker.pending.Load()) + require.Equal(t, int64(1), txTracker.pending.Load()) }) } diff --git a/x-pack/filebeat/input/entityanalytics/internal/kvstore/transaction.go b/x-pack/filebeat/input/entityanalytics/internal/kvstore/transaction.go index 7f32429a4e9..c62155d27fb 100644 --- a/x-pack/filebeat/input/entityanalytics/internal/kvstore/transaction.go +++ b/x-pack/filebeat/input/entityanalytics/internal/kvstore/transaction.go @@ -8,10 +8,9 @@ import ( "encoding/json" "errors" "fmt" + "sync/atomic" "go.etcd.io/bbolt" - - "github.com/elastic/beats/v7/libbeat/common/atomic" ) var ( @@ -126,7 +125,7 @@ func (t *Transaction) Delete(bucket, key []byte) error { // Commit will write any changes to disk. For read-only transactions, calling // Commit will route to Rollback. func (t *Transaction) Commit() error { - if !t.closed.CAS(false, true) { + if !t.closed.CompareAndSwap(false, true) { return nil } if !t.writeable { @@ -138,7 +137,7 @@ func (t *Transaction) Commit() error { // Rollback closes the transaction. For write transactions, all updates made // within the transaction will be discarded. func (t *Transaction) Rollback() error { - if !t.closed.CAS(false, true) { + if !t.closed.CompareAndSwap(false, true) { return nil } return t.tx.Rollback() diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go index 3d8bdae11c9..42ffc060178 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta.go @@ -14,13 +14,9 @@ import ( "io" "net/http" "net/url" - "path" - "strconv" "strings" "time" - "golang.org/x/time/rate" - "github.com/elastic/elastic-agent-libs/logp" ) @@ -195,16 +191,23 @@ func (o Response) String() string { // https://${yourOktaDomain}/reports/rate-limit. // // See https://developer.okta.com/docs/reference/api/users/#list-users for details. -func GetUserDetails(ctx context.Context, cli *http.Client, host, key, user string, query url.Values, omit Response, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]User, http.Header, error) { - const endpoint = "/api/v1/users" +func GetUserDetails(ctx context.Context, cli *http.Client, host, key, user string, query url.Values, omit Response, lim RateLimiter, window time.Duration, log *logp.Logger) ([]User, http.Header, error) { + var endpoint, path string + if user == "" { + endpoint = "/api/v1/users" + path = endpoint + } else { + endpoint = "/api/v1/users/{user}" + path = strings.Replace(endpoint, "{user}", user, 1) + } u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, user), + Path: path, RawQuery: query.Encode(), } - return getDetails[User](ctx, cli, u, key, user == "", omit, lim, window, log) + return getDetails[User](ctx, cli, u, endpoint, key, user == "", omit, lim, window, log) } // GetUserFactors returns Okta group roles using the groups API endpoint. host is the @@ -213,19 +216,20 @@ func GetUserDetails(ctx context.Context, cli *http.Client, host, key, user strin // See GetUserDetails for details of the query and rate limit parameters. // // See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/listFactors. -func GetUserFactors(ctx context.Context, cli *http.Client, host, key, user string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Factor, http.Header, error) { - const endpoint = "/api/v1/users" - +func GetUserFactors(ctx context.Context, cli *http.Client, host, key, user string, lim RateLimiter, window time.Duration, log *logp.Logger) ([]Factor, http.Header, error) { if user == "" { return nil, nil, errors.New("no user specified") } + const endpoint = "/api/v1/users/{user}/factors" + path := strings.Replace(endpoint, "{user}", user, 1) + u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, user, "factors"), + Path: path, } - return getDetails[Factor](ctx, cli, u, key, true, OmitNone, lim, window, log) + return getDetails[Factor](ctx, cli, u, endpoint, key, true, OmitNone, lim, window, log) } // GetUserRoles returns Okta group roles using the groups API endpoint. host is the @@ -234,19 +238,20 @@ func GetUserFactors(ctx context.Context, cli *http.Client, host, key, user strin // See GetUserDetails for details of the query and rate limit parameters. // // See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/RoleAssignmentBGroup/#tag/RoleAssignmentBGroup/operation/listGroupAssignedRoles. -func GetUserRoles(ctx context.Context, cli *http.Client, host, key, user string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Role, http.Header, error) { - const endpoint = "/api/v1/users" - +func GetUserRoles(ctx context.Context, cli *http.Client, host, key, user string, lim RateLimiter, window time.Duration, log *logp.Logger) ([]Role, http.Header, error) { if user == "" { return nil, nil, errors.New("no user specified") } + const endpoint = "/api/v1/users/{user}/roles" + path := strings.Replace(endpoint, "{user}", user, 1) + u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, user, "roles"), + Path: path, } - return getDetails[Role](ctx, cli, u, key, true, OmitNone, lim, window, log) + return getDetails[Role](ctx, cli, u, endpoint, key, true, OmitNone, lim, window, log) } // GetUserGroupDetails returns Okta group details using the users API endpoint. host is the @@ -255,19 +260,20 @@ func GetUserRoles(ctx context.Context, cli *http.Client, host, key, user string, // See GetUserDetails for details of the query and rate limit parameters. // // See https://developer.okta.com/docs/reference/api/users/#request-parameters-8 (no anchor exists on the page for this endpoint) for details. -func GetUserGroupDetails(ctx context.Context, cli *http.Client, host, key, user string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Group, http.Header, error) { - const endpoint = "/api/v1/users" - +func GetUserGroupDetails(ctx context.Context, cli *http.Client, host, key, user string, lim RateLimiter, window time.Duration, log *logp.Logger) ([]Group, http.Header, error) { if user == "" { return nil, nil, errors.New("no user specified") } + const endpoint = "/api/v1/users/{user}/groups" + path := strings.Replace(endpoint, "{user}", user, 1) + u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, user, "groups"), + Path: path, } - return getDetails[Group](ctx, cli, u, key, true, OmitNone, lim, window, log) + return getDetails[Group](ctx, cli, u, endpoint, key, true, OmitNone, lim, window, log) } // GetGroupRoles returns Okta group roles using the groups API endpoint. host is the @@ -276,19 +282,20 @@ func GetUserGroupDetails(ctx context.Context, cli *http.Client, host, key, user // See GetUserDetails for details of the query and rate limit parameters. // // See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/RoleAssignmentBGroup/#tag/RoleAssignmentBGroup/operation/listGroupAssignedRoles. -func GetGroupRoles(ctx context.Context, cli *http.Client, host, key, group string, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Role, http.Header, error) { - const endpoint = "/api/v1/groups" - +func GetGroupRoles(ctx context.Context, cli *http.Client, host, key, group string, lim RateLimiter, window time.Duration, log *logp.Logger) ([]Role, http.Header, error) { if group == "" { return nil, nil, errors.New("no group specified") } + const endpoint = "/api/v1/groups/{group}/rules" + path := strings.Replace(endpoint, "{group}", group, 1) + u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, group, "roles"), + Path: path, } - return getDetails[Role](ctx, cli, u, key, true, OmitNone, lim, window, log) + return getDetails[Role](ctx, cli, u, endpoint, key, true, OmitNone, lim, window, log) } // GetDeviceDetails returns Okta device details using the list devices API endpoint. host is the @@ -298,16 +305,24 @@ func GetGroupRoles(ctx context.Context, cli *http.Client, host, key, group strin // See GetUserDetails for details of the query and rate limit parameters. // // See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Device/#tag/Device/operation/listDevices for details. -func GetDeviceDetails(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]Device, http.Header, error) { - const endpoint = "/api/v1/devices" +func GetDeviceDetails(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, lim RateLimiter, window time.Duration, log *logp.Logger) ([]Device, http.Header, error) { + var endpoint string + var path string + if device == "" { + endpoint = "/api/v1/devices" + path = endpoint + } else { + endpoint = "/api/v1/devices/{device}" + path = strings.Replace(endpoint, "{device}", device, 1) + } u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, device), + Path: path, RawQuery: query.Encode(), } - return getDetails[Device](ctx, cli, u, key, device == "", OmitNone, lim, window, log) + return getDetails[Device](ctx, cli, u, endpoint, key, device == "", OmitNone, lim, window, log) } // GetDeviceUsers returns Okta user details for users associated with the provided device identifier @@ -317,21 +332,22 @@ func GetDeviceDetails(ctx context.Context, cli *http.Client, host, key, device s // See GetUserDetails for details of the query and rate limit parameters. // // See https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Device/#tag/Device/operation/listDeviceUsers for details. -func GetDeviceUsers(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, omit Response, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]User, http.Header, error) { +func GetDeviceUsers(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, omit Response, lim RateLimiter, window time.Duration, log *logp.Logger) ([]User, http.Header, error) { if device == "" { // No user associated with a null device. Not an error. return nil, nil, nil } - const endpoint = "/api/v1/devices" + const endpoint = "/api/v1/devices/{device}/users" + path := strings.Replace(endpoint, "{device}", device, 1) u := &url.URL{ Scheme: "https", Host: host, - Path: path.Join(endpoint, device, "users"), + Path: path, RawQuery: query.Encode(), } - du, h, err := getDetails[devUser](ctx, cli, u, key, true, omit, lim, window, log) + du, h, err := getDetails[devUser](ctx, cli, u, endpoint, key, true, omit, lim, window, log) if err != nil { return nil, h, err } @@ -356,7 +372,7 @@ type devUser struct { // for the specific user are returned, otherwise a list of all users is returned. // // See GetUserDetails for details of the query and rate limit parameters. -func getDetails[E entity](ctx context.Context, cli *http.Client, u *url.URL, key string, all bool, omit Response, lim *rate.Limiter, window time.Duration, log *logp.Logger) ([]E, http.Header, error) { +func getDetails[E entity](ctx context.Context, cli *http.Client, u *url.URL, endpoint string, key string, all bool, omit Response, lim RateLimiter, window time.Duration, log *logp.Logger) ([]E, http.Header, error) { url := u.String() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { @@ -370,8 +386,7 @@ func getDetails[E entity](ctx context.Context, cli *http.Client, u *url.URL, key req.Header.Set("Content-Type", contentType) req.Header.Set("Authorization", fmt.Sprintf("SSWS %s", key)) - log.Debugw("rate limit", "limit", lim.Limit(), "burst", lim.Burst(), "url", url) - err = lim.Wait(ctx) + err = lim.Wait(ctx, endpoint, u, log) if err != nil { return nil, nil, err } @@ -380,7 +395,7 @@ func getDetails[E entity](ctx context.Context, cli *http.Client, u *url.URL, key return nil, nil, err } defer resp.Body.Close() - err = oktaRateLimit(resp.Header, window, lim, log) + err = lim.Update(endpoint, resp.Header, window, log) if err != nil { io.Copy(io.Discard, resp.Body) return nil, nil, err @@ -443,59 +458,6 @@ func (e *Error) Error() string { return fmt.Sprintf("%s: %s", summary, strings.Join(causes, ",")) } -// oktaRateLimit implements the Okta rate limit policy translation. -// -// See https://developer.okta.com/docs/reference/rl-best-practices/ for details. -func oktaRateLimit(h http.Header, window time.Duration, limiter *rate.Limiter, log *logp.Logger) error { - limit := h.Get("X-Rate-Limit-Limit") - remaining := h.Get("X-Rate-Limit-Remaining") - reset := h.Get("X-Rate-Limit-Reset") - log.Debugw("rate limit header", "X-Rate-Limit-Limit", limit, "X-Rate-Limit-Remaining", remaining, "X-Rate-Limit-Reset", reset) - if limit == "" || remaining == "" || reset == "" { - return nil - } - - lim, err := strconv.ParseFloat(limit, 64) - if err != nil { - return err - } - rem, err := strconv.ParseFloat(remaining, 64) - if err != nil { - return err - } - rst, err := strconv.ParseInt(reset, 10, 64) - if err != nil { - return err - } - resetTime := time.Unix(rst, 0) - per := time.Until(resetTime).Seconds() - - // Be conservative here; the docs don't exactly specify burst rates. - // Make sure we can make at least one new request, even if we fail - // to get a non-zero rate.Limit. We could set to zero for the case - // that limit=rate.Inf, but that detail is not important. - burst := 1 - - rateLimit := rate.Limit(rem / per) - - // Process reset if we need to wait until reset to avoid a request against a zero quota. - if rateLimit <= 0 { - waitUntil := resetTime.UTC() - // next gives us a sane next window estimate, but the - // estimate will be overwritten when we make the next - // permissible API request. - next := rate.Limit(lim / window.Seconds()) - limiter.SetLimitAt(waitUntil, next) - limiter.SetBurstAt(waitUntil, burst) - log.Debugw("rate limit adjust", "reset_time", waitUntil, "next_rate", next, "next_burst", burst) - return nil - } - limiter.SetLimit(rateLimit) - limiter.SetBurst(burst) - log.Debugw("rate limit adjust", "set_rate", rateLimit, "set_burst", burst) - return nil -} - // Next returns the next URL query for a pagination sequence. If no further // page is available, Next returns io.EOF. func Next(h http.Header) (query url.Values, err error) { diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go index 9b04d3996bf..45b1b2a4ca4 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/okta_test.go @@ -44,8 +44,8 @@ func Test(t *testing.T) { t.Skip("okta tests require ${OKTA_TOKEN} to be set") } - // Make a global limiter with the capacity to proceed once. - limiter := rate.NewLimiter(1, 1) + // Make a global limiter + limiter := NewRateLimiter() // There are a variety of windows, the most conservative is one minute. // The rate limit will be adjusted on the second call to the API if @@ -263,14 +263,14 @@ var localTests = []struct { name string msg string id string - fn func(ctx context.Context, cli *http.Client, host, key, user string, query url.Values, lim *rate.Limiter, window time.Duration, log *logp.Logger) (any, http.Header, error) + fn func(ctx context.Context, cli *http.Client, host, key, user string, query url.Values, lim RateLimiter, window time.Duration, log *logp.Logger) (any, http.Header, error) mkWant func(string) (any, error) }{ { // Test case constructed from API-returned value with details anonymised. name: "users", msg: `[{"id":"userid","status":"STATUS","created":"2023-05-14T13:37:20.000Z","activated":null,"statusChanged":"2023-05-15T01:50:30.000Z","lastLogin":"2023-05-15T01:59:20.000Z","lastUpdated":"2023-05-15T01:50:32.000Z","passwordChanged":"2023-05-15T01:50:32.000Z","recovery_question":{"question":"Who's a major player in the cowboy scene?","answer":"Annie Oakley"},"type":{"id":"typeid"},"profile":{"firstName":"name","lastName":"surname","mobilePhone":null,"secondEmail":null,"login":"name.surname@example.com","email":"name.surname@example.com"},"credentials":{"password":{"value":"secret"},"emails":[{"value":"name.surname@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://localhost/api/v1/users/userid"}}}]`, - fn: func(ctx context.Context, cli *http.Client, host, key, user string, query url.Values, lim *rate.Limiter, window time.Duration, log *logp.Logger) (any, http.Header, error) { + fn: func(ctx context.Context, cli *http.Client, host, key, user string, query url.Values, lim RateLimiter, window time.Duration, log *logp.Logger) (any, http.Header, error) { return GetUserDetails(context.Background(), cli, host, key, user, query, OmitNone, lim, window, log) }, mkWant: mkWant[User], @@ -279,7 +279,7 @@ var localTests = []struct { // Test case from https://developer.okta.com/docs/api/openapi/okta-management/management/tag/Device/#tag/Device/operation/listDevices name: "devices", msg: `[{"id":"devid","status":"CREATED","created":"2019-10-02T18:03:07.000Z","lastUpdated":"2019-10-02T18:03:07.000Z","profile":{"displayName":"Example Device name 1","platform":"WINDOWS","serialNumber":"XXDDRFCFRGF3M8MD6D","sid":"S-1-11-111","registered":true,"secureHardwarePresent":false,"diskEncryptionType":"ALL_INTERNAL_VOLUMES"},"resourceType":"UDDevice","resourceDisplayName":{"value":"Example Device name 1","sensitive":false},"resourceAlternateId":null,"resourceId":"guo4a5u7YAHhjXrMK0g4","_links":{"activate":{"href":"https://{yourOktaDomain}/api/v1/devices/guo4a5u7YAHhjXrMK0g4/lifecycle/activate","hints":{"allow":["POST"]}},"self":{"href":"https://{yourOktaDomain}/api/v1/devices/guo4a5u7YAHhjXrMK0g4","hints":{"allow":["GET","PATCH","PUT"]}},"users":{"href":"https://{yourOktaDomain}/api/v1/devices/guo4a5u7YAHhjXrMK0g4/users","hints":{"allow":["GET"]}}}},{"id":"guo4a5u7YAHhjXrMK0g5","status":"ACTIVE","created":"2023-06-21T23:24:02.000Z","lastUpdated":"2023-06-21T23:24:02.000Z","profile":{"displayName":"Example Device name 2","platform":"ANDROID","manufacturer":"Google","model":"Pixel 6","osVersion":"13:2023-05-05","registered":true,"secureHardwarePresent":true,"diskEncryptionType":"USER"},"resourceType":"UDDevice","resourceDisplayName":{"value":"Example Device name 2","sensitive":false},"resourceAlternateId":null,"resourceId":"guo4a5u7YAHhjXrMK0g5","_links":{"activate":{"href":"https://{yourOktaDomain}/api/v1/devices/guo4a5u7YAHhjXrMK0g5/lifecycle/activate","hints":{"allow":["POST"]}},"self":{"href":"https://{yourOktaDomain}/api/v1/devices/guo4a5u7YAHhjXrMK0g5","hints":{"allow":["GET","PATCH","PUT"]}},"users":{"href":"https://{yourOktaDomain}/api/v1/devices/guo4a5u7YAHhjXrMK0g5/users","hints":{"allow":["GET"]}}}}]`, - fn: func(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, lim *rate.Limiter, window time.Duration, log *logp.Logger) (any, http.Header, error) { + fn: func(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, lim RateLimiter, window time.Duration, log *logp.Logger) (any, http.Header, error) { return GetDeviceDetails(context.Background(), cli, host, key, device, query, lim, window, log) }, mkWant: mkWant[Device], @@ -289,7 +289,7 @@ var localTests = []struct { name: "devices_users", msg: `[{"created":"2023-08-07T21:48:27.000Z","managementStatus":"NOT_MANAGED","user":{"id":"userid","status":"STATUS","created":"2023-05-14T13:37:20.000Z","activated":null,"statusChanged":"2023-05-15T01:50:30.000Z","lastLogin":"2023-05-15T01:59:20.000Z","lastUpdated":"2023-05-15T01:50:32.000Z","passwordChanged":"2023-05-15T01:50:32.000Z","type":{"id":"typeid"},"profile":{"firstName":"name","lastName":"surname","mobilePhone":null,"secondEmail":null,"login":"name.surname@example.com","email":"name.surname@example.com"},"credentials":{"password":{"value":"secret"},"recovery_question":{"question":"Who's a major player in the cowboy scene?","answer":"Annie Oakley"},"emails":[{"value":"name.surname@example.com","status":"VERIFIED","type":"PRIMARY"}],"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"self":{"href":"https://localhost/api/v1/users/userid"}}}}]`, id: "devid", - fn: func(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, lim *rate.Limiter, window time.Duration, log *logp.Logger) (any, http.Header, error) { + fn: func(ctx context.Context, cli *http.Client, host, key, device string, query url.Values, lim RateLimiter, window time.Duration, log *logp.Logger) (any, http.Header, error) { return GetDeviceUsers(context.Background(), cli, host, key, device, query, OmitNone, lim, window, log) }, mkWant: mkWant[devUser], @@ -315,9 +315,7 @@ func TestLocal(t *testing.T) { for _, test := range localTests { t.Run(test.name, func(t *testing.T) { - // Make a global limiter with more capacity than will be set by the mock API. - // This will show the burst drop. - limiter := rate.NewLimiter(10, 10) + limiter := NewRateLimiter() // There are a variety of windows, the most conservative is one minute. // The rate limit will be adjusted on the second call to the API if @@ -377,12 +375,23 @@ func TestLocal(t *testing.T) { t.Errorf("unexpected result:\n- want\n+ got\n%s", cmp.Diff(want, got)) } - lim := limiter.Limit() - if lim < 49.0/60.0 || 50.0/60.0 < lim { - t.Errorf("unexpected rate limit (outside [49/60, 50/60]: %f", lim) + if len(limiter) != 1 { + t.Errorf("unexpected number endpoints track by rate limiter: %d", len(limiter)) } - if limiter.Burst() != 1 { // Set in GetUserDetails. - t.Errorf("unexpected burst: got:%d want:1", limiter.Burst()) + // retrieve the rate.Limiter parameters for the one endpoint + var limit rate.Limit + var burst int + for _, l := range limiter { + limit = l.Limit() + burst = l.Burst() + break + } + + if limit < 49.0/60.0 || 50.0/60.0 < limit { + t.Errorf("unexpected rate limit (outside [49/60, 50/60]: %f", limit) + } + if burst != 1 { + t.Errorf("unexpected burst: got:%d want:1", burst) } next, err := Next(h) diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/ratelimiter.go b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/ratelimiter.go new file mode 100644 index 00000000000..1b58e01328c --- /dev/null +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/ratelimiter.go @@ -0,0 +1,97 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package okta + +import ( + "context" + "net/http" + "net/url" + "strconv" + "time" + + "golang.org/x/time/rate" + + "github.com/elastic/elastic-agent-libs/logp" +) + +type RateLimiter map[string]*rate.Limiter + +func NewRateLimiter() RateLimiter { + r := make(RateLimiter) + return r +} + +func (r RateLimiter) limiter(path string) *rate.Limiter { + if existing, ok := r[path]; ok { + return existing + } + initial := rate.NewLimiter(1, 1) // Allow a single fetch operation to obtain limits from the API + r[path] = initial + return initial +} + +func (r RateLimiter) Wait(ctx context.Context, endpoint string, url *url.URL, log *logp.Logger) (err error) { + limiter := r.limiter(endpoint) + log.Debugw("rate limit", "limit", limiter.Limit(), "burst", limiter.Burst(), "url", url.String()) + return limiter.Wait(ctx) +} + +// Update implements the Okta rate limit policy translation. +// +// See https://developer.okta.com/docs/reference/rl-best-practices/ for details. +func (r RateLimiter) Update(endpoint string, h http.Header, window time.Duration, log *logp.Logger) error { + limiter := r.limiter(endpoint) + limit := h.Get("X-Rate-Limit-Limit") + remaining := h.Get("X-Rate-Limit-Remaining") + reset := h.Get("X-Rate-Limit-Reset") + log.Debugw("rate limit header", "X-Rate-Limit-Limit", limit, "X-Rate-Limit-Remaining", remaining, "X-Rate-Limit-Reset", reset) + if limit == "" || remaining == "" || reset == "" { + return nil + } + + lim, err := strconv.ParseFloat(limit, 64) + if err != nil { + return err + } + rem, err := strconv.ParseFloat(remaining, 64) + if err != nil { + return err + } + rst, err := strconv.ParseInt(reset, 10, 64) + if err != nil { + return err + } + resetTime := time.Unix(rst, 0) + per := time.Until(resetTime).Seconds() + + // Be conservative here; the docs don't exactly specify burst rates. + // Make sure we can make at least one new request, even if we fail + // to get a non-zero rate.Limit. We could set to zero for the case + // that limit=rate.Inf, but that detail is not important. + burst := 1 + + rateLimit := rate.Limit(rem / per) + + // Process reset if we need to wait until reset to avoid a request against a zero quota. + if rateLimit <= 0 { + // Reset limiter to block requests until reset + limiter := rate.NewLimiter(0, 0) + r[endpoint] = limiter + + // next gives us a sane next window estimate, but the + // estimate will be overwritten when we make the next + // permissible API request. + next := rate.Limit(lim / window.Seconds()) + waitUntil := resetTime.UTC() + limiter.SetLimitAt(waitUntil, next) + limiter.SetBurstAt(waitUntil, burst) + log.Debugw("rate limit reset", "reset_time", waitUntil, "next_rate", next, "next_burst", burst) + return nil + } + limiter.SetLimit(rateLimit) + limiter.SetBurst(burst) + log.Debugw("rate limit adjust", "set_rate", rateLimit, "set_burst", burst) + return nil +} diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/ratelimiter_test.go b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/ratelimiter_test.go new file mode 100644 index 00000000000..1492e55c8a6 --- /dev/null +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta/ratelimiter_test.go @@ -0,0 +1,84 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package okta + +import ( + "net/http" + "strconv" + "testing" + "time" + + "github.com/elastic/elastic-agent-libs/logp" +) + +func TestRateLimiter(t *testing.T) { + logp.TestingSetup() + + t.Run("separation by endpoint", func(t *testing.T) { + r := NewRateLimiter() + limiter1 := r.limiter("/foo") + limiter2 := r.limiter("/bar") + + limiter1.SetBurst(1000) + + if limiter2.Burst() == 1000 { + t.Errorf("changes to one endpoint's limits affected another") + } + }) + + t.Run("Update stops requests when none are remaining", func(t *testing.T) { + r := NewRateLimiter() + + const endpoint = "/foo" + limiter := r.limiter(endpoint) + + if !limiter.Allow() { + t.Errorf("doesn't allow an initial request") + } + + now := time.Now().Unix() + reset := now + 30 + + headers := http.Header{ + "X-Rate-Limit-Limit": []string{"60"}, + "X-Rate-Limit-Remaining": []string{"0"}, + "X-Rate-Limit-Reset": []string{strconv.FormatInt(reset, 10)}, + } + window := time.Minute + + err := r.Update(endpoint, headers, window, logp.L()) + if err != nil { + t.Errorf("unexpected error from Update(): %v", err) + } + limiter = r.limiter(endpoint) + + if limiter.Allow() { + t.Errorf("allowed a request when none are remaining") + } + + if limiter.AllowN(time.Unix(reset-1, 999999999), 1) { + t.Errorf("allowed a request before reset, when none are remaining") + } + + if !limiter.AllowN(time.Unix(reset+1, 0), 1) { + t.Errorf("doesn't allow requests to resume after reset") + } + + if limiter.Limit() != 1.0 { + t.Errorf("unexpected rate following reset (not 60 requests / 60 seconds): %f", limiter.Limit()) + } + + if limiter.Burst() != 1 { + t.Errorf("unexpected burst following reset (not 1): %d", limiter.Burst()) + } + + limiter.SetBurstAt(time.Unix(reset, 0), 100) // increase bucket size to check token accumulation + tokens := limiter.TokensAt(time.Unix(reset+30, 0)) + if tokens < 29.5 || tokens > 30.0 { + t.Errorf("tokens don't accumulate at the expected rate. tokens 30s after reset: %f", tokens) + } + + }) +} diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/okta.go b/x-pack/filebeat/input/entityanalytics/provider/okta/okta.go index 5d68cf3f5c4..30103d3ccdb 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/okta/okta.go +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/okta.go @@ -23,7 +23,6 @@ import ( "go.elastic.co/ecszap" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "golang.org/x/time/rate" v2 "github.com/elastic/beats/v7/filebeat/input/v2" "github.com/elastic/beats/v7/libbeat/beat" @@ -60,7 +59,7 @@ type oktaInput struct { cfg conf client *http.Client - lim *rate.Limiter + lim okta.RateLimiter metrics *inputMetrics logger *logp.Logger @@ -111,7 +110,7 @@ func (p *oktaInput) Run(inputCtx v2.Context, store *kvstore.Store, client beat.C updateTimer := time.NewTimer(updateWaitTime) // Allow a single fetch operation to obtain limits from the API. - p.lim = rate.NewLimiter(1, 1) + p.lim = okta.NewRateLimiter() if p.cfg.Tracer != nil { id := sanitizeFileName(inputCtx.IDWithoutName) diff --git a/x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go b/x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go index 5752370c4ce..e7e2bffbba2 100644 --- a/x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go +++ b/x-pack/filebeat/input/entityanalytics/provider/okta/okta_test.go @@ -18,7 +18,6 @@ import ( "testing" "time" - "golang.org/x/time/rate" "gopkg.in/natefinch/lumberjack.v2" "github.com/elastic/beats/v7/x-pack/filebeat/input/entityanalytics/provider/okta/internal/okta" @@ -177,6 +176,7 @@ func TestOktaDoFetch(t *testing.T) { if err != nil { t.Errorf("failed to parse server URL: %v", err) } + rateLimiter := okta.NewRateLimiter() a := oktaInput{ cfg: conf{ OktaDomain: u.Host, @@ -185,7 +185,7 @@ func TestOktaDoFetch(t *testing.T) { EnrichWith: test.enrichWith, }, client: ts.Client(), - lim: rate.NewLimiter(1, 1), + lim: rateLimiter, logger: logp.L(), } if *trace { diff --git a/x-pack/filebeat/input/gcppubsub/pubsub_test.go b/x-pack/filebeat/input/gcppubsub/pubsub_test.go index 7981a3ee772..16fdbf1ebbd 100644 --- a/x-pack/filebeat/input/gcppubsub/pubsub_test.go +++ b/x-pack/filebeat/input/gcppubsub/pubsub_test.go @@ -12,6 +12,7 @@ import ( "os" "strconv" "sync" + "sync/atomic" "testing" "time" @@ -23,7 +24,6 @@ import ( "github.com/elastic/beats/v7/filebeat/channel" "github.com/elastic/beats/v7/filebeat/input" "github.com/elastic/beats/v7/libbeat/beat" - "github.com/elastic/beats/v7/libbeat/common/atomic" "github.com/elastic/beats/v7/libbeat/tests/compose" "github.com/elastic/beats/v7/libbeat/tests/resources" conf "github.com/elastic/elastic-agent-libs/config" @@ -247,6 +247,7 @@ func runTestWithACKer(t *testing.T, cfg *conf.C, onEvent eventHandler, run func( if err != nil { t.Fatal(err) } + //nolint:errcheck // ignore pubsubInput := in.(*pubsubInput) defer pubsubInput.Stop() @@ -421,13 +422,14 @@ func TestRunStop(t *testing.T) { func TestEndToEndACK(t *testing.T) { cfg := defaultTestConfig() - var count atomic.Int + var count atomic.Int64 seen := make(map[string]struct{}) // ACK every other message halfAcker := func(ev beat.Event, clientConfig beat.ClientConfig) bool { + //nolint:errcheck // ignore msg := ev.Private.(*pubsub.Message) seen[msg.ID] = struct{}{} - if count.Inc()&1 != 0 { + if count.Add(1)&1 != 0 { // Nack will result in the Message being redelivered more quickly than if it were allowed to expire. msg.Nack() return false @@ -453,6 +455,7 @@ func TestEndToEndACK(t *testing.T) { assert.Len(t, events, len(seen)) got := make(map[string]struct{}) for _, ev := range events { + //nolint:errcheck // ignore msg := ev.Private.(*pubsub.Message) got[msg.ID] = struct{}{} } diff --git a/x-pack/filebeat/input/gcs/config.go b/x-pack/filebeat/input/gcs/config.go index 64f64c69bc5..9a9f6dceadd 100644 --- a/x-pack/filebeat/input/gcs/config.go +++ b/x-pack/filebeat/input/gcs/config.go @@ -50,6 +50,8 @@ type config struct { ExpandEventListFromField string `config:"expand_event_list_from_field"` // This field is only used for system test purposes, to override the HTTP endpoint. AlternativeHost string `config:"alternative_host"` + // Retry - Defines the retry configuration for the input. + Retry retryConfig `config:"retry"` } // bucket contains the config for each specific object storage bucket in the root account @@ -92,6 +94,19 @@ type jsonCredentialsConfig struct { AccountKey string `config:"account_key"` } +type retryConfig struct { + // MaxAttempts configures the maximum number of times an API call can be made in the case of retryable errors. + // For example, if you set MaxAttempts(5), the operation will be attempted up to 5 times total (initial call plus 4 retries). + // If you set MaxAttempts(1), the operation will be attempted only once and there will be no retries. This setting defaults to 3. + MaxAttempts int `config:"max_attempts" validate:"min=1"` + // InitialBackOffDuration is the initial value of the retry period, defaults to 1 second. + InitialBackOffDuration time.Duration `config:"initial_backoff_duration" validate:"min=1"` + // MaxBackOffDuration is the maximum value of the retry period, defaults to 30 seconds. + MaxBackOffDuration time.Duration `config:"max_backoff_duration" validate:"min=2"` + // BackOffMultiplier is the factor by which the retry period increases. It should be greater than 1 and defaults to 2. + BackOffMultiplier float64 `config:"backoff_multiplier" validate:"min=1.1"` +} + func (c authConfig) Validate() error { // credentials_file if c.CredentialsFile != nil { @@ -126,5 +141,11 @@ func defaultConfig() config { PollInterval: 5 * time.Minute, BucketTimeOut: 120 * time.Second, ParseJSON: false, + Retry: retryConfig{ + MaxAttempts: 3, + InitialBackOffDuration: time.Second, + MaxBackOffDuration: 30 * time.Second, + BackOffMultiplier: 2, + }, } } diff --git a/x-pack/filebeat/input/gcs/input.go b/x-pack/filebeat/input/gcs/input.go index 33e46d034d7..1d6bff8b857 100644 --- a/x-pack/filebeat/input/gcs/input.go +++ b/x-pack/filebeat/input/gcs/input.go @@ -72,6 +72,7 @@ func configure(cfg *conf.C) ([]cursor.Source, cursor.Input, error) { ExpandEventListFromField: bucket.ExpandEventListFromField, FileSelectors: bucket.FileSelectors, ReaderConfig: bucket.ReaderConfig, + Retry: config.Retry, }) } @@ -158,9 +159,13 @@ func (input *gcsInput) Run(inputCtx v2.Context, src cursor.Source, } bucket := client.Bucket(currentSource.BucketName).Retryer( + // Use WithMaxAttempts to change the maximum number of attempts. + storage.WithMaxAttempts(currentSource.Retry.MaxAttempts), // Use WithBackoff to change the timing of the exponential backoff. storage.WithBackoff(gax.Backoff{ - Initial: 2 * time.Second, + Initial: currentSource.Retry.InitialBackOffDuration, + Max: currentSource.Retry.MaxBackOffDuration, + Multiplier: currentSource.Retry.BackOffMultiplier, }), // RetryAlways will retry the operation even if it is non-idempotent. // Since we are only reading, the operation is always idempotent diff --git a/x-pack/filebeat/input/gcs/input_stateless.go b/x-pack/filebeat/input/gcs/input_stateless.go index c0038bf31dc..b6901f0df25 100644 --- a/x-pack/filebeat/input/gcs/input_stateless.go +++ b/x-pack/filebeat/input/gcs/input_stateless.go @@ -6,7 +6,6 @@ package gcs import ( "context" - "time" "cloud.google.com/go/storage" gax "github.com/googleapis/gax-go/v2" @@ -64,6 +63,7 @@ func (in *statelessInput) Run(inputCtx v2.Context, publisher stateless.Publisher ExpandEventListFromField: bucket.ExpandEventListFromField, FileSelectors: bucket.FileSelectors, ReaderConfig: bucket.ReaderConfig, + Retry: in.config.Retry, } st := newState() @@ -80,9 +80,13 @@ func (in *statelessInput) Run(inputCtx v2.Context, publisher stateless.Publisher }() bkt := client.Bucket(currentSource.BucketName).Retryer( + // Use WithMaxAttempts to change the maximum number of attempts. + storage.WithMaxAttempts(currentSource.Retry.MaxAttempts), // Use WithBackoff to change the timing of the exponential backoff. storage.WithBackoff(gax.Backoff{ - Initial: 2 * time.Second, + Initial: currentSource.Retry.InitialBackOffDuration, + Max: currentSource.Retry.MaxBackOffDuration, + Multiplier: currentSource.Retry.BackOffMultiplier, }), // RetryAlways will retry the operation even if it is non-idempotent. // Since we are only reading, the operation is always idempotent diff --git a/x-pack/filebeat/input/gcs/input_test.go b/x-pack/filebeat/input/gcs/input_test.go index 5595622c93e..345c7c57909 100644 --- a/x-pack/filebeat/input/gcs/input_test.go +++ b/x-pack/filebeat/input/gcs/input_test.go @@ -520,6 +520,81 @@ func Test_StorageClient(t *testing.T) { mock.Gcs_test_new_object_docs_ata_json: true, }, }, + { + name: "RetryWithDefaultValues", + baseConfig: map[string]interface{}{ + "project_id": "elastic-sa", + "auth.credentials_file.path": "testdata/gcs_creds.json", + "max_workers": 1, + "poll": true, + "poll_interval": "1m", + "bucket_timeout": "1m", + "buckets": []map[string]interface{}{ + { + "name": "gcs-test-new", + }, + }, + }, + mockHandler: mock.GCSRetryServer, + expected: map[string]bool{ + mock.Gcs_test_new_object_ata_json: true, + mock.Gcs_test_new_object_data3_json: true, + mock.Gcs_test_new_object_docs_ata_json: true, + }, + }, + { + name: "RetryWithCustomValues", + baseConfig: map[string]interface{}{ + "project_id": "elastic-sa", + "auth.credentials_file.path": "testdata/gcs_creds.json", + "max_workers": 1, + "poll": true, + "poll_interval": "10s", + "bucket_timeout": "10s", + "retry": map[string]interface{}{ + "max_attempts": 5, + "initial_backoff_duration": "1s", + "max_backoff_duration": "3s", + "backoff_multiplier": 1.4, + }, + "buckets": []map[string]interface{}{ + { + "name": "gcs-test-new", + }, + }, + }, + mockHandler: mock.GCSRetryServer, + expected: map[string]bool{ + mock.Gcs_test_new_object_ata_json: true, + mock.Gcs_test_new_object_data3_json: true, + mock.Gcs_test_new_object_docs_ata_json: true, + }, + }, + { + name: "RetryMinimumValueCheck", + baseConfig: map[string]interface{}{ + "project_id": "elastic-sa", + "auth.credentials_file.path": "testdata/gcs_creds.json", + "max_workers": 1, + "poll": true, + "poll_interval": "10s", + "bucket_timeout": "10s", + "retry": map[string]interface{}{ + "max_attempts": 5, + "initial_backoff_duration": "1s", + "max_backoff_duration": "3s", + "backoff_multiplier": 1, + }, + "buckets": []map[string]interface{}{ + { + "name": "gcs-test-new", + }, + }, + }, + mockHandler: mock.GCSRetryServer, + expected: map[string]bool{}, + isError: errors.New("requires value >= 1.1 accessing 'retry.backoff_multiplier'"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/x-pack/filebeat/input/gcs/mock/mock.go b/x-pack/filebeat/input/gcs/mock/mock.go index 50d2a431e01..1def436511a 100644 --- a/x-pack/filebeat/input/gcs/mock/mock.go +++ b/x-pack/filebeat/input/gcs/mock/mock.go @@ -98,3 +98,43 @@ func GCSFileServer() http.Handler { w.Write([]byte("resource not found")) }) } + +//nolint:errcheck // We can ignore errors here, as this is just for testing +func GCSRetryServer() http.Handler { + retries := 0 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + retries++ + path := strings.Split(strings.TrimLeft(r.URL.Path, "/"), "/") + if r.Method == http.MethodGet && retries >= 3 { + switch len(path) { + case 2: + if path[0] == "b" { + if buckets[path[1]] { + w.Write([]byte(fetchBucket[path[1]])) + return + } + } else if buckets[path[0]] && availableObjects[path[0]][path[1]] { + w.Write([]byte(objects[path[0]][path[1]])) + return + } + case 3: + if path[0] == "b" && path[2] == "o" { + if buckets[path[1]] { + w.Write([]byte(objectList[path[1]])) + return + } + } else if buckets[path[0]] { + objName := strings.Join(path[1:], "/") + if availableObjects[path[0]][objName] { + w.Write([]byte(objects[path[0]][objName])) + return + } + } + default: + w.WriteHeader(http.StatusNotFound) + return + } + } + w.WriteHeader(http.StatusInternalServerError) + }) +} diff --git a/x-pack/filebeat/input/gcs/types.go b/x-pack/filebeat/input/gcs/types.go index a34c7f7160f..82fb737cdb6 100644 --- a/x-pack/filebeat/input/gcs/types.go +++ b/x-pack/filebeat/input/gcs/types.go @@ -22,6 +22,7 @@ type Source struct { FileSelectors []fileSelectorConfig ReaderConfig readerConfig ExpandEventListFromField string + Retry retryConfig } func (s *Source) Name() string { diff --git a/x-pack/filebeat/input/httpjson/input.go b/x-pack/filebeat/input/httpjson/input.go index ad61aceff89..51a9d446f47 100644 --- a/x-pack/filebeat/input/httpjson/input.go +++ b/x-pack/filebeat/input/httpjson/input.go @@ -15,6 +15,7 @@ import ( "net/url" "os" "path/filepath" + "sort" "strings" "time" @@ -33,6 +34,7 @@ import ( "github.com/elastic/beats/v7/libbeat/version" "github.com/elastic/beats/v7/x-pack/filebeat/input/internal/httplog" "github.com/elastic/beats/v7/x-pack/filebeat/input/internal/httpmon" + "github.com/elastic/beats/v7/x-pack/filebeat/input/internal/private" "github.com/elastic/elastic-agent-libs/logp" "github.com/elastic/elastic-agent-libs/mapstr" "github.com/elastic/elastic-agent-libs/monitoring" @@ -91,6 +93,60 @@ func Plugin(log *logp.Logger, store inputcursor.StateStore) v2.Plugin { } } +type redact struct { + value mapstrM + fields []string +} + +func (r redact) MarshalLogObject(enc zapcore.ObjectEncoder) error { + v, err := private.Redact(r.value, "", r.fields) + if err != nil { + return fmt.Errorf("could not redact value: %v", err) + } + return v.MarshalLogObject(enc) +} + +// mapstrM is a non-mutating version of mapstr.M. +// See https://github.com/elastic/elastic-agent-libs/issues/232. +type mapstrM mapstr.M + +// MarshalLogObject implements the zapcore.ObjectMarshaler interface and allows +// for more efficient marshaling of mapstrM in structured logging. +func (m mapstrM) MarshalLogObject(enc zapcore.ObjectEncoder) error { + if len(m) == 0 { + return nil + } + + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := m[k] + if inner, ok := tryToMapStr(v); ok { + err := enc.AddObject(k, inner) + if err != nil { + return fmt.Errorf("failed to add object: %w", err) + } + continue + } + zap.Any(k, v).AddTo(enc) + } + return nil +} + +func tryToMapStr(v interface{}) (mapstrM, bool) { + switch m := v.(type) { + case mapstrM: + return m, true + case map[string]interface{}: + return mapstrM(m), true + default: + return nil, false + } +} + func test(url *url.URL) error { port := func() string { if url.Port() != "" { diff --git a/x-pack/filebeat/input/httpjson/request.go b/x-pack/filebeat/input/httpjson/request.go index 160ac67fe9e..fb847570826 100644 --- a/x-pack/filebeat/input/httpjson/request.go +++ b/x-pack/filebeat/input/httpjson/request.go @@ -465,7 +465,7 @@ func (rf *requestFactory) newRequest(ctx *transformContext) (transformable, erro } } - rf.log.Debugf("new request: %#v", req) + rf.log.Debugw("new request", "req", redact{value: mapstrM(req), fields: []string{"header.Authorization"}}) return req, nil } diff --git a/x-pack/filebeat/input/netflow/decoder/atomic/bool.go b/x-pack/filebeat/input/netflow/decoder/atomic/bool.go deleted file mode 100644 index b294cc6c395..00000000000 --- a/x-pack/filebeat/input/netflow/decoder/atomic/bool.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -// or more contributor license agreements. Licensed under the Elastic License; -// you may not use this file except in compliance with the Elastic License. - -package atomic - -import "sync/atomic" - -type Bool struct { - value uint32 -} - -func (b *Bool) Store(value bool) { - atomic.StoreUint32(&b.value, encodeBool(value)) -} - -func (b *Bool) CAS(old bool, new bool) (swapped bool) { - return atomic.CompareAndSwapUint32(&b.value, encodeBool(old), encodeBool(new)) -} - -func (b *Bool) Load() (value bool) { - return atomic.LoadUint32(&b.value) != 0 -} - -func encodeBool(value bool) (result uint32) { - if value { - result = 1 - } - return -} diff --git a/x-pack/filebeat/input/netflow/decoder/v9/session.go b/x-pack/filebeat/input/netflow/decoder/v9/session.go index 492576f6b96..e72fa1ab80a 100644 --- a/x-pack/filebeat/input/netflow/decoder/v9/session.go +++ b/x-pack/filebeat/input/netflow/decoder/v9/session.go @@ -8,9 +8,9 @@ import ( "log" "net" "sync" + "sync/atomic" "time" - "github.com/elastic/beats/v7/x-pack/filebeat/input/netflow/decoder/atomic" "github.com/elastic/beats/v7/x-pack/filebeat/input/netflow/decoder/config" "github.com/elastic/beats/v7/x-pack/filebeat/input/netflow/decoder/template" ) @@ -83,7 +83,7 @@ func (s *SessionState) ExpireTemplates() (alive int, removed int) { var toDelete []TemplateKey s.mutex.RLock() for id, template := range s.Templates { - if !template.Delete.CAS(false, true) { + if !template.Delete.CompareAndSwap(false, true) { toDelete = append(toDelete, id) } } @@ -183,7 +183,7 @@ func (m *SessionMap) cleanup() (aliveSession int, removedSession int, aliveTempl a, r := session.ExpireTemplates() aliveTemplates += a removedTemplates += r - if !session.Delete.CAS(false, true) { + if !session.Delete.CompareAndSwap(false, true) { toDelete = append(toDelete, key) } } diff --git a/x-pack/heartbeat/monitors/browser/synthexec/execmultiplexer.go b/x-pack/heartbeat/monitors/browser/synthexec/execmultiplexer.go index f3684398a51..fe384ae15b2 100644 --- a/x-pack/heartbeat/monitors/browser/synthexec/execmultiplexer.go +++ b/x-pack/heartbeat/monitors/browser/synthexec/execmultiplexer.go @@ -5,12 +5,10 @@ package synthexec -import ( - "github.com/elastic/beats/v7/libbeat/common/atomic" -) +import "sync/atomic" type ExecMultiplexer struct { - eventCounter *atomic.Int + eventCounter *atomic.Int64 synthEvents chan *SynthEvent done chan struct{} } @@ -27,7 +25,7 @@ func (e *ExecMultiplexer) writeSynthEvent(se *SynthEvent) { if se.Type == JourneyStart { e.eventCounter.Store(-1) } - se.index = e.eventCounter.Inc() + se.index = int(e.eventCounter.Add(1)) e.synthEvents <- se } @@ -48,8 +46,10 @@ func (e *ExecMultiplexer) Wait() { } func NewExecMultiplexer() *ExecMultiplexer { + c := &atomic.Int64{} + c.Store(-1) // Start from -1 so first call to Inc returns 0 return &ExecMultiplexer{ - eventCounter: atomic.NewInt(-1), // Start from -1 so first call to Inc returns 0 + eventCounter: c, synthEvents: make(chan *SynthEvent), done: make(chan struct{}), } diff --git a/x-pack/heartbeat/scenarios/framework/framework.go b/x-pack/heartbeat/scenarios/framework/framework.go index a2fb77e6307..6119f549e99 100644 --- a/x-pack/heartbeat/scenarios/framework/framework.go +++ b/x-pack/heartbeat/scenarios/framework/framework.go @@ -30,11 +30,13 @@ import ( beatversion "github.com/elastic/beats/v7/libbeat/version" ) -type ScenarioRun func(t *testing.T) (config mapstr.M, meta ScenarioRunMeta, close func(), err error) -type ScenarioRunMeta struct { - URL *url.URL - Status monitorstate.StateStatus -} +type ( + ScenarioRun func(t *testing.T) (config mapstr.M, meta ScenarioRunMeta, close func(), err error) + ScenarioRunMeta struct { + URL *url.URL + Status monitorstate.StateStatus + } +) type Scenario struct { Name string @@ -155,7 +157,6 @@ func NewScenarioDB() *ScenarioDB { ByTag: map[string][]Scenario{}, All: []Scenario{}, } - } func (sdb *ScenarioDB) Init() { @@ -250,7 +251,9 @@ func runMonitorOnce(t *testing.T, monitorConfig mapstr.M, meta ScenarioRunMeta, mIface, err := f.Create(pipe, conf) require.NoError(t, err) - mtr.monitor = mIface.(*monitors.Monitor) + mon, ok := mIface.(*monitors.Monitor) + require.True(t, ok, "type assertion didn't succeed") + mtr.monitor = mon require.NotNil(t, mtr.monitor, "could not convert to monitor %v", mIface) mtr.Events = pipe.PublishedEvents @@ -281,12 +284,8 @@ func setupFactoryAndSched(location *hbconfig.LocationWithID, stateLoader monitor EphemeralID: eid, FirstStart: time.Now(), StartTime: time.Now(), - Monitoring: struct { - DefaultUsername string - }{ - DefaultUsername: "test", - }, } + info.Monitoring.DefaultUsername = "test" sched = scheduler.Create( 1, diff --git a/x-pack/libbeat/management/managerV2.go b/x-pack/libbeat/management/managerV2.go index 2ffa7f1c807..87a4a2146f8 100644 --- a/x-pack/libbeat/management/managerV2.go +++ b/x-pack/libbeat/management/managerV2.go @@ -897,6 +897,13 @@ func (cm *BeatV2Manager) reloadAPM(unit *agentUnit) { apmConfig = expected.APMConfig } } + + if (cm.lastAPMCfg == nil && apmConfig == nil) || (cm.lastAPMCfg != nil && gproto.Equal(cm.lastAPMCfg, apmConfig)) { + // configuration for the APM tracing did not change; do nothing + cm.logger.Debug("Skipped reloading APM tracing; configuration didn't change") + return + } + if apmConfig == nil { // APM tracing is being stopped cm.logger.Debug("Stopping APM tracing") @@ -911,12 +918,6 @@ func (cm *BeatV2Manager) reloadAPM(unit *agentUnit) { return } - if cm.lastAPMCfg != nil && gproto.Equal(cm.lastAPMCfg, apmConfig) { - // configuration for the APM tracing did not change; do nothing - cm.logger.Debug("Skipped reloading APM tracing; configuration didn't change") - return - } - uconfig, err := conf.NewConfigFrom(apmConfig) if err != nil { cm.logger.Errorf("Failed to create uconfig from APM configuration: %s", err) diff --git a/x-pack/libbeat/management/managerV2_test.go b/x-pack/libbeat/management/managerV2_test.go index f76bda8e1ff..16c63b2448f 100644 --- a/x-pack/libbeat/management/managerV2_test.go +++ b/x-pack/libbeat/management/managerV2_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" "google.golang.org/grpc" @@ -265,6 +266,141 @@ func TestManagerV2(t *testing.T) { }, 15*time.Second, 300*time.Millisecond) } +func TestManagerV2_ReloadCount(t *testing.T) { + r := reload.NewRegistry() + + output := &reloadable{} + r.MustRegisterOutput(output) + inputs := &reloadableList{} + r.MustRegisterInput(inputs) + apm := &reloadable{} + r.MustRegisterAPM(apm) + + inputConfigUpdated := make(chan struct{}) + onObserved := func(observed *proto.CheckinObserved, currentIdx int) { + if currentIdx == 1 { + period, err := inputs.Configs()[0].Config.String("period", -1) + require.NoError(t, err) + if period == "10m" { + select { + case <-inputConfigUpdated: + default: + close(inputConfigUpdated) + } + } + } + } + + agentInfo := &proto.AgentInfo{ + Id: "elastic-agent-id", + Version: version.GetDefaultVersion(), + Snapshot: true, + } + srv := integration.NewMockServer([]*proto.CheckinExpected{ + { + AgentInfo: agentInfo, + Units: []*proto.UnitExpected{ + { + Id: "output-unit", + Type: proto.UnitType_OUTPUT, + ConfigStateIdx: 1, + Config: &proto.UnitExpectedConfig{ + Id: "default", + Type: "elasticsearch", + Name: "elasticsearch", + }, + State: proto.State_HEALTHY, + LogLevel: proto.UnitLogLevel_INFO, + }, + { + Id: "input-unit-1", + Type: proto.UnitType_INPUT, + ConfigStateIdx: 1, + Config: &proto.UnitExpectedConfig{ + Id: "system/metrics-system-default-system-1", + Type: "system/metrics", + Name: "system-1", + Streams: []*proto.Stream{ + { + Id: "system/metrics-system.filesystem-default-system-1", + Source: integration.RequireNewStruct(t, map[string]interface{}{ + "metricsets": []interface{}{"filesystem"}, + "period": "1m", + }), + }, + }, + }, + State: proto.State_HEALTHY, + LogLevel: proto.UnitLogLevel_INFO, + }, + }, + Features: nil, + FeaturesIdx: 1, + }, + { + AgentInfo: agentInfo, + Units: []*proto.UnitExpected{ + { + Id: "output-unit", + Type: proto.UnitType_OUTPUT, + ConfigStateIdx: 1, + State: proto.State_HEALTHY, + LogLevel: proto.UnitLogLevel_INFO, + }, + { + Id: "input-unit-1", + Type: proto.UnitType_INPUT, + ConfigStateIdx: 2, + Config: &proto.UnitExpectedConfig{ + Id: "system/metrics-system-default-system-1", + Type: "system/metrics", + Name: "system-1", + Streams: []*proto.Stream{ + { + Id: "system/metrics-system.filesystem-default-system-1", + Source: integration.RequireNewStruct(t, map[string]interface{}{ + "metricsets": []interface{}{"filesystem"}, + "period": "10m", + }), + }, + }, + }, + State: proto.State_HEALTHY, + LogLevel: proto.UnitLogLevel_INFO, + }, + }, + Features: nil, + FeaturesIdx: 1, + }, + }, + onObserved, + 500*time.Millisecond, + ) + require.NoError(t, srv.Start()) + defer srv.Stop() + + client := client.NewV2(fmt.Sprintf(":%d", srv.Port), "", client.VersionInfo{ + Name: "program", + Meta: map[string]string{ + "key": "value", + }, + }, client.WithGRPCDialOptions(grpc.WithTransportCredentials(insecure.NewCredentials()))) + + m, err := NewV2AgentManagerWithClient(&Config{ + Enabled: true, + }, r, client) + require.NoError(t, err) + + err = m.Start() + require.NoError(t, err) + defer m.Stop() + + <-inputConfigUpdated + assert.Equal(t, 1, output.reloadCount) // initial load + assert.Equal(t, 2, inputs.reloadCount) // initial load + config update + assert.Equal(t, 0, apm.reloadCount) // no apm tracing config applied +} + func TestOutputError(t *testing.T) { // Uncomment the line below to see the debug logs for this test // logp.DevelopmentSetup(logp.WithLevel(logp.DebugLevel), logp.WithSelectors("*")) @@ -552,19 +688,22 @@ func TestErrorPerUnit(t *testing.T) { } type reloadable struct { - mx sync.Mutex - config *reload.ConfigWithMeta + mx sync.Mutex + config *reload.ConfigWithMeta + reloadCount int } type reloadableList struct { - mx sync.Mutex - configs []*reload.ConfigWithMeta + mx sync.Mutex + configs []*reload.ConfigWithMeta + reloadCount int } func (r *reloadable) Reload(config *reload.ConfigWithMeta) error { r.mx.Lock() defer r.mx.Unlock() r.config = config + r.reloadCount++ return nil } @@ -578,6 +717,7 @@ func (r *reloadableList) Reload(configs []*reload.ConfigWithMeta) error { r.mx.Lock() defer r.mx.Unlock() r.configs = configs + r.reloadCount++ return nil } diff --git a/x-pack/metricbeat/include/list.go b/x-pack/metricbeat/include/list.go index 01ce86edf78..b9b426cf0ad 100644 --- a/x-pack/metricbeat/include/list.go +++ b/x-pack/metricbeat/include/list.go @@ -21,6 +21,8 @@ import ( _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/azure/billing" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/azure/monitor" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/azure/storage" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/benchmark" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/benchmark/info" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/cloudfoundry" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/cloudfoundry/container" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/cloudfoundry/counter" diff --git a/x-pack/metricbeat/metricbeat.reference.yml b/x-pack/metricbeat/metricbeat.reference.yml index f7f048da84b..8c4250caece 100644 --- a/x-pack/metricbeat/metricbeat.reference.yml +++ b/x-pack/metricbeat/metricbeat.reference.yml @@ -404,6 +404,14 @@ metricbeat.modules: # Monitoring instead of metricbeat-* indices. #xpack.enabled: false +#------------------------------ Benchmark Module ------------------------------ +- module: benchmark + metricsets: + - info + enabled: false + period: 10s + + #--------------------------------- Ceph Module --------------------------------- # Metricsets depending on the Ceph REST API (default port: 5000) - module: ceph @@ -513,6 +521,9 @@ metricbeat.modules: # If set to true, replace dots in labels with `_`. #labels.dedot: false + # Docker module supports metrics collection from podman's docker compatible API. In case of podman set to true. + # podman: false + # Skip metrics for certain device major numbers in docker/diskio. # Necessary on systems with software RAID, device mappers, # or other configurations where virtual disks will sum metrics from other disks. @@ -1356,9 +1367,11 @@ metricbeat.modules: # Password to use when connecting to PostgreSQL. Empty by default. #password: pass -#----------------------- Prometheus Typed Metrics Module ----------------------- +#------------------------------ Prometheus Module ------------------------------ +# Metrics collected from a Prometheus endpoint - module: prometheus period: 10s + metricsets: ["collector"] hosts: ["localhost:9090"] metrics_path: /metrics #metrics_filters: @@ -1367,20 +1380,14 @@ metricbeat.modules: #username: "user" #password: "secret" + # Count number of metrics present in Elasticsearch document (default: false) + #metrics_count: false + # This can be used for service account based authorization: #bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token #ssl.certificate_authorities: # - /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt - # Count number of metrics present in Elasticsearch document (default: false) - #metrics_count: false - - # Use Elasticsearch histogram type to store histograms (beta, default: false) - # This will change the default layout and put metric type in the field name - #use_types: true - - # Store counter rates instead of original cumulative counters (experimental, default: false) - #rate_counters: true # Metrics sent by a Prometheus server using remote_write option #- module: prometheus @@ -1388,24 +1395,12 @@ metricbeat.modules: # host: "localhost" # port: "9201" - # Secure settings for the server using TLS/SSL: - #ssl.certificate: "/etc/pki/server/cert.pem" - #ssl.key: "/etc/pki/server/cert.key" - # Count number of metrics present in Elasticsearch document (default: false) #metrics_count: false - # Use Elasticsearch histogram type to store histograms (beta, default: false) - # This will change the default layout and put metric type in the field name - #use_types: true - - # Store counter rates instead of original cumulative counters (experimental, default: false) - #rate_counters: true - - # Define patterns for counter and histogram types so as to identify metrics' types according to these patterns - #types_patterns: - # counter_patterns: [] - # histogram_patterns: [] + # Secure settings for the server using TLS/SSL: + #ssl.certificate: "/etc/pki/server/cert.pem" + #ssl.key: "/etc/pki/server/cert.key" # Metrics that will be collected using a PromQL #- module: prometheus @@ -1433,11 +1428,9 @@ metricbeat.modules: # params: # query: "some_value" -#------------------------------ Prometheus Module ------------------------------ -# Metrics collected from a Prometheus endpoint +#----------------------- Prometheus Typed Metrics Module ----------------------- - module: prometheus period: 10s - metricsets: ["collector"] hosts: ["localhost:9090"] metrics_path: /metrics #metrics_filters: @@ -1446,14 +1439,20 @@ metricbeat.modules: #username: "user" #password: "secret" - # Count number of metrics present in Elasticsearch document (default: false) - #metrics_count: false - # This can be used for service account based authorization: #bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token #ssl.certificate_authorities: # - /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt + # Count number of metrics present in Elasticsearch document (default: false) + #metrics_count: false + + # Use Elasticsearch histogram type to store histograms (beta, default: false) + # This will change the default layout and put metric type in the field name + #use_types: true + + # Store counter rates instead of original cumulative counters (experimental, default: false) + #rate_counters: true # Metrics sent by a Prometheus server using remote_write option #- module: prometheus @@ -1461,13 +1460,25 @@ metricbeat.modules: # host: "localhost" # port: "9201" - # Count number of metrics present in Elasticsearch document (default: false) - #metrics_count: false - # Secure settings for the server using TLS/SSL: #ssl.certificate: "/etc/pki/server/cert.pem" #ssl.key: "/etc/pki/server/cert.key" + # Count number of metrics present in Elasticsearch document (default: false) + #metrics_count: false + + # Use Elasticsearch histogram type to store histograms (beta, default: false) + # This will change the default layout and put metric type in the field name + #use_types: true + + # Store counter rates instead of original cumulative counters (experimental, default: false) + #rate_counters: true + + # Define patterns for counter and histogram types so as to identify metrics' types according to these patterns + #types_patterns: + # counter_patterns: [] + # histogram_patterns: [] + # Metrics that will be collected using a PromQL #- module: prometheus # metricsets: ["query"] diff --git a/x-pack/metricbeat/module/benchmark/_meta/config.yml b/x-pack/metricbeat/module/benchmark/_meta/config.yml new file mode 100644 index 00000000000..535ce486754 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/_meta/config.yml @@ -0,0 +1,6 @@ +- module: benchmark + metricsets: + - info + enabled: false + period: 10s + diff --git a/x-pack/metricbeat/module/benchmark/_meta/docs.asciidoc b/x-pack/metricbeat/module/benchmark/_meta/docs.asciidoc new file mode 100644 index 00000000000..5b0dbf829fc --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/_meta/docs.asciidoc @@ -0,0 +1,31 @@ +include::{libbeat-dir}/shared/integration-link.asciidoc[] + +:modulename!: + +The `benchmark` module is used to generate synthetic metrics at a predictable rate. This can be useful when you want to test output settings or test system sizing without using real data. + +The `benchmark` module metricset is `info`. + +[source,yaml] +---- +- module: benchmark + metricsets: + - info + enabled: true + period: 10s +---- + +[float] +== Metricsets + +[float] +=== `info` +A metric that includes a `counter` field which is used to keep the metric unique. + +[float] +=== Module-specific configuration notes + +`count`:: number, the number of metrics to emit per fetch. + + + diff --git a/x-pack/metricbeat/module/benchmark/_meta/fields.yml b/x-pack/metricbeat/module/benchmark/_meta/fields.yml new file mode 100644 index 00000000000..308834cb5dc --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/_meta/fields.yml @@ -0,0 +1,10 @@ +- key: benchmark + title: "Benchmark" + release: beta + description: > + benchmark module + fields: + - name: benchmark + type: group + description: > + fields: diff --git a/x-pack/metricbeat/module/benchmark/doc.go b/x-pack/metricbeat/module/benchmark/doc.go new file mode 100644 index 00000000000..461045f17c2 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/doc.go @@ -0,0 +1,6 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// Package benchmark is a Metricbeat module that contains MetricSets. +package benchmark diff --git a/x-pack/metricbeat/module/benchmark/fields.go b/x-pack/metricbeat/module/benchmark/fields.go new file mode 100644 index 00000000000..30fa60460d8 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/fields.go @@ -0,0 +1,23 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// Code generated by beats/dev-tools/cmd/asset/asset.go - DO NOT EDIT. + +package benchmark + +import ( + "github.com/elastic/beats/v7/libbeat/asset" +) + +func init() { + if err := asset.SetFields("metricbeat", "benchmark", asset.ModuleFieldsPri, AssetBenchmark); err != nil { + panic(err) + } +} + +// AssetBenchmark returns asset data. +// This is the base64 encoded zlib format compressed contents of module/benchmark. +func AssetBenchmark() string { + return "eJx8kM1txCAUhO9UMdr7NsAhh9SQBliYDcj8WPhZEd1HOHFiEyvf8Q36ZsQdE5vGg9n6ZOqkAAkSqXF73W83BVRGmoX9pRgFOC62hllCyRovCsCvA6m4NVIBz8DoFr3Fd2STeK7qSJup8V7LOn9fLtxn1VEX8rP8HK9snXH9zmXTF4N3rD9OsGXNwnrK9iUT20epbsj+6e28eSKL3zYgUWqwYAoidHg0iOffz/4MAAD//xhTfdM=" +} diff --git a/x-pack/metricbeat/module/benchmark/info/_meta/data.json b/x-pack/metricbeat/module/benchmark/info/_meta/data.json new file mode 100644 index 00000000000..a64b394dfd9 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/_meta/data.json @@ -0,0 +1,23 @@ +{ + "@timestamp": "2016-05-23T08:05:34.853Z", + "metricset": { + "name": "info", + "period": 1000 + }, + "event": { + "duration": 27000, + "dataset": "benchmark.info", + "module": "benchmark" + }, + "service": { + "type": "benchmark" + }, + "service": { + "type": "benchmark" + }, + "benchmark": { + "info": { + "counter": 42 + } + } +} diff --git a/x-pack/metricbeat/module/benchmark/info/_meta/docs.asciidoc b/x-pack/metricbeat/module/benchmark/info/_meta/docs.asciidoc new file mode 100644 index 00000000000..a8f407cf7f3 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the info metricset of the module benchmark. diff --git a/x-pack/metricbeat/module/benchmark/info/_meta/fields.yml b/x-pack/metricbeat/module/benchmark/info/_meta/fields.yml new file mode 100644 index 00000000000..0068b75bcf0 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/_meta/fields.yml @@ -0,0 +1,10 @@ +- name: info + type: group + release: beta + description: > + info + fields: + - name: counter + type: keyword + description: > + The nth info metric emitted by the benchmark module diff --git a/x-pack/metricbeat/module/benchmark/info/config.go b/x-pack/metricbeat/module/benchmark/info/config.go new file mode 100644 index 00000000000..424a8a5c60c --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/config.go @@ -0,0 +1,24 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package info + +import ( + "errors" +) + +type infoConfig struct { + Count uint `config:"count"` +} + +var defaultConfig = infoConfig{ + Count: 1, +} + +func (c *infoConfig) Validate() error { + if c.Count == 0 { + return errors.New("benchmark module 'count' must be greater than 0") + } + return nil +} diff --git a/x-pack/metricbeat/module/benchmark/info/config_test.go b/x-pack/metricbeat/module/benchmark/info/config_test.go new file mode 100644 index 00000000000..95030443b01 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/config_test.go @@ -0,0 +1,37 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package info + +import ( + "strings" + "testing" +) + +func TestValidate(t *testing.T) { + tests := map[string]struct { + cfg infoConfig + expectError bool + errorString string + }{ + "default": {cfg: defaultConfig}, + "empty": {cfg: infoConfig{}, expectError: true, errorString: "benchmark module 'count' must be greater than 0"}, + "counter 0": {cfg: infoConfig{Count: 0}, expectError: true, errorString: "benchmark module 'count' must be greater than 0"}, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.cfg.Validate() + if err == nil && tc.expectError == true { + t.Fatalf("expected validation error, didn't get it") + } + if err != nil && tc.expectError == false { + t.Fatalf("unexpected validation error: %s", err) + } + if err != nil && !strings.Contains(err.Error(), tc.errorString) { + t.Fatalf("error: '%s' didn't contain expected string: '%s'", err, tc.errorString) + } + }) + } +} diff --git a/x-pack/metricbeat/module/benchmark/info/info.go b/x-pack/metricbeat/module/benchmark/info/info.go new file mode 100644 index 00000000000..bd4ca1e6272 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/info.go @@ -0,0 +1,62 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package info + +import ( + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host is defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet("benchmark", "info", New) +} + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + counter uint + eventsPerFetch uint +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The benchmark info metricset is beta.") + + config := defaultConfig + if err := base.Module().UnpackConfig(&config); err != nil { + return nil, err + } + + return &MetricSet{ + BaseMetricSet: base, + counter: 1, + eventsPerFetch: config.Count, + }, nil +} + +// Fetch method implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch(report mb.ReporterV2) error { + for i := uint(0); i < m.eventsPerFetch; i++ { + report.Event(mb.Event{ + MetricSetFields: mapstr.M{ + "counter": m.counter, + }, + }) + m.counter++ + } + + return nil +} diff --git a/x-pack/metricbeat/module/benchmark/info/info_test.go b/x-pack/metricbeat/module/benchmark/info/info_test.go new file mode 100644 index 00000000000..1df775e76b0 --- /dev/null +++ b/x-pack/metricbeat/module/benchmark/info/info_test.go @@ -0,0 +1,54 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package info + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + mbtest "github.com/elastic/beats/v7/metricbeat/mb/testing" +) + +func TestFetch(t *testing.T) { + tests := map[string]struct { + count uint + }{ + "one count": {count: uint(1)}, + "five count": {count: uint(5)}, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + config := map[string]interface{}{ + "module": "benchmark", + "period": "1s", + "metricsets": []string{"info"}, + "count": tc.count, + } + t.Cleanup(func() { + if t.Failed() { + t.Logf("Contents of config:\n%v", config) + } + }) + + f := mbtest.NewFetcher(t, config) + events, errs := f.FetchEvents() + assert.Emptyf(t, errs, "errs should be empty, err: %v", errs) + assert.Equalf(t, int(tc.count), len(events), "events should have %d events not %d, events: %v", int(tc.count), len(events), events) + + for i, event := range events { + msf := event.MetricSetFields + + ok, err := msf.HasKey("counter") + assert.Truef(t, ok, "MetricSetFields must contain \"counter\", msf: %v", msf) + assert.NoErrorf(t, err, "MetricSetFields must contain \"counter\", msf: %v", msf) + + v, err := msf.GetValue("counter") + assert.NoErrorf(t, err, "MetricSetFields must contain \"counter\", msf: %v", msf) + assert.Equalf(t, uint(i+1), v, "counter should be %d, was %v", i+1, v) + } + }) + } +} diff --git a/x-pack/metricbeat/module/cloudfoundry/v1.go b/x-pack/metricbeat/module/cloudfoundry/v1.go index f03f6a98ebb..86610e0d6cb 100644 --- a/x-pack/metricbeat/module/cloudfoundry/v1.go +++ b/x-pack/metricbeat/module/cloudfoundry/v1.go @@ -7,7 +7,8 @@ package cloudfoundry import ( - "github.com/elastic/beats/v7/libbeat/common/atomic" + "sync/atomic" + "github.com/elastic/beats/v7/metricbeat/mb" cfcommon "github.com/elastic/beats/v7/x-pack/libbeat/common/cloudfoundry" "github.com/elastic/elastic-agent-libs/logp" @@ -29,7 +30,6 @@ func newModuleV1(base mb.BaseModule, hub CloudfoundryHub, log *logp.Logger) (*Mo m := ModuleV1{ BaseModule: base, log: log, - running: atomic.MakeBool(false), } consumer, err := hub.DopplerConsumer(cfcommon.DopplerCallbacks{ Metric: m.callback, @@ -83,7 +83,7 @@ func (m *ModuleV1) callback(event cfcommon.Event) { // run ensures that the module is running with the passed subscription func (m *ModuleV1) run(s subscription) { - if !m.running.CAS(false, true) { + if !m.running.CompareAndSwap(false, true) { // Module is already running, queue subscription for current dispatcher. m.subscriptions <- s return diff --git a/x-pack/metricbeat/modules.d/benchmark.yml.disabled b/x-pack/metricbeat/modules.d/benchmark.yml.disabled new file mode 100644 index 00000000000..29901546c42 --- /dev/null +++ b/x-pack/metricbeat/modules.d/benchmark.yml.disabled @@ -0,0 +1,9 @@ +# Module: benchmark +# Docs: https://www.elastic.co/guide/en/beats/metricbeat/main/metricbeat-module-benchmark.html + +- module: benchmark + metricsets: + - info + enabled: false + period: 10s +