Compare commits
No commits in common. "empty" and "master" have entirely different histories.
40 changed files with 3330 additions and 2 deletions
21
.forgejo/workflows/dco.yml
Normal file
21
.forgejo/workflows/dco.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: DCO action
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dco:
|
||||||
|
name: DCO
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Run commit format checker
|
||||||
|
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
||||||
|
with:
|
||||||
|
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
73
.forgejo/workflows/tests.yml
Normal file
73
.forgejo/workflows/tests.yml
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
name: Tests and linters
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install linters
|
||||||
|
run: make lint-install
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
run: make lint
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go_versions: [ '1.21', '1.22' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '${{ matrix.go_versions }}'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
tests-race:
|
||||||
|
name: Tests with -race
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test ./... -count=1 -race
|
||||||
|
|
||||||
|
staticcheck:
|
||||||
|
name: Staticcheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install staticcheck
|
||||||
|
run: make staticcheck-install
|
||||||
|
|
||||||
|
- name: Run staticcheck
|
||||||
|
run: make staticcheck-run
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/**/*.pb.go -diff -merge
|
||||||
|
/**/*.pb.go linguist-generated=true
|
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# IDE
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Vendoring
|
||||||
|
vendor
|
||||||
|
|
||||||
|
# tempfiles
|
||||||
|
.DS_Store
|
||||||
|
*~
|
||||||
|
.cache
|
||||||
|
|
||||||
|
temp
|
||||||
|
tmp
|
||||||
|
|
||||||
|
# binary
|
||||||
|
bin/
|
||||||
|
release/
|
||||||
|
|
||||||
|
# coverage
|
||||||
|
coverage.txt
|
||||||
|
coverage.html
|
||||||
|
|
||||||
|
# testing
|
||||||
|
cmd/test
|
||||||
|
/plugins/
|
||||||
|
testfile
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.neofs-cli.yml
|
||||||
|
|
||||||
|
# debhelpers
|
||||||
|
debian/*debhelper*
|
||||||
|
|
||||||
|
# logfiles
|
||||||
|
debian/*.log
|
||||||
|
|
||||||
|
# .substvars
|
||||||
|
debian/*.substvars
|
||||||
|
|
||||||
|
# .bash-completion
|
||||||
|
debian/*.bash-completion
|
||||||
|
|
||||||
|
# Install folders and files
|
||||||
|
debian/frostfs-cli/
|
||||||
|
debian/frostfs-ir/
|
||||||
|
debian/files
|
||||||
|
debian/frostfs-storage/
|
||||||
|
debian/changelog
|
||||||
|
man/
|
||||||
|
debs/
|
66
.golangci.yml
Normal file
66
.golangci.yml
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# This file contains all available configuration options
|
||||||
|
# with their default values.
|
||||||
|
|
||||||
|
# options for analysis running
|
||||||
|
run:
|
||||||
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
|
timeout: 10m
|
||||||
|
|
||||||
|
# include test files or not, default is true
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
# output configuration options
|
||||||
|
output:
|
||||||
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
|
format: tab
|
||||||
|
|
||||||
|
# all available settings of specific linters
|
||||||
|
linters-settings:
|
||||||
|
exhaustive:
|
||||||
|
# indicates that switch statements are to be considered exhaustive if a
|
||||||
|
# 'default' case is present, even if all enum members aren't listed in the
|
||||||
|
# switch
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
govet:
|
||||||
|
# report about shadowed variables
|
||||||
|
check-shadowing: false
|
||||||
|
staticcheck:
|
||||||
|
checks: ["all"]
|
||||||
|
funlen:
|
||||||
|
lines: 80 # default 60
|
||||||
|
statements: 60 # default 40
|
||||||
|
gocognit:
|
||||||
|
min-complexity: 40 # default 30
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
# mandatory linters
|
||||||
|
- govet
|
||||||
|
- revive
|
||||||
|
|
||||||
|
# some default golangci-lint linters
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- godot
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
|
||||||
|
# extra linters
|
||||||
|
- bidichk
|
||||||
|
- durationcheck
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- misspell
|
||||||
|
- predeclared
|
||||||
|
- reassign
|
||||||
|
- whitespace
|
||||||
|
- containedctx
|
||||||
|
- funlen
|
||||||
|
- gocognit
|
||||||
|
- contextcheck
|
||||||
|
disable-all: true
|
||||||
|
fast: false
|
38
.pre-commit-config.yaml
Normal file
38
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
ci:
|
||||||
|
autofix_prs: false
|
||||||
|
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.4.0
|
||||||
|
hooks:
|
||||||
|
- id: check-added-large-files
|
||||||
|
- id: check-case-conflict
|
||||||
|
- id: check-executables-have-shebangs
|
||||||
|
- id: check-shebang-scripts-are-executable
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: check-json
|
||||||
|
- id: check-xml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: trailing-whitespace
|
||||||
|
args: [--markdown-linebreak-ext=md]
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
exclude: ".key$"
|
||||||
|
|
||||||
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
|
rev: v0.9.0.2
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
||||||
|
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v1.59.1
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: go-unit-tests
|
||||||
|
name: go unit tests
|
||||||
|
entry: make test GOFLAGS=''
|
||||||
|
pass_filenames: false
|
||||||
|
types: [go]
|
||||||
|
language: system
|
201
LICENSE
Normal file
201
LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
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.
|
64
Makefile
Executable file
64
Makefile
Executable file
|
@ -0,0 +1,64 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
SHELL = bash
|
||||||
|
|
||||||
|
TRUECLOUDLAB_LINT_VERSION ?= 0.0.3
|
||||||
|
TMP_DIR := .cache
|
||||||
|
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
|
||||||
|
LINT_VERSION ?= 1.59.1
|
||||||
|
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
||||||
|
|
||||||
|
# Run all code formatters
|
||||||
|
fmts: fmt imports
|
||||||
|
|
||||||
|
# Reformat code
|
||||||
|
fmt:
|
||||||
|
@echo "⇒ Processing gofmt check"
|
||||||
|
@gofumpt -s -w .
|
||||||
|
|
||||||
|
# Reformat imports
|
||||||
|
imports:
|
||||||
|
@echo "⇒ Processing goimports check"
|
||||||
|
@goimports -w .
|
||||||
|
|
||||||
|
# Run Unit Test with go test
|
||||||
|
test: GOFLAGS ?= "-count=1"
|
||||||
|
test:
|
||||||
|
@echo "⇒ Running go test"
|
||||||
|
@GOFLAGS="$(GOFLAGS)" go test ./...
|
||||||
|
|
||||||
|
# Activate pre-commit hooks
|
||||||
|
pre-commit:
|
||||||
|
pre-commit install -t pre-commit -t commit-msg
|
||||||
|
|
||||||
|
# Deactivate pre-commit hooks
|
||||||
|
unpre-commit:
|
||||||
|
pre-commit uninstall -t pre-commit -t commit-msg
|
||||||
|
|
||||||
|
pre-commit-run:
|
||||||
|
@pre-commit run -a --hook-stage manual
|
||||||
|
|
||||||
|
# Install linters
|
||||||
|
lint-install:
|
||||||
|
@mkdir -p $(TMP_DIR)
|
||||||
|
@rm -rf $(TMP_DIR)/linters
|
||||||
|
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
|
||||||
|
@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
|
||||||
|
@rm -rf $(TMP_DIR)/linters
|
||||||
|
@rmdir $(TMP_DIR) 2>/dev/null || true
|
||||||
|
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
|
||||||
|
|
||||||
|
# Run linters
|
||||||
|
lint:
|
||||||
|
@if [ ! -d "$(LINT_DIR)" ]; then \
|
||||||
|
echo "Run make lint-install"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@$(LINT_DIR)/golangci-lint run
|
||||||
|
|
||||||
|
# Install staticcheck
|
||||||
|
staticcheck-install:
|
||||||
|
@go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
|
||||||
|
# Run staticcheck
|
||||||
|
staticcheck-run:
|
||||||
|
@staticcheck ./...
|
23
README.md
23
README.md
|
@ -1,3 +1,22 @@
|
||||||
# WIP area: this repo is just a fork!
|
# Helper Go libraries for working with metrics, traces and logging
|
||||||
|
|
||||||
Useful things may be published only in [other branches](../../../branches)
|
See package documentation
|
||||||
|
at [pkg.go.dev](https://pkg.go.dev/git.frostfs.info/TrueCloudLab/frostfs-observability)
|
||||||
|
|
||||||
|
## License and copyright
|
||||||
|
|
||||||
|
Copyright 2023-2024 FrostFS contributors
|
||||||
|
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
```
|
||||||
|
|
47
go.mod
Normal file
47
go.mod
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
module git.frostfs.info/TrueCloudLab/frostfs-observability
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0
|
||||||
|
github.com/prometheus/client_golang v1.15.1
|
||||||
|
github.com/prometheus/client_model v0.3.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
go.opentelemetry.io/otel v1.28.0
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0
|
||||||
|
google.golang.org/grpc v1.64.0
|
||||||
|
google.golang.org/protobuf v1.34.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.4 // indirect
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/common v0.42.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.9.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||||
|
go.uber.org/zap v1.26.0
|
||||||
|
golang.org/x/net v0.26.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/text v0.16.0 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
458
go.sum
Normal file
458
go.sum
Normal file
|
@ -0,0 +1,458 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||||
|
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||||
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||||
|
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||||
|
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||||
|
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||||
|
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||||
|
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||||
|
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||||
|
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||||
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
|
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
|
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||||
|
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||||
|
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||||
|
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||||
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
|
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||||
|
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||||
|
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||||
|
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||||
|
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||||
|
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||||
|
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
|
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||||
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
|
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||||
|
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
|
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||||
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
|
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||||
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0 h1:mdLirNAJBxnGgyB6pjZLcs6ue/6eZGBui6gXspfq4ks=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0/go.mod h1:kdXbOySqcQeTxiqglW7aahTmWZy3Pgi6SYL36yvKeyA=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 h1:o95KDiV/b1xdkumY5YbLR0/n2+wBxUpgf3HgfKgTyLI=
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3/go.mod h1:hTxjzRcX49ogbTGVJ1sM5mz5s+SSgiGIyL3jjPxl32E=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
|
||||||
|
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||||
|
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||||
|
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||||
|
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||||
|
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||||
|
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||||
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0 h1:R3X6ZXmNPRR8ul6i3WgFURCHzaXjHdm0karRG/+dj3s=
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.28.0/go.mod h1:QWFXnDavXWwMx2EEcZsf3yxgEKAqsxQ+Syjp+seyInw=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0 h1:EVSnY9JbEEW92bEkIYOVMw4q1WJxIAGoFTrtYOzWuRQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.28.0/go.mod h1:Ea1N1QQryNXpCD0I1fdLibBAIpQuBkznMmkdKrapk1Y=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||||
|
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||||
|
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/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||||
|
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||||
|
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||||
|
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||||
|
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||||
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||||
|
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||||
|
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||||
|
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||||
|
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||||
|
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||||
|
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
|
golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||||
|
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||||
|
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
|
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||||
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||||
|
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||||
|
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
|
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||||
|
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||||
|
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||||
|
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||||
|
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||||
|
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||||
|
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||||
|
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
|
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||||
|
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||||
|
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||||
|
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
|
||||||
|
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||||
|
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||||
|
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||||
|
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
|
||||||
|
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
|
||||||
|
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
|
||||||
|
google.golang.org/grpc/examples v0.0.0-20210424002626-9572fd6faeae/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
|
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
67
logging/lokicore/core.go
Normal file
67
logging/lokicore/core.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package lokicore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/logging/lokicore/loki"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Zap Core for loki.
|
||||||
|
// Expands the zapcore.Core interface with calls to export logs to Loki.
|
||||||
|
type LokiCore struct {
|
||||||
|
original zapcore.Core
|
||||||
|
encoder zapcore.Encoder
|
||||||
|
loki *loki.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(original zapcore.Core, lokiCfg loki.Config) *LokiCore {
|
||||||
|
encoderConfig := zap.NewProductionEncoderConfig()
|
||||||
|
encoder := zapcore.NewJSONEncoder(encoderConfig)
|
||||||
|
encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339Nano)
|
||||||
|
|
||||||
|
return &LokiCore{
|
||||||
|
original: original,
|
||||||
|
encoder: encoder,
|
||||||
|
loki: loki.Setup(lokiCfg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LokiCore) With(fields []zapcore.Field) zapcore.Core {
|
||||||
|
return &LokiCore{
|
||||||
|
original: c.original.With(fields),
|
||||||
|
encoder: c.encoder,
|
||||||
|
loki: c.loki,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LokiCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
|
||||||
|
if err := c.original.Write(entry, fields); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer, err := c.encoder.EncodeEntry(entry, fields)
|
||||||
|
defer buffer.Free()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.loki.Send(buffer.String(), entry.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LokiCore) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
|
||||||
|
if c.Enabled(entry.Level) {
|
||||||
|
return checked.AddCore(entry, c)
|
||||||
|
}
|
||||||
|
return checked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LokiCore) Sync() error {
|
||||||
|
return c.original.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LokiCore) Enabled(level zapcore.Level) bool {
|
||||||
|
return c.original.Enabled(level)
|
||||||
|
}
|
32
logging/lokicore/loki/README.md
Normal file
32
logging/lokicore/loki/README.md
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# git.frostfs.info/TrueCloudLab/frostfs-observability/loki"
|
||||||
|
|
||||||
|
A simple asynchronous client in Go for sending logs to Loki.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/logging/lokicore/loki"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
loki := loki.Setup(loki.Config{
|
||||||
|
Address: "localhost:3100/api/prom/push",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"label": "test",
|
||||||
|
},
|
||||||
|
BatchWait: 1000,
|
||||||
|
BatchEntriesNumber: 200,
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
defer loki.Shutdown()
|
||||||
|
|
||||||
|
loki.Send("log message", time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
50
logging/lokicore/loki/example/main.go
Normal file
50
logging/lokicore/loki/example/main.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/logging/lokicore/loki"
|
||||||
|
)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
const (
|
||||||
|
countMsgGroup = 100
|
||||||
|
countMsg = 500000
|
||||||
|
)
|
||||||
|
|
||||||
|
func send(loki *loki.Client) {
|
||||||
|
wg.Add(1)
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
for j := 0; j < countMsg/countMsgGroup; j++ {
|
||||||
|
for i := 0; i < countMsgGroup; i++ {
|
||||||
|
err := loki.Send(strconv.Itoa(j)+" "+strconv.Itoa(i)+" test log message", time.Now())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "send: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
loki := loki.Setup(loki.Config{
|
||||||
|
Endpoint: "localhost:3100/api/prom/push",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"label": "test",
|
||||||
|
},
|
||||||
|
BatchWait: 1000,
|
||||||
|
BatchEntriesNumber: 200,
|
||||||
|
Enabled: true,
|
||||||
|
})
|
||||||
|
go send(loki)
|
||||||
|
send(loki)
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
loki.Shutdown()
|
||||||
|
}
|
108
logging/lokicore/loki/log.go
Normal file
108
logging/lokicore/loki/log.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send sends the message to the loki server.
|
||||||
|
// If the client is disabled, it returns error.
|
||||||
|
// If the entries channel is full, the message is discarded and returns error.
|
||||||
|
func (client *Client) Send(msg string, timestamp time.Time) error {
|
||||||
|
if !client.IsEnabled() {
|
||||||
|
client.missedMessages++
|
||||||
|
return errors.New("client disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.addToEntries(timestamp, msg) {
|
||||||
|
if client.missedMessages > 0 {
|
||||||
|
client.addToEntries(time.Now(), strconv.FormatInt(client.missedMessages, 10)+" messages missed")
|
||||||
|
client.missedMessages = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.missedMessages++
|
||||||
|
return errors.New("channel is full")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addToEntries attempts to add a log entry to the entries channel.
|
||||||
|
// It returns true if the entry was added successfully, and false otherwise.
|
||||||
|
func (client *Client) addToEntries(timestamp time.Time, msg string) bool {
|
||||||
|
select {
|
||||||
|
case client.entries <- logEntry{
|
||||||
|
Ts: timestamp,
|
||||||
|
Line: msg,
|
||||||
|
}:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run manages the sending of log batches to Loki.
|
||||||
|
// It collects log entries into batches and sends them either when a batch is full,
|
||||||
|
// or when the maximum wait time has elapsed.
|
||||||
|
func (client *Client) run() {
|
||||||
|
if !client.IsEnabled() {
|
||||||
|
client.drainEntries()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.waitGroup.Add(1)
|
||||||
|
defer client.waitGroup.Done()
|
||||||
|
|
||||||
|
var batch []logEntry
|
||||||
|
maxWait := time.NewTimer(client.config.BatchWait)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-client.quit:
|
||||||
|
for len(client.entries) > 0 {
|
||||||
|
entry := <-client.entries
|
||||||
|
batch = append(batch, entry)
|
||||||
|
}
|
||||||
|
batch = client.processMissedMessages(batch)
|
||||||
|
client.processBatch(batch)
|
||||||
|
return
|
||||||
|
|
||||||
|
case entry := <-client.entries:
|
||||||
|
batch = append(batch, entry)
|
||||||
|
if len(batch) >= client.config.BatchEntriesNumber {
|
||||||
|
batch = client.processBatch(batch)
|
||||||
|
maxWait.Reset(client.config.BatchWait)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-maxWait.C:
|
||||||
|
batch = client.processBatch(batch)
|
||||||
|
maxWait.Reset(client.config.BatchWait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) drainEntries() {
|
||||||
|
for len(client.entries) > 0 {
|
||||||
|
<-client.entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) processBatch(batch []logEntry) []logEntry {
|
||||||
|
if len(batch) > 0 {
|
||||||
|
client.sendLogs(batch)
|
||||||
|
batch = batch[:0]
|
||||||
|
}
|
||||||
|
return batch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) processMissedMessages(batch []logEntry) []logEntry {
|
||||||
|
if client.missedMessages > 0 {
|
||||||
|
batch = append(batch, logEntry{
|
||||||
|
Ts: time.Now(),
|
||||||
|
Line: strconv.FormatInt(client.missedMessages, 10) + " messages missed",
|
||||||
|
})
|
||||||
|
client.missedMessages = 0
|
||||||
|
}
|
||||||
|
return batch
|
||||||
|
}
|
66
logging/lokicore/loki/send.go
Normal file
66
logging/lokicore/loki/send.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (client *Client) sendLogs(entries []logEntry) {
|
||||||
|
if len(entries) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var streams []stream
|
||||||
|
stream := stream{
|
||||||
|
Labels: client.config.Labels,
|
||||||
|
Entries: entries,
|
||||||
|
}
|
||||||
|
streams = append(streams, stream)
|
||||||
|
|
||||||
|
msg := promtailMsg{Streams: streams}
|
||||||
|
jsonMsg, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client.mutex.RLock()
|
||||||
|
endpoint := client.config.Endpoint
|
||||||
|
client.mutex.RUnlock()
|
||||||
|
|
||||||
|
client.sendRequest("POST", endpoint, "application/json", jsonMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) sendRequest(method, url string, ctype string, reqBody []byte) {
|
||||||
|
req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", ctype)
|
||||||
|
|
||||||
|
resp, _ := client.client.Do(req)
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *stream) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
Labels string `json:"labels"`
|
||||||
|
Entries []logEntry `json:"entries"`
|
||||||
|
}{
|
||||||
|
Labels: p.Labels.String(),
|
||||||
|
Entries: p.Entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l labels) String() string {
|
||||||
|
var labelPairs []string
|
||||||
|
for key, value := range l {
|
||||||
|
labelPairs = append(labelPairs, fmt.Sprintf(`%s="%s"`, key, value))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("{%s}", strings.Join(labelPairs, ","))
|
||||||
|
}
|
96
logging/lokicore/loki/setup.go
Normal file
96
logging/lokicore/loki/setup.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package loki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const logEntriesChanSize = 5000
|
||||||
|
|
||||||
|
// Represents a single log entry.
|
||||||
|
type logEntry struct {
|
||||||
|
Ts time.Time `json:"ts"`
|
||||||
|
Line string `json:"line"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type labels map[string]string
|
||||||
|
|
||||||
|
// Stream represents a stream of log entries with associated labels.
|
||||||
|
type stream struct {
|
||||||
|
Labels labels `json:"-"`
|
||||||
|
Entries []logEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type promtailMsg struct {
|
||||||
|
Streams []stream `json:"streams"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is a client for sending log entries.
|
||||||
|
type Client struct {
|
||||||
|
config Config
|
||||||
|
quit chan struct{}
|
||||||
|
entries chan logEntry
|
||||||
|
waitGroup sync.WaitGroup
|
||||||
|
client http.Client
|
||||||
|
missedMessages int64
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool
|
||||||
|
// E.g. localhost:3100/api/prom/push.
|
||||||
|
Endpoint string
|
||||||
|
Labels map[string]string
|
||||||
|
// Maximum message buffering time.
|
||||||
|
BatchWait time.Duration
|
||||||
|
// Maximum number of messages in the queue.
|
||||||
|
BatchEntriesNumber int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup initializes the client with the given configuration and starts the processing goroutine.
|
||||||
|
// It is the caller's responsibility to call Shutdown() to free resources.
|
||||||
|
func Setup(conf Config) *Client {
|
||||||
|
client := newClient()
|
||||||
|
client.config = conf
|
||||||
|
client.config.Endpoint = normalizeURL(client.config.Endpoint)
|
||||||
|
go client.run()
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops the client and waits for all logs to be sent.
|
||||||
|
func (client *Client) Shutdown() {
|
||||||
|
client.mutex.Lock()
|
||||||
|
client.config.Enabled = false
|
||||||
|
client.mutex.Unlock()
|
||||||
|
|
||||||
|
close(client.quit)
|
||||||
|
client.waitGroup.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnabled checks whether the client is enabled.
|
||||||
|
func (client *Client) IsEnabled() bool {
|
||||||
|
client.mutex.RLock()
|
||||||
|
defer client.mutex.RUnlock()
|
||||||
|
return client.config.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClient() *Client {
|
||||||
|
return &Client{
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
entries: make(chan logEntry, logEntriesChanSize),
|
||||||
|
client: http.Client{},
|
||||||
|
config: Config{Enabled: false},
|
||||||
|
mutex: sync.RWMutex{},
|
||||||
|
waitGroup: sync.WaitGroup{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeURL(host string) string {
|
||||||
|
if !regexp.MustCompile(`^https?:\/\/`).MatchString(host) {
|
||||||
|
host = "http://" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
return host
|
||||||
|
}
|
105
metrics/desc.go
Normal file
105
metrics/desc.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Descriptions contains metric Description suitable for further processing.
|
||||||
|
// The only reason for it to exist is `prometheus.Desc` disallowing field access directly.
|
||||||
|
// https://github.com/prometheus/client_golang/pull/326
|
||||||
|
// https://github.com/prometheus/client_golang/issues/516
|
||||||
|
// https://github.com/prometheus/client_golang/issues/222
|
||||||
|
type Description struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Help string `json:"help"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ConstantLabels prometheus.Labels `json:"constant_labels,omitempty"`
|
||||||
|
VariableLabels []string `json:"variable_labels,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGauge returns new registered prometheus.Gauge.
|
||||||
|
func NewGauge(opts prometheus.GaugeOpts) prometheus.Gauge {
|
||||||
|
value := prometheus.NewGauge(opts)
|
||||||
|
MustRegister(value, Description{
|
||||||
|
Name: prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
Type: dto.MetricType_GAUGE.String(),
|
||||||
|
Help: opts.Help,
|
||||||
|
ConstantLabels: opts.ConstLabels,
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGaugeVec returns new registered *prometheus.GaugeVec.
|
||||||
|
func NewGaugeVec(opts prometheus.GaugeOpts, labelNames []string) *prometheus.GaugeVec {
|
||||||
|
value := prometheus.NewGaugeVec(opts, labelNames)
|
||||||
|
MustRegister(value, Description{
|
||||||
|
Name: prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
Type: dto.MetricType_GAUGE.String(),
|
||||||
|
Help: opts.Help,
|
||||||
|
ConstantLabels: opts.ConstLabels,
|
||||||
|
VariableLabels: labelNames,
|
||||||
|
})
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGaugeFunc returns new registered prometheus.GaugeFunc.
|
||||||
|
func NewGaugeFunc(opts prometheus.GaugeOpts, f func() float64) prometheus.GaugeFunc {
|
||||||
|
value := prometheus.NewGaugeFunc(opts, f)
|
||||||
|
MustRegister(value, Description{
|
||||||
|
Name: prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
Type: dto.MetricType_GAUGE.String(),
|
||||||
|
Help: opts.Help,
|
||||||
|
ConstantLabels: opts.ConstLabels,
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounter returns new registered prometheus.Counter.
|
||||||
|
func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
|
||||||
|
value := prometheus.NewCounter(opts)
|
||||||
|
MustRegister(value, Description{
|
||||||
|
Name: prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: opts.Help,
|
||||||
|
ConstantLabels: opts.ConstLabels,
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounterVec returns new registered *prometheus.CounterVec.
|
||||||
|
func NewCounterVec(opts prometheus.CounterOpts, labels []string) *prometheus.CounterVec {
|
||||||
|
value := prometheus.NewCounterVec(opts, labels)
|
||||||
|
MustRegister(value, Description{
|
||||||
|
Name: prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: opts.Help,
|
||||||
|
ConstantLabels: opts.ConstLabels,
|
||||||
|
VariableLabels: labels,
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHistogramVec returns new registered *prometheus.HistogramVec.
|
||||||
|
func NewHistogramVec(opts prometheus.HistogramOpts, labelNames []string) *prometheus.HistogramVec {
|
||||||
|
value := prometheus.NewHistogramVec(opts, labelNames)
|
||||||
|
MustRegister(value, Description{
|
||||||
|
Name: prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
|
||||||
|
Type: dto.MetricType_HISTOGRAM.String(),
|
||||||
|
Help: opts.Help,
|
||||||
|
ConstantLabels: opts.ConstLabels,
|
||||||
|
VariableLabels: labelNames,
|
||||||
|
})
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescribeAll returns descriptions for all registered metrics.
|
||||||
|
func DescribeAll() []Description {
|
||||||
|
registeredDescriptionsMtx.Lock()
|
||||||
|
defer registeredDescriptionsMtx.Unlock()
|
||||||
|
|
||||||
|
ds := make([]Description, len(registeredDescriptions))
|
||||||
|
copy(ds, registeredDescriptions)
|
||||||
|
return ds
|
||||||
|
}
|
64
metrics/desc_test.go
Normal file
64
metrics/desc_test.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDescribeAll(t *testing.T) {
|
||||||
|
const (
|
||||||
|
namespace = "my_ns"
|
||||||
|
subsystem = "mysub"
|
||||||
|
)
|
||||||
|
NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "my_counter",
|
||||||
|
})
|
||||||
|
|
||||||
|
labels := []string{"label1", "label2"}
|
||||||
|
NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "my_gauge",
|
||||||
|
}, labels)
|
||||||
|
|
||||||
|
constLabels := prometheus.Labels{
|
||||||
|
"const1": "abc",
|
||||||
|
"const2": "xyz",
|
||||||
|
}
|
||||||
|
NewCounter(prometheus.CounterOpts{
|
||||||
|
Namespace: namespace,
|
||||||
|
Subsystem: subsystem,
|
||||||
|
Name: "with_const_labels",
|
||||||
|
ConstLabels: constLabels,
|
||||||
|
})
|
||||||
|
|
||||||
|
descriptions := DescribeAll()
|
||||||
|
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for i := range descriptions {
|
||||||
|
if !strings.HasPrefix(descriptions[i].Name, namespace) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.False(t, seen[descriptions[i].Name], "metric %s was seen twice", descriptions[i].Name)
|
||||||
|
seen[descriptions[i].Name] = true
|
||||||
|
|
||||||
|
switch descriptions[i].Name {
|
||||||
|
case prometheus.BuildFQName(namespace, subsystem, "my_counter"):
|
||||||
|
require.True(t, len(descriptions[i].VariableLabels) == 0)
|
||||||
|
case prometheus.BuildFQName(namespace, subsystem, "my_gauge"):
|
||||||
|
require.Equal(t, labels, descriptions[i].VariableLabels)
|
||||||
|
case prometheus.BuildFQName(namespace, subsystem, "with_const_labels"):
|
||||||
|
require.Equal(t, len(constLabels), len(descriptions[i].ConstantLabels))
|
||||||
|
require.Equal(t, constLabels, descriptions[i].ConstantLabels)
|
||||||
|
default:
|
||||||
|
require.FailNow(t, "unexpected metric name: %s", descriptions[i].Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 3, len(seen), "not all registered metrics were iterated over")
|
||||||
|
}
|
72
metrics/grpc/client.go
Normal file
72
metrics/grpc/client.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/metrics"
|
||||||
|
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientMetrics = grpcprom.NewClientMetrics(
|
||||||
|
grpcprom.WithClientHandlingTimeHistogram(
|
||||||
|
grpcprom.WithHistogramBuckets(prometheus.DefBuckets),
|
||||||
|
),
|
||||||
|
grpcprom.WithClientStreamRecvHistogram(
|
||||||
|
grpcprom.WithHistogramBuckets(prometheus.DefBuckets),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Description copied from repository of grpc-ecosystem
|
||||||
|
// https://github.com/grpc-ecosystem/go-grpc-middleware/blob/71d7422112b1d7fadd4b8bf12a6f33ba6d22e98e/providers/prometheus/client_metrics.go#L31
|
||||||
|
descs := []metrics.Description{
|
||||||
|
{
|
||||||
|
Name: "grpc_client_started_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of RPCs started on the client.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_client_handled_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of RPCs completed by the client, regardless of success or failure.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_client_msg_received_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of RPC stream messages received by the client.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_client_msg_sent_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of gRPC stream messages sent by the client.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_client_handling_seconds",
|
||||||
|
Type: dto.MetricType_HISTOGRAM.String(),
|
||||||
|
Help: "Histogram of response latency (seconds) of the gRPC until it is finished by the application.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_client_msg_recv_handling_seconds",
|
||||||
|
Type: dto.MetricType_HISTOGRAM.String(),
|
||||||
|
Help: "Histogram of response latency (seconds) of the gRPC single message receive.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
metrics.MustRegister(clientMetrics, descs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnaryClientInterceptor returns client interceptor to collect metrics from unary RPCs.
|
||||||
|
func NewUnaryClientInterceptor() grpc.UnaryClientInterceptor {
|
||||||
|
return clientMetrics.UnaryClientInterceptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamClientInterceptor returns client interceptor to collect metrics from stream RPCs.
|
||||||
|
func NewStreamClientInterceptor() grpc.StreamClientInterceptor {
|
||||||
|
return clientMetrics.StreamClientInterceptor()
|
||||||
|
}
|
63
metrics/grpc/server.go
Normal file
63
metrics/grpc/server.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/metrics"
|
||||||
|
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
dto "github.com/prometheus/client_model/go"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serverMetrics = grpcprom.NewServerMetrics(
|
||||||
|
grpcprom.WithServerHandlingTimeHistogram(
|
||||||
|
grpcprom.WithHistogramBuckets(prometheus.DefBuckets),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Description copied from grpc-ecosystem:
|
||||||
|
// https://github.com/grpc-ecosystem/go-grpc-middleware/blob/71d7422112b1d7fadd4b8bf12a6f33ba6d22e98e/providers/prometheus/server_metrics.go#L26
|
||||||
|
descs := []metrics.Description{
|
||||||
|
{
|
||||||
|
Name: "grpc_server_started_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of RPCs started on the server.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_server_handled_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of RPCs completed on the server, regardless of success or failure.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method", "grpc_code"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_server_msg_received_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of RPC stream messages received on the server.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_server_msg_sent_total",
|
||||||
|
Type: dto.MetricType_COUNTER.String(),
|
||||||
|
Help: "Total number of gRPC stream messages sent by the server.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "grpc_server_handling_seconds",
|
||||||
|
Type: dto.MetricType_HISTOGRAM.String(),
|
||||||
|
Help: "Histogram of response latency (seconds) of gRPC that had been application-level handled by the server.",
|
||||||
|
VariableLabels: []string{"grpc_type", "grpc_service", "grpc_method"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
metrics.MustRegister(serverMetrics, descs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnaryServerInterceptor returns server interceptor to collect metrics from unary RPCs.
|
||||||
|
func NewUnaryServerInterceptor() grpc.UnaryServerInterceptor {
|
||||||
|
return serverMetrics.UnaryServerInterceptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamServerInterceptor returns server interceptor to collect metrics from stream RPCs.
|
||||||
|
func NewStreamServerInterceptor() grpc.StreamServerInterceptor {
|
||||||
|
return serverMetrics.StreamServerInterceptor()
|
||||||
|
}
|
44
metrics/registry.go
Normal file
44
metrics/registry.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package metrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
registry = prometheus.NewRegistry()
|
||||||
|
// registeredDescriptionsMtx protects collectors slice.
|
||||||
|
// It should not be acessed concurrently, but we can easily forget this in future, thus this mutex.
|
||||||
|
registeredDescriptionsMtx sync.Mutex
|
||||||
|
registeredDescriptions []Description
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
|
||||||
|
registry.MustRegister(collectors.NewGoCollector())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register registers custom collectors to registry.
|
||||||
|
// Should be used with metrics from other packages.
|
||||||
|
func Register(customCollectors ...prometheus.Collector) {
|
||||||
|
registry.MustRegister(customCollectors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustRegister(c prometheus.Collector, descs ...Description) {
|
||||||
|
registry.MustRegister(c)
|
||||||
|
registeredDescriptionsMtx.Lock()
|
||||||
|
defer registeredDescriptionsMtx.Unlock()
|
||||||
|
registeredDescriptions = append(registeredDescriptions, descs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler returns an http.Handler for the local registry.
|
||||||
|
func Handler() http.Handler {
|
||||||
|
promhttp.Handler()
|
||||||
|
return promhttp.InstrumentMetricHandler(
|
||||||
|
registry,
|
||||||
|
promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
|
||||||
|
}
|
0
testdata/tracing/invalid_empty_root_ca.pem
vendored
Normal file
0
testdata/tracing/invalid_empty_root_ca.pem
vendored
Normal file
1
testdata/tracing/invalid_root_ca.pem
vendored
Normal file
1
testdata/tracing/invalid_root_ca.pem
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
invalid content
|
12
testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem
vendored
Normal file
12
testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD
|
||||||
|
VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh
|
||||||
|
bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw
|
||||||
|
MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g
|
||||||
|
UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT
|
||||||
|
BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx
|
||||||
|
uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV
|
||||||
|
HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/
|
||||||
|
+wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147
|
||||||
|
bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm
|
||||||
|
-----END CERTIFICATE-----
|
13
testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem
vendored
Normal file
13
testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD
|
||||||
|
VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG
|
||||||
|
A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw
|
||||||
|
WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz
|
||||||
|
IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi
|
||||||
|
AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi
|
||||||
|
QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR
|
||||||
|
HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
|
||||||
|
BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D
|
||||||
|
9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8
|
||||||
|
p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD
|
||||||
|
-----END CERTIFICATE-----
|
90
tracing/config.go
Normal file
90
tracing/config.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Exporter is type of tracing target.
|
||||||
|
type Exporter string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StdoutExporter Exporter = "stdout"
|
||||||
|
OTLPgRPCExporter Exporter = "otlp_grpc"
|
||||||
|
NoOpExporter Exporter = "noop"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Enabled is true, if tracing enabled.
|
||||||
|
Enabled bool
|
||||||
|
// Exporter is collector type.
|
||||||
|
Exporter Exporter
|
||||||
|
// Endpoint is collector endpoint for OTLP exporters.
|
||||||
|
Endpoint string
|
||||||
|
// ServerCaCertPool is cert pool of the remote server CA certificate. Use for TLS setup.
|
||||||
|
ServerCaCertPool *x509.CertPool
|
||||||
|
|
||||||
|
// Service is service name that will be used in tracing.
|
||||||
|
// Mandatory.
|
||||||
|
Service string
|
||||||
|
// InstanceID is identity of service instance.
|
||||||
|
// Optional.
|
||||||
|
InstanceID string
|
||||||
|
// Version is version of service instance.
|
||||||
|
// Optional.
|
||||||
|
Version string
|
||||||
|
// Attributes is KV list of attributes.
|
||||||
|
// Optional.
|
||||||
|
Attributes map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) validate() error {
|
||||||
|
if !c.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Exporter != StdoutExporter && c.Exporter != OTLPgRPCExporter && c.Exporter != NoOpExporter {
|
||||||
|
return fmt.Errorf("tracing config error: unknown exporter '%s', valid values are %v",
|
||||||
|
c.Exporter, []string{string(StdoutExporter), string(OTLPgRPCExporter), string(NoOpExporter)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Service) == 0 {
|
||||||
|
return fmt.Errorf("tracing config error: service name must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Exporter == OTLPgRPCExporter && len(c.Endpoint) == 0 {
|
||||||
|
return fmt.Errorf("tracing config error: exporter '%s' requires endpoint", c.Exporter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) hasChange(other *Config) bool {
|
||||||
|
if !c.Enabled && !other.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.Enabled != other.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.Exporter == StdoutExporter && other.Exporter == StdoutExporter) ||
|
||||||
|
(c.Exporter == NoOpExporter && other.Exporter == NoOpExporter) {
|
||||||
|
return !c.serviceInfoEqual(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
if other.Exporter == OTLPgRPCExporter && !c.ServerCaCertPool.Equal(other.ServerCaCertPool) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Exporter != other.Exporter ||
|
||||||
|
c.Endpoint != other.Endpoint ||
|
||||||
|
!c.serviceInfoEqual(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) serviceInfoEqual(other *Config) bool {
|
||||||
|
return c.Service == other.Service &&
|
||||||
|
c.InstanceID == other.InstanceID &&
|
||||||
|
c.Version == other.Version &&
|
||||||
|
maps.Equal(c.Attributes, other.Attributes)
|
||||||
|
}
|
278
tracing/config_test.go
Normal file
278
tracing/config_test.go
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_validate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "disabled",
|
||||||
|
wantErr: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stdout",
|
||||||
|
wantErr: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: StdoutExporter,
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noop",
|
||||||
|
wantErr: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: StdoutExporter,
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OTLP gRPC",
|
||||||
|
wantErr: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Service: "test",
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown exporter",
|
||||||
|
wantErr: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: "unknown",
|
||||||
|
Service: "test",
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no exporter",
|
||||||
|
wantErr: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Service: "test",
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no service",
|
||||||
|
wantErr: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no endpoint for grpc",
|
||||||
|
wantErr: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v0.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if err := tt.config.validate(); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Config.validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_hasChange(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
other Config
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "disabled configs always equal",
|
||||||
|
want: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: false,
|
||||||
|
Exporter: StdoutExporter,
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: false,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "enabled",
|
||||||
|
want: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: false,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled",
|
||||||
|
want: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: false,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not use endpoint for stdout",
|
||||||
|
want: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: StdoutExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: StdoutExporter,
|
||||||
|
Endpoint: "otherhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not use endpoint for noop",
|
||||||
|
want: false,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: NoOpExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: NoOpExporter,
|
||||||
|
Endpoint: "otherhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use endpoint for grpc",
|
||||||
|
want: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "otherhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "use tls root ca certificate for grpc",
|
||||||
|
want: true,
|
||||||
|
config: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ServerCaCertPool: readCertPoolByPath(t, "../testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem"),
|
||||||
|
},
|
||||||
|
other: Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: OTLPgRPCExporter,
|
||||||
|
Endpoint: "localhost:4717",
|
||||||
|
Service: "test",
|
||||||
|
InstanceID: "s01",
|
||||||
|
Version: "v1.0.0",
|
||||||
|
ServerCaCertPool: readCertPoolByPath(t, "../testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := tt.config.hasChange(&tt.other); got != tt.want {
|
||||||
|
t.Errorf("Config.equal() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCertPoolByPath(t *testing.T, path string) *x509.CertPool {
|
||||||
|
ca, err := os.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
ok := roots.AppendCertsFromPEM(ca)
|
||||||
|
require.True(t, ok)
|
||||||
|
return roots
|
||||||
|
}
|
107
tracing/examples/grpc/main.go
Normal file
107
tracing/examples/grpc/main.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
||||||
|
srv "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/examples/grpc/server"
|
||||||
|
tracing_grpc "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/grpc"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
srv.UnimplementedServerServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) Echo(ctx context.Context, req *srv.Request) (*srv.Response, error) {
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
if !sc.TraceID().IsValid() || !sc.SpanID().IsValid() {
|
||||||
|
return nil, fmt.Errorf("no trace id or span id on server side")
|
||||||
|
}
|
||||||
|
log.Printf("server trace id: %v", sc.TraceID())
|
||||||
|
log.Printf("server span id: %v", sc.SpanID())
|
||||||
|
return &srv.Response{
|
||||||
|
Value: req.GetValue(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyClientTraceID(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
if !sc.TraceID().IsValid() || !sc.SpanID().IsValid() {
|
||||||
|
return fmt.Errorf("no trace id or span id on client side")
|
||||||
|
}
|
||||||
|
log.Printf("client trace id: %v", sc.TraceID())
|
||||||
|
log.Printf("client span id: %v", sc.SpanID())
|
||||||
|
return invoker(ctx, method, req, reply, cc, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
tracingCfg := tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.NoOpExporter,
|
||||||
|
Service: "example-grpc",
|
||||||
|
}
|
||||||
|
enabled, err := tracing.Setup(ctx, tracingCfg)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to setup tracing: %v", err)
|
||||||
|
}
|
||||||
|
if !enabled {
|
||||||
|
log.Fatalf("failed to enable tracing")
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", ":7000")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
s := grpc.NewServer(
|
||||||
|
grpc.ChainStreamInterceptor(tracing_grpc.NewStreamServerInterceptor()),
|
||||||
|
grpc.ChainUnaryInterceptor(tracing_grpc.NewUnaryServerInterceptor()),
|
||||||
|
)
|
||||||
|
srv.RegisterServerServer(s, &server{})
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
cc, err := grpc.NewClient(":7000",
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithChainUnaryInterceptor(
|
||||||
|
tracing_grpc.NewUnaryClientInteceptor(),
|
||||||
|
verifyClientTraceID,
|
||||||
|
),
|
||||||
|
grpc.WithChainStreamInterceptor(
|
||||||
|
tracing_grpc.NewStreamClientInterceptor(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to dial: %v", err)
|
||||||
|
}
|
||||||
|
client := srv.NewServerClient(cc)
|
||||||
|
resp, err := client.Echo(ctx, &srv.Request{
|
||||||
|
Value: "Hello!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to get response: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("response received: %s", resp.GetValue())
|
||||||
|
|
||||||
|
s.GracefulStop()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
BIN
tracing/examples/grpc/server/server.pb.go
generated
Normal file
BIN
tracing/examples/grpc/server/server.pb.go
generated
Normal file
Binary file not shown.
17
tracing/examples/grpc/server/server.proto
Normal file
17
tracing/examples/grpc/server/server.proto
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package server;
|
||||||
|
|
||||||
|
option go_package = "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/example_grpc/server";
|
||||||
|
|
||||||
|
service Server {
|
||||||
|
rpc Echo(Request) returns (Response);
|
||||||
|
}
|
||||||
|
|
||||||
|
message Request {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Response {
|
||||||
|
string value = 1;
|
||||||
|
}
|
29
tracing/grpc/carrier.go
Normal file
29
tracing/grpc/carrier.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type grpcMetadataCarrier struct {
|
||||||
|
md *metadata.MD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcMetadataCarrier) Get(key string) string {
|
||||||
|
values := c.md.Get(key)
|
||||||
|
if len(values) > 0 {
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcMetadataCarrier) Set(key string, value string) {
|
||||||
|
c.md.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcMetadataCarrier) Keys() []string {
|
||||||
|
result := make([]string, 0, c.md.Len())
|
||||||
|
for key := range *c.md {
|
||||||
|
result = append(result, key)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
95
tracing/grpc/client.go
Normal file
95
tracing/grpc/client.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientStream struct {
|
||||||
|
originalStream grpc.ClientStream
|
||||||
|
desc *grpc.StreamDesc
|
||||||
|
span trace.Span
|
||||||
|
finished chan<- error
|
||||||
|
done <-chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newgRPCClientStream(originalStream grpc.ClientStream, desc *grpc.StreamDesc, span trace.Span, finished chan<- error, done <-chan struct{}) grpc.ClientStream {
|
||||||
|
return &clientStream{
|
||||||
|
originalStream: originalStream,
|
||||||
|
desc: desc,
|
||||||
|
span: span,
|
||||||
|
finished: finished,
|
||||||
|
done: done,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *clientStream) Header() (metadata.MD, error) {
|
||||||
|
md, err := cs.originalStream.Header()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-cs.done:
|
||||||
|
case cs.finished <- err:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return md, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *clientStream) Trailer() metadata.MD {
|
||||||
|
return cs.originalStream.Trailer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *clientStream) CloseSend() error {
|
||||||
|
cs.span.AddEvent("client.stream.close.send.start")
|
||||||
|
err := cs.originalStream.CloseSend()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-cs.done:
|
||||||
|
case cs.finished <- err:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cs.span.AddEvent("client.stream.close.send.finish")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *clientStream) Context() context.Context {
|
||||||
|
return cs.originalStream.Context()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *clientStream) SendMsg(m any) error {
|
||||||
|
cs.span.AddEvent("client.stream.send.msg.start", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
err := cs.originalStream.SendMsg(m)
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-cs.done:
|
||||||
|
case cs.finished <- err:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cs.span.AddEvent("client.stream.send.msg.finish", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *clientStream) RecvMsg(m any) error {
|
||||||
|
cs.span.AddEvent("client.stream.receive.msg.start", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
err := cs.originalStream.RecvMsg(m)
|
||||||
|
if err != nil || !cs.desc.ServerStreams {
|
||||||
|
select {
|
||||||
|
case <-cs.done:
|
||||||
|
case cs.finished <- err:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cs.span.AddEvent("client.stream.receive.msg.finish", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
160
tracing/grpc/interceptors.go
Normal file
160
tracing/grpc/interceptors.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
grpc_codes "google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUnaryClientInteceptor creates new gRPC unary interceptor to save gRPC client traces.
|
||||||
|
func NewUnaryClientInteceptor() grpc.UnaryClientInterceptor {
|
||||||
|
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||||
|
ctx, span := startClientSpan(ctx, cc, method)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
err := invoker(ctx, method, req, reply, cc, opts...)
|
||||||
|
if err != nil {
|
||||||
|
grpcStatus, _ := status.FromError(err)
|
||||||
|
span.SetStatus(codes.Error, grpcStatus.Message())
|
||||||
|
span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int64(int64(grpcStatus.Code())))
|
||||||
|
} else {
|
||||||
|
span.SetStatus(codes.Ok, "")
|
||||||
|
span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int64(int64(grpc_codes.OK)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamClientInterceptor creates new gRPC stream interceptor to save gRPC client traces.
|
||||||
|
func NewStreamClientInterceptor() grpc.StreamClientInterceptor {
|
||||||
|
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||||
|
ctx, span := startClientSpan(ctx, cc, method)
|
||||||
|
str, err := streamer(ctx, desc, cc, method, opts...)
|
||||||
|
if err != nil {
|
||||||
|
grpcStatus, _ := status.FromError(err)
|
||||||
|
span.SetStatus(codes.Error, grpcStatus.Message())
|
||||||
|
span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int64(int64(grpcStatus.Code())))
|
||||||
|
span.End()
|
||||||
|
return str, err
|
||||||
|
}
|
||||||
|
|
||||||
|
finished := make(chan error)
|
||||||
|
done := make(chan struct{})
|
||||||
|
strWrp := newgRPCClientStream(str, desc, span, finished, done)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-finished:
|
||||||
|
if err == nil || err == io.EOF {
|
||||||
|
setGRPCSpanStatus(span, nil)
|
||||||
|
} else {
|
||||||
|
setGRPCSpanStatus(span, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
setGRPCSpanStatus(span, ctx.Err())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return strWrp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnaryServerInterceptor creates new gRPC unary interceptor to save gRPC server traces.
|
||||||
|
func NewUnaryServerInterceptor() grpc.UnaryServerInterceptor {
|
||||||
|
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||||
|
ctx = extractGRPCTraceInfo(ctx)
|
||||||
|
var span trace.Span
|
||||||
|
ctx, span = tracing.StartSpanFromContext(ctx, info.FullMethod,
|
||||||
|
trace.WithAttributes(
|
||||||
|
semconv.RPCSystemGRPC,
|
||||||
|
semconv.RPCMethod(info.FullMethod),
|
||||||
|
),
|
||||||
|
trace.WithSpanKind(trace.SpanKindServer))
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
resp, err = handler(ctx, req)
|
||||||
|
|
||||||
|
setGRPCSpanStatus(span, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStreamServerInterceptor creates new gRPC stream interceptor to save gRPC server traces.
|
||||||
|
func NewStreamServerInterceptor() grpc.StreamServerInterceptor {
|
||||||
|
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||||
|
ctx := extractGRPCTraceInfo(ss.Context())
|
||||||
|
var span trace.Span
|
||||||
|
ctx, span = tracing.StartSpanFromContext(ctx, info.FullMethod,
|
||||||
|
trace.WithAttributes(
|
||||||
|
semconv.RPCSystemGRPC,
|
||||||
|
semconv.RPCMethod(info.FullMethod),
|
||||||
|
),
|
||||||
|
trace.WithSpanKind(trace.SpanKindServer))
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
err := handler(srv, newgRPCServerStream(ctx, ss, span))
|
||||||
|
|
||||||
|
setGRPCSpanStatus(span, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startClientSpan(ctx context.Context, cc *grpc.ClientConn, method string) (context.Context, trace.Span) {
|
||||||
|
ctx, span := tracing.StartSpanFromContext(ctx, method, trace.WithAttributes(
|
||||||
|
semconv.RPCSystemGRPC,
|
||||||
|
semconv.RPCMethod(method),
|
||||||
|
attribute.String("rpc.grpc.target", cc.Target())),
|
||||||
|
trace.WithSpanKind(trace.SpanKindClient),
|
||||||
|
)
|
||||||
|
ctx = setGRPCTraceInfo(ctx)
|
||||||
|
return ctx, span
|
||||||
|
}
|
||||||
|
|
||||||
|
func setGRPCSpanStatus(span trace.Span, err error) {
|
||||||
|
if err != nil {
|
||||||
|
grpcStatus, _ := status.FromError(err)
|
||||||
|
span.SetStatus(codes.Error, grpcStatus.Message())
|
||||||
|
span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int64(int64(grpcStatus.Code())))
|
||||||
|
} else {
|
||||||
|
span.SetStatus(codes.Ok, "")
|
||||||
|
span.SetAttributes(semconv.RPCGRPCStatusCodeKey.Int64(int64(grpc_codes.OK)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractGRPCTraceInfo(ctx context.Context) context.Context {
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
carrier := &grpcMetadataCarrier{
|
||||||
|
md: &md,
|
||||||
|
}
|
||||||
|
return tracing.Propagator.Extract(ctx, carrier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setGRPCTraceInfo(ctx context.Context) context.Context {
|
||||||
|
md, ok := metadata.FromOutgoingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
md = metadata.MD{}
|
||||||
|
}
|
||||||
|
carrier := &grpcMetadataCarrier{
|
||||||
|
md: &md,
|
||||||
|
}
|
||||||
|
tracing.Propagator.Inject(ctx, carrier)
|
||||||
|
return metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
63
tracing/grpc/server.go
Normal file
63
tracing/grpc/server.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package grpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverStream struct {
|
||||||
|
originalStream grpc.ServerStream
|
||||||
|
ctx context.Context // nolint:containedctx
|
||||||
|
span trace.Span
|
||||||
|
}
|
||||||
|
|
||||||
|
func newgRPCServerStream(ctx context.Context, originalStream grpc.ServerStream, span trace.Span) grpc.ServerStream {
|
||||||
|
return &serverStream{
|
||||||
|
originalStream: originalStream,
|
||||||
|
ctx: ctx,
|
||||||
|
span: span,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *serverStream) SetHeader(md metadata.MD) error {
|
||||||
|
return ss.originalStream.SendHeader(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *serverStream) SendHeader(md metadata.MD) error {
|
||||||
|
return ss.originalStream.SendHeader(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *serverStream) SetTrailer(md metadata.MD) {
|
||||||
|
ss.originalStream.SetTrailer(md)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *serverStream) Context() context.Context {
|
||||||
|
return ss.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *serverStream) SendMsg(m any) error {
|
||||||
|
ss.span.AddEvent("server.stream.send.msg.start", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
err := ss.originalStream.SendMsg(m)
|
||||||
|
ss.span.AddEvent("server.stream.send.msg.finish", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *serverStream) RecvMsg(m any) error {
|
||||||
|
ss.span.AddEvent("server.stream.receive.msg.start", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
err := ss.originalStream.RecvMsg(m)
|
||||||
|
ss.span.AddEvent("server.stream.receive.msg.finish", trace.WithAttributes(
|
||||||
|
attribute.String("message.type", fmt.Sprintf("%T", m))),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
94
tracing/propagator.go
Normal file
94
tracing/propagator.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
traceIDHeader = "x-frostfs-trace-id"
|
||||||
|
spanIDHeader = "x-frostfs-span-id"
|
||||||
|
flagsHeader = "x-frostfs-trace-flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagsSampled = 1 << iota
|
||||||
|
)
|
||||||
|
|
||||||
|
// propagator serializes SpanContext to/from headers.
|
||||||
|
// x-frostfs-trace-id - TraceID, 16 bytes, hex-string (32 bytes).
|
||||||
|
// x-frostfs-span-id - SpanID, 8 bytes, hexstring (16 bytes).
|
||||||
|
// x-frostfs-trace-flags - trace flags (now sampled only).
|
||||||
|
type propagator struct{}
|
||||||
|
|
||||||
|
// Propagator is propagation.TextMapPropagator instance, used to extract/inject trace info from/to remote context.
|
||||||
|
var Propagator propagation.TextMapPropagator = &propagator{}
|
||||||
|
|
||||||
|
// Inject injects tracing info to carrier.
|
||||||
|
func (p *propagator) Inject(ctx context.Context, carrier propagation.TextMapCarrier) {
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
if !sc.TraceID().IsValid() || !sc.SpanID().IsValid() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags int
|
||||||
|
if sc.IsSampled() {
|
||||||
|
flags |= flagsSampled
|
||||||
|
}
|
||||||
|
|
||||||
|
carrier.Set(traceIDHeader, sc.TraceID().String())
|
||||||
|
carrier.Set(spanIDHeader, sc.SpanID().String())
|
||||||
|
carrier.Set(flagsHeader, fmt.Sprintf("%x", flags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract extracts tracing info from carrier and returns context with tracing info.
|
||||||
|
// In case of error returns ctx.
|
||||||
|
func (p *propagator) Extract(ctx context.Context, carrier propagation.TextMapCarrier) context.Context {
|
||||||
|
spanConfig := trace.SpanContextConfig{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
traceIDStr := carrier.Get(traceIDHeader)
|
||||||
|
traceIDDefined := traceIDStr != ""
|
||||||
|
if traceIDDefined {
|
||||||
|
spanConfig.TraceID, err = trace.TraceIDFromHex(traceIDStr)
|
||||||
|
if err != nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spanIDstr := carrier.Get(spanIDHeader)
|
||||||
|
spanIDDefined := spanIDstr != ""
|
||||||
|
if spanIDDefined {
|
||||||
|
spanConfig.SpanID, err = trace.SpanIDFromHex(spanIDstr)
|
||||||
|
if err != nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if traceIDDefined != spanIDDefined {
|
||||||
|
return ctx // traceID + spanID must be defined OR no traceID and no spanID
|
||||||
|
}
|
||||||
|
|
||||||
|
flagsStr := carrier.Get(flagsHeader)
|
||||||
|
if flagsStr != "" {
|
||||||
|
var v int64
|
||||||
|
v, err = strconv.ParseInt(flagsStr, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
if v&flagsSampled == flagsSampled {
|
||||||
|
spanConfig.TraceFlags = trace.FlagsSampled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trace.ContextWithRemoteSpanContext(ctx, trace.NewSpanContext(spanConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns the keys whose values are set with Inject.
|
||||||
|
func (p *propagator) Fields() []string {
|
||||||
|
return []string{traceIDHeader, spanIDHeader, flagsHeader}
|
||||||
|
}
|
256
tracing/propagator_test.go
Normal file
256
tracing/propagator_test.go
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testCarrier struct {
|
||||||
|
Values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testCarrier) Get(key string) string {
|
||||||
|
return c.Values[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testCarrier) Set(key string, value string) {
|
||||||
|
c.Values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testCarrier) Keys() []string {
|
||||||
|
res := make([]string, 0, len(c.Values))
|
||||||
|
for k := range c.Values {
|
||||||
|
res = append(res, k)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
var p = &propagator{}
|
||||||
|
|
||||||
|
func TestPropagator_Inject(t *testing.T) {
|
||||||
|
t.Run("injects trace_id and span_id if valid", func(t *testing.T) {
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
|
||||||
|
spanConfig := trace.SpanContextConfig{}
|
||||||
|
spanConfig.TraceID, _ = trace.TraceIDFromHex(traceIDHex)
|
||||||
|
spanConfig.SpanID, _ = trace.SpanIDFromHex(spanIDHex)
|
||||||
|
spanConfig.TraceFlags = trace.FlagsSampled
|
||||||
|
|
||||||
|
ctx := trace.ContextWithRemoteSpanContext(context.Background(), trace.NewSpanContext(spanConfig))
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
p.Inject(ctx, c)
|
||||||
|
|
||||||
|
require.Equal(t, 3, len(c.Values), "not all headers were saved")
|
||||||
|
require.Equal(t, traceIDHex, c.Values[traceIDHeader], "unexpected trace id")
|
||||||
|
require.Equal(t, spanIDHex, c.Values[spanIDHeader], "unexpected span id")
|
||||||
|
require.Equal(t, "1", c.Values[flagsHeader], "unexpected flags")
|
||||||
|
})
|
||||||
|
t.Run("doesn't injects if trace_id is invalid", func(t *testing.T) {
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
|
||||||
|
spanConfig := trace.SpanContextConfig{}
|
||||||
|
spanConfig.TraceID, _ = trace.TraceIDFromHex(traceIDHex)
|
||||||
|
spanConfig.SpanID, _ = trace.SpanIDFromHex(spanIDHex)
|
||||||
|
spanConfig.TraceFlags = trace.FlagsSampled
|
||||||
|
|
||||||
|
ctx := trace.ContextWithRemoteSpanContext(context.Background(), trace.NewSpanContext(spanConfig))
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
p.Inject(ctx, c)
|
||||||
|
|
||||||
|
require.Equal(t, 0, len(c.Values), "some headers were saved")
|
||||||
|
})
|
||||||
|
t.Run("doesn't injects if span_id is invalid", func(t *testing.T) {
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
|
||||||
|
spanConfig := trace.SpanContextConfig{}
|
||||||
|
spanConfig.TraceID, _ = trace.TraceIDFromHex(traceIDHex)
|
||||||
|
spanConfig.SpanID, _ = trace.SpanIDFromHex(spanIDHex)
|
||||||
|
spanConfig.TraceFlags = trace.FlagsSampled
|
||||||
|
|
||||||
|
ctx := trace.ContextWithRemoteSpanContext(context.Background(), trace.NewSpanContext(spanConfig))
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
p.Inject(ctx, c)
|
||||||
|
|
||||||
|
require.Equal(t, 0, len(c.Values), "some headers were saved")
|
||||||
|
})
|
||||||
|
t.Run("injects flags if no flags specified", func(t *testing.T) {
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
|
||||||
|
spanConfig := trace.SpanContextConfig{}
|
||||||
|
spanConfig.TraceID, _ = trace.TraceIDFromHex(traceIDHex)
|
||||||
|
spanConfig.SpanID, _ = trace.SpanIDFromHex(spanIDHex)
|
||||||
|
|
||||||
|
ctx := trace.ContextWithRemoteSpanContext(context.Background(), trace.NewSpanContext(spanConfig))
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
p.Inject(ctx, c)
|
||||||
|
|
||||||
|
require.Equal(t, 3, len(c.Values), "not all headers were saved")
|
||||||
|
require.Equal(t, traceIDHex, c.Values[traceIDHeader], "unexpected trace id")
|
||||||
|
require.Equal(t, spanIDHex, c.Values[spanIDHeader], "unexpected span id")
|
||||||
|
require.Equal(t, "0", c.Values[flagsHeader], "unexpected flags")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPropagator_Extract(t *testing.T) {
|
||||||
|
t.Run("extracts if set", func(t *testing.T) {
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
c.Values[traceIDHeader] = traceIDHex
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
c.Values[spanIDHeader] = spanIDHex
|
||||||
|
|
||||||
|
c.Values[flagsHeader] = "1"
|
||||||
|
|
||||||
|
ctx := p.Extract(context.Background(), c)
|
||||||
|
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
require.True(t, sc.HasTraceID(), "trace_id was not set")
|
||||||
|
require.Equal(t, traceIDHex, sc.TraceID().String(), "trace_id doesn't match")
|
||||||
|
require.True(t, sc.HasSpanID(), "span_id was not set")
|
||||||
|
require.Equal(t, spanIDHex, sc.SpanID().String(), "span_id doesn't match")
|
||||||
|
require.True(t, sc.IsSampled(), "sampled was not set")
|
||||||
|
})
|
||||||
|
t.Run("not extracts if only trace_id defined", func(t *testing.T) {
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
c.Values[traceIDHeader] = traceIDHex
|
||||||
|
c.Values[flagsHeader] = "1"
|
||||||
|
|
||||||
|
ctx := p.Extract(context.Background(), c)
|
||||||
|
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
require.False(t, sc.HasTraceID(), "trace_id was set")
|
||||||
|
require.False(t, sc.HasSpanID(), "span_id was set")
|
||||||
|
require.False(t, sc.IsSampled(), "sampled was set")
|
||||||
|
})
|
||||||
|
t.Run("not extracts if only span_id defined", func(t *testing.T) {
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
c.Values[spanIDHeader] = spanIDHex
|
||||||
|
c.Values[flagsHeader] = "1"
|
||||||
|
|
||||||
|
ctx := p.Extract(context.Background(), c)
|
||||||
|
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
require.False(t, sc.HasTraceID(), "trace_id was set")
|
||||||
|
require.False(t, sc.HasSpanID(), "span_id was set")
|
||||||
|
require.False(t, sc.IsSampled(), "sampled was set")
|
||||||
|
})
|
||||||
|
t.Run("not extracts if trace_id is in invalid", func(t *testing.T) {
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Values[traceIDHeader] = "loren ipsum"
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
c.Values[spanIDHeader] = spanIDHex
|
||||||
|
c.Values[flagsHeader] = "1"
|
||||||
|
|
||||||
|
ctx := p.Extract(context.Background(), c)
|
||||||
|
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
require.False(t, sc.HasTraceID(), "trace_id was set")
|
||||||
|
require.False(t, sc.HasSpanID(), "span_id was set")
|
||||||
|
require.False(t, sc.IsSampled(), "sampled was set")
|
||||||
|
})
|
||||||
|
t.Run("not extracts if span_id is invalid", func(t *testing.T) {
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Values[spanIDHeader] = "loren ipsum"
|
||||||
|
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
c.Values[traceIDHeader] = traceIDHex
|
||||||
|
c.Values[flagsHeader] = "1"
|
||||||
|
|
||||||
|
ctx := p.Extract(context.Background(), c)
|
||||||
|
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
require.False(t, sc.HasTraceID(), "trace_id was set")
|
||||||
|
require.False(t, sc.HasSpanID(), "span_id was set")
|
||||||
|
require.False(t, sc.IsSampled(), "sampled was set")
|
||||||
|
})
|
||||||
|
t.Run("not extracts if flags is invalid", func(t *testing.T) {
|
||||||
|
c := &testCarrier{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
traceIDBytes := make([]byte, 16)
|
||||||
|
rand.Read(traceIDBytes)
|
||||||
|
traceIDHex := hex.EncodeToString(traceIDBytes)
|
||||||
|
c.Values[traceIDHeader] = traceIDHex
|
||||||
|
|
||||||
|
spanIDBytes := make([]byte, 8)
|
||||||
|
rand.Read(spanIDBytes)
|
||||||
|
spanIDHex := hex.EncodeToString(spanIDBytes)
|
||||||
|
c.Values[spanIDHeader] = spanIDHex
|
||||||
|
|
||||||
|
c.Values[flagsHeader] = "loren ipsum"
|
||||||
|
|
||||||
|
ctx := p.Extract(context.Background(), c)
|
||||||
|
|
||||||
|
sc := trace.SpanFromContext(ctx).SpanContext()
|
||||||
|
require.False(t, sc.HasTraceID(), "trace_id was set")
|
||||||
|
require.False(t, sc.HasSpanID(), "span_id was set")
|
||||||
|
require.False(t, sc.IsSampled(), "sampled was set")
|
||||||
|
})
|
||||||
|
}
|
176
tracing/setup.go
Normal file
176
tracing/setup.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
|
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||||
|
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
"go.opentelemetry.io/otel/trace/noop"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrEmptyServerRootCaPool indicates that cert pool is empty and does not have any certificates inside.
|
||||||
|
var ErrEmptyServerRootCaPool = errors.New("empty server root ca cert pool")
|
||||||
|
|
||||||
|
var (
|
||||||
|
// tracingLock protects provider, done, config and tracer from concurrent update.
|
||||||
|
// These fields change when the config is updated or the application is shutdown.
|
||||||
|
tracingLock = &sync.Mutex{}
|
||||||
|
|
||||||
|
provider *sdktrace.TracerProvider
|
||||||
|
done bool
|
||||||
|
|
||||||
|
config = Config{}
|
||||||
|
tracer = getDefaultTracer()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup initializes global tracer.
|
||||||
|
// Returns true if global tracer was updated.
|
||||||
|
// Shutdown method must be called for graceful shutdown.
|
||||||
|
func Setup(ctx context.Context, cfg Config) (bool, error) {
|
||||||
|
if err := cfg.validate(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tracingLock.Lock()
|
||||||
|
defer tracingLock.Unlock()
|
||||||
|
|
||||||
|
if done {
|
||||||
|
return false, fmt.Errorf("failed to setup tracing: already shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !config.hasChange(&cfg) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Enabled {
|
||||||
|
config = cfg
|
||||||
|
tracer.Store(&tracerHolder{Tracer: noop.NewTracerProvider().Tracer("")})
|
||||||
|
return true, flushAndShutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, err := getExporter(ctx, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prevProvider := provider
|
||||||
|
|
||||||
|
provider = sdktrace.NewTracerProvider(
|
||||||
|
sdktrace.WithBatcher(exp),
|
||||||
|
sdktrace.WithResource(newResource(&cfg)),
|
||||||
|
)
|
||||||
|
|
||||||
|
config = cfg
|
||||||
|
tracer.Store(&tracerHolder{Tracer: provider.Tracer(cfg.Service)})
|
||||||
|
|
||||||
|
var retErr error
|
||||||
|
if prevProvider != nil {
|
||||||
|
retErr = prevProvider.ForceFlush(ctx)
|
||||||
|
if err := prevProvider.Shutdown(ctx); err != nil {
|
||||||
|
if retErr == nil {
|
||||||
|
retErr = err
|
||||||
|
} else {
|
||||||
|
retErr = fmt.Errorf("%v ; %v", retErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown shutdowns tracing.
|
||||||
|
func Shutdown(ctx context.Context) error {
|
||||||
|
tracingLock.Lock()
|
||||||
|
defer tracingLock.Unlock()
|
||||||
|
|
||||||
|
if done {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
done = true
|
||||||
|
|
||||||
|
config = Config{}
|
||||||
|
tracer.Store(&tracerHolder{Tracer: noop.NewTracerProvider().Tracer("")})
|
||||||
|
|
||||||
|
return flushAndShutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDefaultTracer() *atomic.Pointer[tracerHolder] {
|
||||||
|
v := new(atomic.Pointer[tracerHolder])
|
||||||
|
v.Store(&tracerHolder{Tracer: noop.NewTracerProvider().Tracer("")})
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func flushAndShutdown(ctx context.Context) error {
|
||||||
|
if provider == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp := provider
|
||||||
|
provider = nil
|
||||||
|
var retErr error
|
||||||
|
retErr = tmp.ForceFlush(ctx)
|
||||||
|
if err := tmp.Shutdown(ctx); err != nil {
|
||||||
|
if retErr == nil {
|
||||||
|
retErr = err
|
||||||
|
} else {
|
||||||
|
retErr = fmt.Errorf("%v ; %v", retErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExporter(ctx context.Context, cfg *Config) (sdktrace.SpanExporter, error) {
|
||||||
|
switch cfg.Exporter {
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("failed to setup tracing: unknown tracing exporter (%s)", cfg.Exporter)
|
||||||
|
case StdoutExporter:
|
||||||
|
return stdouttrace.New()
|
||||||
|
case NoOpExporter:
|
||||||
|
return tracetest.NewNoopExporter(), nil
|
||||||
|
case OTLPgRPCExporter:
|
||||||
|
securityOption := otlptracegrpc.WithInsecure()
|
||||||
|
if cfg.ServerCaCertPool != nil {
|
||||||
|
if cfg.ServerCaCertPool.Equal(x509.NewCertPool()) {
|
||||||
|
return nil, fmt.Errorf("failed to setup tracing: %w", ErrEmptyServerRootCaPool)
|
||||||
|
}
|
||||||
|
securityOption = otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(cfg.ServerCaCertPool, ""))
|
||||||
|
}
|
||||||
|
return otlptracegrpc.New(ctx, otlptracegrpc.WithEndpoint(cfg.Endpoint), securityOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResource(cfg *Config) *resource.Resource {
|
||||||
|
attrs := []attribute.KeyValue{
|
||||||
|
semconv.ServiceName(cfg.Service),
|
||||||
|
}
|
||||||
|
if len(cfg.Version) > 0 {
|
||||||
|
attrs = append(attrs, semconv.ServiceVersion(cfg.Version))
|
||||||
|
}
|
||||||
|
if len(cfg.InstanceID) > 0 {
|
||||||
|
attrs = append(attrs, semconv.ServiceInstanceID(cfg.InstanceID))
|
||||||
|
}
|
||||||
|
for k, v := range cfg.Attributes {
|
||||||
|
attrs = append(attrs, attribute.String(k, v))
|
||||||
|
}
|
||||||
|
return resource.NewWithAttributes(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
attrs...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tracerHolder struct {
|
||||||
|
Tracer trace.Tracer
|
||||||
|
}
|
118
tracing/setup_test.go
Normal file
118
tracing/setup_test.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package tracing_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config tracing.Config
|
||||||
|
want bool
|
||||||
|
expErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "setup stdout exporter",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.StdoutExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
expErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup noop exporter",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.NoOpExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
},
|
||||||
|
|
||||||
|
want: true,
|
||||||
|
expErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup otlp_grpc insecure exporter",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.OTLPgRPCExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
Endpoint: "test-endpoint.com:4317",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
expErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup otlp_grpc secure exporter with valid rsa root ca certificate",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.OTLPgRPCExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
Endpoint: "test-endpoint.com:4317",
|
||||||
|
ServerCaCertPool: readCertPoolByPath(t, "../testdata/tracing/valid_google_globalsign_r4_rsa_root_ca.pem"),
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
expErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup otlp_grpc secure exporter with valid ecdsa root ca certificate",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.OTLPgRPCExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
Endpoint: "test-endpoint.com:4317",
|
||||||
|
ServerCaCertPool: readCertPoolByPath(t, "../testdata/tracing/valid_google_gts_r4_ecdsa_root_ca.pem"),
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
expErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup otlp_grpc secure exporter with invalid root ca certificate",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.OTLPgRPCExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
Endpoint: "test-endpoint.com:4317",
|
||||||
|
ServerCaCertPool: readCertPoolByPath(t, "../testdata/tracing/invalid_root_ca.pem"),
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
expErr: tracing.ErrEmptyServerRootCaPool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "setup otlp_grpc secure exporter with empty root ca certificate",
|
||||||
|
config: tracing.Config{
|
||||||
|
Enabled: true,
|
||||||
|
Exporter: tracing.OTLPgRPCExporter,
|
||||||
|
Service: "service-name",
|
||||||
|
Endpoint: "test-endpoint.com:4317",
|
||||||
|
ServerCaCertPool: readCertPoolByPath(t, "../testdata/tracing/invalid_empty_root_ca.pem"),
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
expErr: tracing.ErrEmptyServerRootCaPool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tracing.Setup(context.Background(), tt.config)
|
||||||
|
require.ErrorIs(t, err, tt.expErr)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("Setup config = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readCertPoolByPath(t *testing.T, path string) *x509.CertPool {
|
||||||
|
ca, err := os.ReadFile(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
_ = roots.AppendCertsFromPEM(ca)
|
||||||
|
return roots
|
||||||
|
}
|
12
tracing/span.go
Normal file
12
tracing/span.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package tracing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartSpanFromContext creates a span and a context.Context containing the newly-created span.
|
||||||
|
func StartSpanFromContext(ctx context.Context, operationName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
|
||||||
|
return tracer.Load().Tracer.Start(ctx, operationName, opts...)
|
||||||
|
}
|
Loading…
Reference in a new issue