├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── lint-test.yml │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── examples ├── param │ ├── docker-compose.yml │ └── param.js ├── shared │ └── collector-config.yaml └── template │ ├── docker-compose.yml │ └── template.js ├── go.mod ├── go.sum ├── golangci.yml ├── pkg ├── random │ ├── random.go │ └── random_test.go ├── tracegen │ ├── parameterized.go │ ├── parameterized_test.go │ ├── templated.go │ ├── templated_test.go │ └── tracegen.go └── util │ └── maps.go └── tracing.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /examples/**/*.yml 2 | /examples/**/*.yaml 3 | *.md 4 | Dockerfile 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: 'gomod' 7 | directory: '/' 8 | schedule: 9 | interval: 'weekly' 10 | 11 | - package-ecosystem: 'docker' 12 | directory: '/' 13 | schedule: 14 | interval: 'weekly' 15 | 16 | - package-ecosystem: 'github-actions' 17 | # Workflow files stored in the 18 | # default location of `.github/workflows` 19 | directory: '/' 20 | schedule: 21 | interval: 'weekly' 22 | -------------------------------------------------------------------------------- /.github/workflows/lint-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: pull_request 4 | 5 | permissions: {} 6 | 7 | jobs: 8 | lint-and-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 12 | with: 13 | persist-credentials: false 14 | 15 | - name: Set-up Go 16 | uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # v3.6.1 17 | with: 18 | go-version: ^1.22 19 | cache: true 20 | 21 | - name: Format 22 | run: make check-fmt 23 | 24 | - name: Lint 25 | uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 #v6.5.0 26 | with: 27 | version: v1.64.6 28 | args: --config ./golangci.yml 29 | 30 | - name: Test 31 | run: make test 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 15 | with: 16 | persist-credentials: false 17 | 18 | - name: Set-up Go 19 | uses: actions/setup-go@be3c94b385c4f180051c996d336f57a34c397495 # v3.6.1 20 | with: 21 | go-version: ^1.22 22 | cache: false 23 | 24 | - name: Test 25 | run: make test 26 | 27 | - name: Docker set up buildx 28 | uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0 29 | with: 30 | cache-binary: false 31 | 32 | - name: Docker set up qemu 33 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.0 34 | 35 | - name: Docker login 36 | uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.0 37 | with: 38 | registry: ghcr.io 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Docker build and push 43 | uses: docker/build-push-action@1104d471370f9806843c095c1db02b5a90c5f8b6 # v3.0 44 | with: 45 | context: . 46 | file: Dockerfile 47 | platforms: linux/amd64,linux/arm64 48 | push: true 49 | tags: ghcr.io/grafana/xk6-client-tracing:latest,ghcr.io/grafana/xk6-client-tracing:${{ github.event.release.tag_name }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | k6 2 | k6-tracing 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS xk6-client-tracing-build 2 | 3 | RUN apk add --no-cache \ 4 | build-base \ 5 | gcc \ 6 | git \ 7 | make 8 | 9 | RUN go install go.k6.io/xk6/cmd/xk6@latest \ 10 | && wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.64.6 \ 11 | && golangci-lint --version 12 | 13 | WORKDIR /opt/xk6-client-tracing 14 | COPY go.mod go.sum ./ 15 | RUN go mod download 16 | 17 | COPY . . 18 | RUN make build 19 | 20 | FROM alpine:latest 21 | 22 | COPY --from=xk6-client-tracing-build /opt/xk6-client-tracing/k6-tracing /k6-tracing 23 | COPY ./examples/template/template.js /example-script.js 24 | 25 | ENTRYPOINT [ "/k6-tracing" ] 26 | CMD ["run", "/example-script.js"] 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU AFFERO GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 19 November 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU Affero General Public License is a free, copyleft license for 14 | software and other kinds of works, specifically designed to ensure 15 | cooperation with the community in the case of network server software. 16 | 17 | The licenses for most software and other practical works are designed 18 | to take away your freedom to share and change the works. By contrast, 19 | our General Public Licenses are intended to guarantee your freedom to 20 | share and change all versions of a program--to make sure it remains 21 | free software for all its users. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | Developers that use our General Public Licenses protect your rights 31 | with two steps: (1) assert copyright on the software, and (2) offer 32 | you this License which gives you legal permission to copy, distribute 33 | and/or modify the software. 34 | 35 | A secondary benefit of defending all users' freedom is that 36 | improvements made in alternate versions of the program, if they 37 | receive widespread use, become available for other developers to 38 | incorporate. Many developers of free software are heartened and 39 | encouraged by the resulting cooperation. However, in the case of 40 | software used on network servers, this result may fail to come about. 41 | The GNU General Public License permits making a modified version and 42 | letting the public access it on a server without ever releasing its 43 | source code to the public. 44 | 45 | The GNU Affero General Public License is designed specifically to 46 | ensure that, in such cases, the modified source code becomes available 47 | to the community. It requires the operator of a network server to 48 | provide the source code of the modified version running there to the 49 | users of that server. Therefore, public use of a modified version, on 50 | a publicly accessible server, gives the public access to the source 51 | code of the modified version. 52 | 53 | An older license, called the Affero General Public License and 54 | published by Affero, was designed to accomplish similar goals. This is 55 | a different license, not a version of the Affero GPL, but Affero has 56 | released a new version of the Affero GPL which permits relicensing 57 | under this license. 58 | 59 | The precise terms and conditions for copying, distribution and 60 | modification follow. 61 | 62 | ### TERMS AND CONDITIONS 63 | 64 | #### 0. Definitions. 65 | 66 | "This License" refers to version 3 of the GNU Affero General Public 67 | License. 68 | 69 | "Copyright" also means copyright-like laws that apply to other kinds 70 | of works, such as semiconductor masks. 71 | 72 | "The Program" refers to any copyrightable work licensed under this 73 | License. Each licensee is addressed as "you". "Licensees" and 74 | "recipients" may be individuals or organizations. 75 | 76 | To "modify" a work means to copy from or adapt all or part of the work 77 | in a fashion requiring copyright permission, other than the making of 78 | an exact copy. The resulting work is called a "modified version" of 79 | the earlier work or a work "based on" the earlier work. 80 | 81 | A "covered work" means either the unmodified Program or a work based 82 | on the Program. 83 | 84 | To "propagate" a work means to do anything with it that, without 85 | permission, would make you directly or secondarily liable for 86 | infringement under applicable copyright law, except executing it on a 87 | computer or modifying a private copy. Propagation includes copying, 88 | distribution (with or without modification), making available to the 89 | public, and in some countries other activities as well. 90 | 91 | To "convey" a work means any kind of propagation that enables other 92 | parties to make or receive copies. Mere interaction with a user 93 | through a computer network, with no transfer of a copy, is not 94 | conveying. 95 | 96 | An interactive user interface displays "Appropriate Legal Notices" to 97 | the extent that it includes a convenient and prominently visible 98 | feature that (1) displays an appropriate copyright notice, and (2) 99 | tells the user that there is no warranty for the work (except to the 100 | extent that warranties are provided), that licensees may convey the 101 | work under this License, and how to view a copy of this License. If 102 | the interface presents a list of user commands or options, such as a 103 | menu, a prominent item in the list meets this criterion. 104 | 105 | #### 1. Source Code. 106 | 107 | The "source code" for a work means the preferred form of the work for 108 | making modifications to it. "Object code" means any non-source form of 109 | a work. 110 | 111 | A "Standard Interface" means an interface that either is an official 112 | standard defined by a recognized standards body, or, in the case of 113 | interfaces specified for a particular programming language, one that 114 | is widely used among developers working in that language. 115 | 116 | The "System Libraries" of an executable work include anything, other 117 | than the work as a whole, that (a) is included in the normal form of 118 | packaging a Major Component, but which is not part of that Major 119 | Component, and (b) serves only to enable use of the work with that 120 | Major Component, or to implement a Standard Interface for which an 121 | implementation is available to the public in source code form. A 122 | "Major Component", in this context, means a major essential component 123 | (kernel, window system, and so on) of the specific operating system 124 | (if any) on which the executable work runs, or a compiler used to 125 | produce the work, or an object code interpreter used to run it. 126 | 127 | The "Corresponding Source" for a work in object code form means all 128 | the source code needed to generate, install, and (for an executable 129 | work) run the object code and to modify the work, including scripts to 130 | control those activities. However, it does not include the work's 131 | System Libraries, or general-purpose tools or generally available free 132 | programs which are used unmodified in performing those activities but 133 | which are not part of the work. For example, Corresponding Source 134 | includes interface definition files associated with source files for 135 | the work, and the source code for shared libraries and dynamically 136 | linked subprograms that the work is specifically designed to require, 137 | such as by intimate data communication or control flow between those 138 | subprograms and other parts of the work. 139 | 140 | The Corresponding Source need not include anything that users can 141 | regenerate automatically from other parts of the Corresponding Source. 142 | 143 | The Corresponding Source for a work in source code form is that same 144 | work. 145 | 146 | #### 2. Basic Permissions. 147 | 148 | All rights granted under this License are granted for the term of 149 | copyright on the Program, and are irrevocable provided the stated 150 | conditions are met. This License explicitly affirms your unlimited 151 | permission to run the unmodified Program. The output from running a 152 | covered work is covered by this License only if the output, given its 153 | content, constitutes a covered work. This License acknowledges your 154 | rights of fair use or other equivalent, as provided by copyright law. 155 | 156 | You may make, run and propagate covered works that you do not convey, 157 | without conditions so long as your license otherwise remains in force. 158 | You may convey covered works to others for the sole purpose of having 159 | them make modifications exclusively for you, or provide you with 160 | facilities for running those works, provided that you comply with the 161 | terms of this License in conveying all material for which you do not 162 | control copyright. Those thus making or running the covered works for 163 | you must do so exclusively on your behalf, under your direction and 164 | control, on terms that prohibit them from making any copies of your 165 | copyrighted material outside their relationship with you. 166 | 167 | Conveying under any other circumstances is permitted solely under the 168 | conditions stated below. Sublicensing is not allowed; section 10 makes 169 | it unnecessary. 170 | 171 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 172 | 173 | No covered work shall be deemed part of an effective technological 174 | measure under any applicable law fulfilling obligations under article 175 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 176 | similar laws prohibiting or restricting circumvention of such 177 | measures. 178 | 179 | When you convey a covered work, you waive any legal power to forbid 180 | circumvention of technological measures to the extent such 181 | circumvention is effected by exercising rights under this License with 182 | respect to the covered work, and you disclaim any intention to limit 183 | operation or modification of the work as a means of enforcing, against 184 | the work's users, your or third parties' legal rights to forbid 185 | circumvention of technological measures. 186 | 187 | #### 4. Conveying Verbatim Copies. 188 | 189 | You may convey verbatim copies of the Program's source code as you 190 | receive it, in any medium, provided that you conspicuously and 191 | appropriately publish on each copy an appropriate copyright notice; 192 | keep intact all notices stating that this License and any 193 | non-permissive terms added in accord with section 7 apply to the code; 194 | keep intact all notices of the absence of any warranty; and give all 195 | recipients a copy of this License along with the Program. 196 | 197 | You may charge any price or no price for each copy that you convey, 198 | and you may offer support or warranty protection for a fee. 199 | 200 | #### 5. Conveying Modified Source Versions. 201 | 202 | You may convey a work based on the Program, or the modifications to 203 | produce it from the Program, in the form of source code under the 204 | terms of section 4, provided that you also meet all of these 205 | conditions: 206 | 207 | - a) The work must carry prominent notices stating that you modified 208 | it, and giving a relevant date. 209 | - b) The work must carry prominent notices stating that it is 210 | released under this License and any conditions added under 211 | section 7. This requirement modifies the requirement in section 4 212 | to "keep intact all notices". 213 | - c) You must license the entire work, as a whole, under this 214 | License to anyone who comes into possession of a copy. This 215 | License will therefore apply, along with any applicable section 7 216 | additional terms, to the whole of the work, and all its parts, 217 | regardless of how they are packaged. This License gives no 218 | permission to license the work in any other way, but it does not 219 | invalidate such permission if you have separately received it. 220 | - d) If the work has interactive user interfaces, each must display 221 | Appropriate Legal Notices; however, if the Program has interactive 222 | interfaces that do not display Appropriate Legal Notices, your 223 | work need not make them do so. 224 | 225 | A compilation of a covered work with other separate and independent 226 | works, which are not by their nature extensions of the covered work, 227 | and which are not combined with it such as to form a larger program, 228 | in or on a volume of a storage or distribution medium, is called an 229 | "aggregate" if the compilation and its resulting copyright are not 230 | used to limit the access or legal rights of the compilation's users 231 | beyond what the individual works permit. Inclusion of a covered work 232 | in an aggregate does not cause this License to apply to the other 233 | parts of the aggregate. 234 | 235 | #### 6. Conveying Non-Source Forms. 236 | 237 | You may convey a covered work in object code form under the terms of 238 | sections 4 and 5, provided that you also convey the machine-readable 239 | Corresponding Source under the terms of this License, in one of these 240 | ways: 241 | 242 | - a) Convey the object code in, or embodied in, a physical product 243 | (including a physical distribution medium), accompanied by the 244 | Corresponding Source fixed on a durable physical medium 245 | customarily used for software interchange. 246 | - b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the Corresponding 256 | Source from a network server at no charge. 257 | - c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | - d) Convey the object code by offering access from a designated 263 | place (gratis or for a charge), and offer equivalent access to the 264 | Corresponding Source in the same way through the same place at no 265 | further charge. You need not require recipients to copy the 266 | Corresponding Source along with the object code. If the place to 267 | copy the object code is a network server, the Corresponding Source 268 | may be on a different server (operated by you or a third party) 269 | that supports equivalent copying facilities, provided you maintain 270 | clear directions next to the object code saying where to find the 271 | Corresponding Source. Regardless of what server hosts the 272 | Corresponding Source, you remain obligated to ensure that it is 273 | available for as long as needed to satisfy these requirements. 274 | - e) Convey the object code using peer-to-peer transmission, 275 | provided you inform other peers where the object code and 276 | Corresponding Source of the work are being offered to the general 277 | public at no charge under subsection 6d. 278 | 279 | A separable portion of the object code, whose source code is excluded 280 | from the Corresponding Source as a System Library, need not be 281 | included in conveying the object code work. 282 | 283 | A "User Product" is either (1) a "consumer product", which means any 284 | tangible personal property which is normally used for personal, 285 | family, or household purposes, or (2) anything designed or sold for 286 | incorporation into a dwelling. In determining whether a product is a 287 | consumer product, doubtful cases shall be resolved in favor of 288 | coverage. For a particular product received by a particular user, 289 | "normally used" refers to a typical or common use of that class of 290 | product, regardless of the status of the particular user or of the way 291 | in which the particular user actually uses, or expects or is expected 292 | to use, the product. A product is a consumer product regardless of 293 | whether the product has substantial commercial, industrial or 294 | non-consumer uses, unless such uses represent the only significant 295 | mode of use of the product. 296 | 297 | "Installation Information" for a User Product means any methods, 298 | procedures, authorization keys, or other information required to 299 | install and execute modified versions of a covered work in that User 300 | Product from a modified version of its Corresponding Source. The 301 | information must suffice to ensure that the continued functioning of 302 | the modified object code is in no case prevented or interfered with 303 | solely because modification has been made. 304 | 305 | If you convey an object code work under this section in, or with, or 306 | specifically for use in, a User Product, and the conveying occurs as 307 | part of a transaction in which the right of possession and use of the 308 | User Product is transferred to the recipient in perpetuity or for a 309 | fixed term (regardless of how the transaction is characterized), the 310 | Corresponding Source conveyed under this section must be accompanied 311 | by the Installation Information. But this requirement does not apply 312 | if neither you nor any third party retains the ability to install 313 | modified object code on the User Product (for example, the work has 314 | been installed in ROM). 315 | 316 | The requirement to provide Installation Information does not include a 317 | requirement to continue to provide support service, warranty, or 318 | updates for a work that has been modified or installed by the 319 | recipient, or for the User Product in which it has been modified or 320 | installed. Access to a network may be denied when the modification 321 | itself materially and adversely affects the operation of the network 322 | or violates the rules and protocols for communication across the 323 | network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | #### 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders 351 | of that material) supplement the terms of this License with terms: 352 | 353 | - a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | - b) Requiring preservation of specified reasonable legal notices or 356 | author attributions in that material or in the Appropriate Legal 357 | Notices displayed by works containing it; or 358 | - c) Prohibiting misrepresentation of the origin of that material, 359 | or requiring that modified versions of such material be marked in 360 | reasonable ways as different from the original version; or 361 | - d) Limiting the use for publicity purposes of names of licensors 362 | or authors of the material; or 363 | - e) Declining to grant rights under trademark law for use of some 364 | trade names, trademarks, or service marks; or 365 | - f) Requiring indemnification of licensors and authors of that 366 | material by anyone who conveys the material (or modified versions 367 | of it) with contractual assumptions of liability to the recipient, 368 | for any liability that these contractual assumptions directly 369 | impose on those licensors and authors. 370 | 371 | All other non-permissive additional terms are considered "further 372 | restrictions" within the meaning of section 10. If the Program as you 373 | received it, or any part of it, contains a notice stating that it is 374 | governed by this License along with a term that is a further 375 | restriction, you may remove that term. If a license document contains 376 | a further restriction but permits relicensing or conveying under this 377 | License, you may add to a covered work material governed by the terms 378 | of that license document, provided that the further restriction does 379 | not survive such relicensing or conveying. 380 | 381 | If you add terms to a covered work in accord with this section, you 382 | must place, in the relevant source files, a statement of the 383 | additional terms that apply to those files, or a notice indicating 384 | where to find the applicable terms. 385 | 386 | Additional terms, permissive or non-permissive, may be stated in the 387 | form of a separately written license, or stated as exceptions; the 388 | above requirements apply either way. 389 | 390 | #### 8. Termination. 391 | 392 | You may not propagate or modify a covered work except as expressly 393 | provided under this License. Any attempt otherwise to propagate or 394 | modify it is void, and will automatically terminate your rights under 395 | this License (including any patent licenses granted under the third 396 | paragraph of section 11). 397 | 398 | However, if you cease all violation of this License, then your license 399 | from a particular copyright holder is reinstated (a) provisionally, 400 | unless and until the copyright holder explicitly and finally 401 | terminates your license, and (b) permanently, if the copyright holder 402 | fails to notify you of the violation by some reasonable means prior to 403 | 60 days after the cessation. 404 | 405 | Moreover, your license from a particular copyright holder is 406 | reinstated permanently if the copyright holder notifies you of the 407 | violation by some reasonable means, this is the first time you have 408 | received notice of violation of this License (for any work) from that 409 | copyright holder, and you cure the violation prior to 30 days after 410 | your receipt of the notice. 411 | 412 | Termination of your rights under this section does not terminate the 413 | licenses of parties who have received copies or rights from you under 414 | this License. If your rights have been terminated and not permanently 415 | reinstated, you do not qualify to receive new licenses for the same 416 | material under section 10. 417 | 418 | #### 9. Acceptance Not Required for Having Copies. 419 | 420 | You are not required to accept this License in order to receive or run 421 | a copy of the Program. Ancillary propagation of a covered work 422 | occurring solely as a consequence of using peer-to-peer transmission 423 | to receive a copy likewise does not require acceptance. However, 424 | nothing other than this License grants you permission to propagate or 425 | modify any covered work. These actions infringe copyright if you do 426 | not accept this License. Therefore, by modifying or propagating a 427 | covered work, you indicate your acceptance of this License to do so. 428 | 429 | #### 10. Automatic Licensing of Downstream Recipients. 430 | 431 | Each time you convey a covered work, the recipient automatically 432 | receives a license from the original licensors, to run, modify and 433 | propagate that work, subject to this License. You are not responsible 434 | for enforcing compliance by third parties with this License. 435 | 436 | An "entity transaction" is a transaction transferring control of an 437 | organization, or substantially all assets of one, or subdividing an 438 | organization, or merging organizations. If propagation of a covered 439 | work results from an entity transaction, each party to that 440 | transaction who receives a copy of the work also receives whatever 441 | licenses to the work the party's predecessor in interest had or could 442 | give under the previous paragraph, plus a right to possession of the 443 | Corresponding Source of the work from the predecessor in interest, if 444 | the predecessor has it or can get it with reasonable efforts. 445 | 446 | You may not impose any further restrictions on the exercise of the 447 | rights granted or affirmed under this License. For example, you may 448 | not impose a license fee, royalty, or other charge for exercise of 449 | rights granted under this License, and you may not initiate litigation 450 | (including a cross-claim or counterclaim in a lawsuit) alleging that 451 | any patent claim is infringed by making, using, selling, offering for 452 | sale, or importing the Program or any portion of it. 453 | 454 | #### 11. Patents. 455 | 456 | A "contributor" is a copyright holder who authorizes use under this 457 | License of the Program or a work on which the Program is based. The 458 | work thus licensed is called the contributor's "contributor version". 459 | 460 | A contributor's "essential patent claims" are all patent claims owned 461 | or controlled by the contributor, whether already acquired or 462 | hereafter acquired, that would be infringed by some manner, permitted 463 | by this License, of making, using, or selling its contributor version, 464 | but do not include claims that would be infringed only as a 465 | consequence of further modification of the contributor version. For 466 | purposes of this definition, "control" includes the right to grant 467 | patent sublicenses in a manner consistent with the requirements of 468 | this License. 469 | 470 | Each contributor grants you a non-exclusive, worldwide, royalty-free 471 | patent license under the contributor's essential patent claims, to 472 | make, use, sell, offer for sale, import and otherwise run, modify and 473 | propagate the contents of its contributor version. 474 | 475 | In the following three paragraphs, a "patent license" is any express 476 | agreement or commitment, however denominated, not to enforce a patent 477 | (such as an express permission to practice a patent or covenant not to 478 | sue for patent infringement). To "grant" such a patent license to a 479 | party means to make such an agreement or commitment not to enforce a 480 | patent against the party. 481 | 482 | If you convey a covered work, knowingly relying on a patent license, 483 | and the Corresponding Source of the work is not available for anyone 484 | to copy, free of charge and under the terms of this License, through a 485 | publicly available network server or other readily accessible means, 486 | then you must either (1) cause the Corresponding Source to be so 487 | available, or (2) arrange to deprive yourself of the benefit of the 488 | patent license for this particular work, or (3) arrange, in a manner 489 | consistent with the requirements of this License, to extend the patent 490 | license to downstream recipients. "Knowingly relying" means you have 491 | actual knowledge that, but for the patent license, your conveying the 492 | covered work in a country, or your recipient's use of the covered work 493 | in a country, would infringe one or more identifiable patents in that 494 | country that you have reason to believe are valid. 495 | 496 | If, pursuant to or in connection with a single transaction or 497 | arrangement, you convey, or propagate by procuring conveyance of, a 498 | covered work, and grant a patent license to some of the parties 499 | receiving the covered work authorizing them to use, propagate, modify 500 | or convey a specific copy of the covered work, then the patent license 501 | you grant is automatically extended to all recipients of the covered 502 | work and works based on it. 503 | 504 | A patent license is "discriminatory" if it does not include within the 505 | scope of its coverage, prohibits the exercise of, or is conditioned on 506 | the non-exercise of one or more of the rights that are specifically 507 | granted under this License. You may not convey a covered work if you 508 | are a party to an arrangement with a third party that is in the 509 | business of distributing software, under which you make payment to the 510 | third party based on the extent of your activity of conveying the 511 | work, and under which the third party grants, to any of the parties 512 | who would receive the covered work from you, a discriminatory patent 513 | license (a) in connection with copies of the covered work conveyed by 514 | you (or copies made from those copies), or (b) primarily for and in 515 | connection with specific products or compilations that contain the 516 | covered work, unless you entered into that arrangement, or that patent 517 | license was granted, prior to 28 March 2007. 518 | 519 | Nothing in this License shall be construed as excluding or limiting 520 | any implied license or other defenses to infringement that may 521 | otherwise be available to you under applicable patent law. 522 | 523 | #### 12. No Surrender of Others' Freedom. 524 | 525 | If conditions are imposed on you (whether by court order, agreement or 526 | otherwise) that contradict the conditions of this License, they do not 527 | excuse you from the conditions of this License. If you cannot convey a 528 | covered work so as to satisfy simultaneously your obligations under 529 | this License and any other pertinent obligations, then as a 530 | consequence you may not convey it at all. For example, if you agree to 531 | terms that obligate you to collect a royalty for further conveying 532 | from those to whom you convey the Program, the only way you could 533 | satisfy both those terms and this License would be to refrain entirely 534 | from conveying the Program. 535 | 536 | #### 13. Remote Network Interaction; Use with the GNU General Public License. 537 | 538 | Notwithstanding any other provision of this License, if you modify the 539 | Program, your modified version must prominently offer all users 540 | interacting with it remotely through a computer network (if your 541 | version supports such interaction) an opportunity to receive the 542 | Corresponding Source of your version by providing access to the 543 | Corresponding Source from a network server at no charge, through some 544 | standard or customary means of facilitating copying of software. This 545 | Corresponding Source shall include the Corresponding Source for any 546 | work covered by version 3 of the GNU General Public License that is 547 | incorporated pursuant to the following paragraph. 548 | 549 | Notwithstanding any other provision of this License, you have 550 | permission to link or combine any covered work with a work licensed 551 | under version 3 of the GNU General Public License into a single 552 | combined work, and to convey the resulting work. The terms of this 553 | License will continue to apply to the part which is the covered work, 554 | but the work with which it is combined will remain governed by version 555 | 3 of the GNU General Public License. 556 | 557 | #### 14. Revised Versions of this License. 558 | 559 | The Free Software Foundation may publish revised and/or new versions 560 | of the GNU Affero General Public License from time to time. Such new 561 | versions will be similar in spirit to the present version, but may 562 | differ in detail to address new problems or concerns. 563 | 564 | Each version is given a distinguishing version number. If the Program 565 | specifies that a certain numbered version of the GNU Affero General 566 | Public License "or any later version" applies to it, you have the 567 | option of following the terms and conditions either of that numbered 568 | version or of any later version published by the Free Software 569 | Foundation. If the Program does not specify a version number of the 570 | GNU Affero General Public License, you may choose any version ever 571 | published by the Free Software Foundation. 572 | 573 | If the Program specifies that a proxy can decide which future versions 574 | of the GNU Affero General Public License can be used, that proxy's 575 | public statement of acceptance of a version permanently authorizes you 576 | to choose that version for the Program. 577 | 578 | Later license versions may give you additional or different 579 | permissions. However, no additional obligations are imposed on any 580 | author or copyright holder as a result of your choosing to follow a 581 | later version. 582 | 583 | #### 15. Disclaimer of Warranty. 584 | 585 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 586 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 587 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 588 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 589 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 590 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 591 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 592 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 593 | CORRECTION. 594 | 595 | #### 16. Limitation of Liability. 596 | 597 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 598 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 599 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 600 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 601 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 602 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 603 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 604 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 605 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 606 | 607 | #### 17. Interpretation of Sections 15 and 16. 608 | 609 | If the disclaimer of warranty and limitation of liability provided 610 | above cannot be given local legal effect according to their terms, 611 | reviewing courts shall apply local law that most closely approximates 612 | an absolute waiver of all civil liability in connection with the 613 | Program, unless a warranty or assumption of liability accompanies a 614 | copy of the Program in return for a fee. 615 | 616 | END OF TERMS AND CONDITIONS 617 | 618 | ### How to Apply These Terms to Your New Programs 619 | 620 | If you develop a new program, and you want it to be of the greatest 621 | possible use to the public, the best way to achieve this is to make it 622 | free software which everyone can redistribute and change under these 623 | terms. 624 | 625 | To do so, attach the following notices to the program. It is safest to 626 | attach them to the start of each source file to most effectively state 627 | the exclusion of warranty; and each file should have at least the 628 | "copyright" line and a pointer to where the full notice is found. 629 | 630 | 631 | Copyright (C) 632 | 633 | This program is free software: you can redistribute it and/or modify 634 | it under the terms of the GNU Affero General Public License as 635 | published by the Free Software Foundation, either version 3 of the 636 | License, or (at your option) any later version. 637 | 638 | This program is distributed in the hope that it will be useful, 639 | but WITHOUT ANY WARRANTY; without even the implied warranty of 640 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 641 | GNU Affero General Public License for more details. 642 | 643 | You should have received a copy of the GNU Affero General Public License 644 | along with this program. If not, see . 645 | 646 | Also add information on how to contact you by electronic and paper 647 | mail. 648 | 649 | If your software can interact with users remotely through a computer 650 | network, you should also make sure that it provides a way for users to 651 | get its source. For example, if your program is a web application, its 652 | interface could display a "Source" link that leads users to an archive 653 | of the code. There are many ways you could offer source, and different 654 | solutions will be better for different programs; see section 13 for 655 | the specific requirements. 656 | 657 | You should also get your employer (if you work as a programmer) or 658 | school, if any, to sign a "copyright disclaimer" for the program, if 659 | necessary. For more information on this, and how to apply and follow 660 | the GNU AGPL, see . 661 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY ?= k6-tracing 2 | IMAGE ?= ghcr.io/grafana/xk6-client-tracing 3 | IMAGE_TAG ?= latest 4 | 5 | GO_MODULE := $(shell head -n1 go.mod | cut -d' ' -f2) 6 | GO_TEST_OPTS := -race -count=1 -cover -v 7 | GO_LINT_OPTS := --config ./golangci.yml 8 | XK6_BUILD_OPTS := --output ./$(BINARY) 9 | 10 | .PHONY: build 11 | build: 12 | xk6 build $(XK6_BUILD_OPTS) --with $(GO_MODULE)=. 13 | 14 | .PHONY: test 15 | test: 16 | go tool gotestsum --format=testname -- $(GO_TEST_OPTS) ./... 17 | 18 | .PHONY: lint 19 | lint: 20 | golangci-lint run $(GO_LINT_OPTS) ./... 21 | 22 | .PHONY: fmt 23 | fmt: 24 | go tool goimports -w ./ 25 | 26 | check-fmt: fmt 27 | @git diff --exit-code 28 | 29 | .PHONY: docker 30 | docker: 31 | docker build . -t $(IMAGE):$(IMAGE_TAG) 32 | 33 | .PHONY: clean 34 | clean: 35 | go clean -cache -testcache 36 | docker rmi -f $(IMAGE) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xk6-client-tracing 2 | 3 | > ### ⚠️ In Development 4 | > 5 | > This project is **in development** and changes a lot between commits. Use at your own risk. 6 | 7 | This extension provides k6 with the required functionality required to load test distributed tracing backends. 8 | 9 | ## Usage 10 | 11 | Generating traces and sending them to an agent or backend requires two things: a client and a trace generator. 12 | Generators have a method called `traces()` that can be used to generate traces. 13 | The client provides a method `push()` which receives the generated traces as first parameter and sends them to the configured collector. 14 | 15 | Creating a client requires a client configuration: 16 | 17 | ```javascript 18 | const config = { 19 | endpoint: "localhost:4317", 20 | exporter: tracing.EXPORTER_OTLP, 21 | }; 22 | let client = new tracing.Client(config); 23 | ``` 24 | 25 | The configuration is an object with the following schema: 26 | 27 | ```javascript 28 | { 29 | // The endpoint to which the traces are sent in the form of : 30 | endpoint: string, 31 | // The exporter protocol used for sending the traces: tracing.EXPORTER_OTLP or tracing.EXPORTER_JAEGER 32 | exporter: string, 33 | // Credentials used for authentication (optional) 34 | authentication: { user: string, password: string }, 35 | // Additional headers sent by the client (optional) 36 | headers: { string : string } 37 | // TLS configuration 38 | tls: { 39 | // Whether insecure connections are allowed (optional, default: false) 40 | insecure: boolean, 41 | // Enable TLS but skip verification (optional, default: false) 42 | insecure_skip_verify: boolean, 43 | // The server name requested by the client (optional) 44 | server_name: string, 45 | // The path to the CA certificate file (optional) 46 | ca_file: string, 47 | // The path to the certificate file (optional) 48 | cert_file: string, 49 | // The path to the key file (optional) 50 | key_file: string, 51 | }, 52 | } 53 | ``` 54 | 55 | There are two different types of generators which are described in the following sections. 56 | 57 | ### Parameterized trace generator 58 | 59 | This generator creates traces consisting of completely randomized spans. 60 | The spans contain a configurable number of random attributes with randomly assigned values. 61 | The main purpose of this generator is to create a large amount of spans with few lines of code. 62 | 63 | An example can be found in [./examples/param](./examples/param). 64 | 65 | ### Templated trace generator 66 | 67 | This generator creates realistically looking and traces that contain spans with span name, span kind, and attributes. 68 | The trace is generated from a template configurations that describes how each should be generated. 69 | 70 | The following listing creates a generator that creates traces with a single span: 71 | 72 | ```javascript 73 | const template = { 74 | spans: [ 75 | {service: "article-service", name: "get-articles", attributes: {"http.method": "GET"}} 76 | ] 77 | }; 78 | let gen = new tracing.TemplatedGenerator(template); 79 | client.push(gen.traces()); 80 | ``` 81 | 82 | The generated span will have the name `get-articles`. 83 | The generator will further assign a span kind as well as some commonly used attributes. 84 | There will also be a corresponding resource span with the respective `service.name` attribute. 85 | 86 | The template has the following schema: 87 | 88 | ```javascript 89 | { 90 | // The defaults can be used to configure parameters that are applied to all spans (optional) 91 | defaults: { 92 | // Fixed attributes that are added to every generated span (optional) 93 | attributes: { string : any }, 94 | // attributeSemantics can be set in order to generate attributes that follow a certain OpenTelemetry 95 | // semantic convention. For example tracing.SEMANTICS_HTTP (optional) 96 | attributeSemantics: string, 97 | // Parameters to configure the creation of random attributes. If missing, no random attributes 98 | // are added to the spans (optional) 99 | randomAttributes: { 100 | // The number of random attributes to generate 101 | count: int, 102 | // The number of distinct values to generate for each attribute (optional, default: 50) 103 | cardinality: int 104 | } 105 | // Default resource attributes for all resources in the trace (optional) 106 | resource: { 107 | // Fixed attributs that are added to each resource (optional) 108 | attributes: { string : any }, 109 | // Parameters to configure the creation of random resource attributes (optional) 110 | randomAttributes: { 111 | // The number of random attributes to generate 112 | count: int, 113 | // The number of distinct values to generate for each attribute (optional, default: 50) 114 | cardinality: int 115 | } 116 | } 117 | }, 118 | // Templates for the individual spans 119 | spans: [ 120 | { 121 | // Is used to set the service.name attribute of the corresponding resource span 122 | service: string, 123 | // The name of the span. If empty, the name will be randomly generated (optional) 124 | name: string, 125 | // The index of the parent span in `spans`. The index must be smaller than the 126 | // own index. If empty, the parent is the span with the position directly before 127 | // this span in `spans` (optional) 128 | parentIdx: int, 129 | // The interval for the generated span duration. If missing, a random duration is 130 | // generated that is shorter than the duration of the parent span (optional) 131 | duration: { min: int, max: int }, 132 | // Fixed attributes that are added to this (optional) 133 | attributes: { string : any }, 134 | // attributeSemantics can be set in order to generate attributes that follow a certain OpenTelemetry 135 | // semantic convention. For example tracing.SEMANTICS_HTTP (optional) 136 | attributeSemantics: string, 137 | // Parameters to configure the creation of random attributes. If missing, no random attributes 138 | // are added to the span (optional) 139 | randomAttributes: { 140 | // The number of random attributes to generate 141 | count: int, 142 | // The number of distinct values to generate for each attribute (optional, default: 50) 143 | cardinality: int 144 | }, 145 | // Additional attributes for the resource associated with this span. Resource attribute definitions 146 | // of different spans with the same service name will me merged into a singe resource (optional) 147 | resource: { 148 | // Fixed attributs that are added to the resource (optional) 149 | attributes: { string : any }, 150 | // Parameters to configure the creation of random resource attributes (optional) 151 | randomAttributes: { 152 | // The number of random attributes to generate 153 | count: int, 154 | // The number of distinct values to generate for each attribute (optional, default: 50) 155 | cardinality: int 156 | } 157 | } 158 | }, 159 | ... 160 | ] 161 | } 162 | ``` 163 | 164 | An example with a templated generator can be found in [./examples/template](./examples/template). 165 | 166 | ## Getting started 167 | 168 | To start using the k6 tracing extension, ensure you have the following prerequisites installed: 169 | 170 | - Docker 171 | - docker-compose 172 | - make 173 | 174 | ### Build docker image 175 | 176 | The docker image is compiled using a multi-stage Docker build and does not require further dependencies. 177 | To start the build process run: 178 | 179 | ```shell 180 | make docker 181 | ``` 182 | 183 | After the command completed successfully the image `grafana/xk6-client-tracing:latest` is available. 184 | 185 | ### Run docker-compose example 186 | 187 | > Note: before running the docker-compose example, make sure to complete the docker image build step above! 188 | 189 | To run the example `cd` into the directory `examples/param` and run: 190 | 191 | ```shell 192 | docker-compose up -d 193 | ``` 194 | 195 | In the example `k6-tracing` uses the script `param.js` to generate spans and sends them to the `otel-collector`. 196 | The generated spans can be observed by inspecting the collector's logs: 197 | 198 | ```shell 199 | docker-compose logs -f otel-collector 200 | ``` 201 | 202 | The example uses the OTLP gRPC exporter. 203 | If you want to use Jaeger gRPC, you can change `param.js` and use the following settings: 204 | 205 | ```javascript 206 | const client = new tracing.Client({ 207 | endpoint: "otel-collector:14250", 208 | exporter: "jaeger", 209 | insecure: true, 210 | }); 211 | ``` 212 | 213 | > Note: HTTP exporters aren't supported (yet) 214 | 215 | ### Build locally 216 | 217 | Building the extension locally has additional prerequisites: 218 | 219 | - [Go toolchain](https://go101.org/article/go-toolchain.html) 220 | - Git 221 | 222 | Furthermore, the build also requires [`xk6`](https://github.com/grafana/xk6) to compile k6 with the bundled tracing extension. 223 | Run the following command to install `xk6`: 224 | 225 | ```shell 226 | go install go.k6.io/xk6/cmd/xk6@latest 227 | ``` 228 | 229 | To build binary run: 230 | ```shell 231 | make build 232 | ``` 233 | 234 | The build step produces the `k6-tracing` binary. 235 | To test the binary you first need to change the endpoint in the client configuration to: 236 | 237 | ```javascript 238 | const client = new tracing.Client({ 239 | endpoint: "localhost:4317", 240 | exporter: "otlp", 241 | insecure: true, 242 | }); 243 | ``` 244 | 245 | Once you've your new binary and configuration ready, you can run a local OTEL collector: 246 | ```bash 247 | docker run --rm -p 13133:13133 -p 14250:14250 -p 14268:14268 \ 248 | -p 55678-55679:55678-55679 -p 4317:4317 -p 9411:9411 \ 249 | -v "${PWD}/examples/shared/collector-config.yaml":/collector-config.yaml \ 250 | --name otelcol otel/opentelemetry-collector \ 251 | --config collector-config.yaml 252 | ``` 253 | 254 | Once that's done, you can run a test like: 255 | ``` 256 | ./k6-tracing run examples/basic/param.js 257 | ``` 258 | 259 | And see the generated spans in the OTEL collector logs! 260 | 261 | ## Using the extension with Grafana Cloud 262 | 263 | You can do that, by using the OTLP exporter and setting the required auth credentials: 264 | 265 | ```javascript 266 | const client = new tracing.Client({ 267 | endpoint: "you-tempo-endpoint:443" 268 | exporter: "otlp", 269 | insecure: false, 270 | authentication: { 271 | user: "tenant-id", 272 | password: "api-token" 273 | } 274 | }); 275 | ``` 276 | -------------------------------------------------------------------------------- /examples/param/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | k6-tracing: 5 | image: ghcr.io/grafana/xk6-client-tracing:latest 6 | command: 7 | - run 8 | - /param.js 9 | volumes: 10 | - ./param.js:/param.js:ro 11 | depends_on: 12 | - otel-collector 13 | restart: always 14 | 15 | otel-collector: 16 | image: otel/opentelemetry-collector:latest 17 | command: 18 | - --config=/collector-config.yaml 19 | volumes: 20 | - ../shared/collector-config.yaml:/collector-config.yaml:ro 21 | ports: 22 | - "13133:13133" 23 | - "14250:14250" 24 | - "14268:14268" 25 | - "55678-55679:55678-55679" 26 | - "4317:4317" 27 | - "4318:4318" 28 | - "9411:9411" 29 | -------------------------------------------------------------------------------- /examples/param/param.js: -------------------------------------------------------------------------------- 1 | import { sleep } from 'k6'; 2 | import tracing from 'k6/x/tracing'; 3 | import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; 4 | 5 | export let options = { 6 | vus: 1, 7 | duration: "20m", 8 | }; 9 | 10 | const endpoint = __ENV.ENDPOINT || "otel-collector:4317" 11 | const client = new tracing.Client({ 12 | endpoint, 13 | exporter: tracing.EXPORTER_OTLP, 14 | tls: { 15 | insecure: true, 16 | } 17 | }); 18 | 19 | export default function () { 20 | let pushSizeTraces = randomIntBetween(2, 3); 21 | let pushSizeSpans = 0; 22 | let t = []; 23 | for (let i = 0; i < pushSizeTraces; i++) { 24 | let c = randomIntBetween(5, 10) 25 | pushSizeSpans += c; 26 | 27 | t.push({ 28 | random_service_name: false, 29 | count: 1, 30 | resource_size: 100, 31 | spans: { 32 | count: c, 33 | size: randomIntBetween(300, 1000), 34 | random_name: true, 35 | fixed_attrs: { 36 | "test": "test", 37 | }, 38 | } 39 | }); 40 | } 41 | 42 | let gen = new tracing.ParameterizedGenerator(t) 43 | let traces = gen.traces() 44 | client.push(traces); 45 | 46 | console.log(`Pushed ${pushSizeSpans} spans from ${pushSizeTraces} different traces. Here is a random traceID: ${t[Math.floor(Math.random() * t.length)].id}`); 47 | sleep(15); 48 | } 49 | 50 | export function teardown() { 51 | client.shutdown(); 52 | } 53 | -------------------------------------------------------------------------------- /examples/shared/collector-config.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | otlp: 3 | protocols: 4 | grpc: 5 | http: 6 | jaeger: 7 | protocols: 8 | grpc: 9 | exporters: 10 | logging: 11 | loglevel: debug 12 | sampling_initial: 5 13 | sampling_thereafter: 200 14 | service: 15 | pipelines: 16 | traces: 17 | exporters: ["logging"] 18 | processors: [] 19 | receivers: ["otlp", "jaeger"] -------------------------------------------------------------------------------- /examples/template/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | k6-tracing: 5 | image: ghcr.io/grafana/xk6-client-tracing:latest 6 | command: 7 | - run 8 | - /template.js 9 | volumes: 10 | - ./template.js:/template.js:ro 11 | depends_on: 12 | - otel-collector 13 | restart: always 14 | 15 | otel-collector: 16 | image: otel/opentelemetry-collector:latest 17 | command: 18 | - --config=/collector-config.yaml 19 | volumes: 20 | - ../shared/collector-config.yaml:/collector-config.yaml:ro 21 | ports: 22 | - "13133:13133" 23 | - "14250:14250" 24 | - "14268:14268" 25 | - "55678-55679:55678-55679" 26 | - "4317:4317" 27 | - "4318:4318" 28 | - "9411:9411" 29 | -------------------------------------------------------------------------------- /examples/template/template.js: -------------------------------------------------------------------------------- 1 | import {sleep} from 'k6'; 2 | import tracing from 'k6/x/tracing'; 3 | import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; 4 | 5 | export const options = { 6 | vus: 1, 7 | duration: "20m", 8 | }; 9 | 10 | const endpoint = __ENV.ENDPOINT || "otel-collector:4317" 11 | const orgid = __ENV.TEMPO_X_SCOPE_ORGID || "k6-test" 12 | const client = new tracing.Client({ 13 | endpoint, 14 | exporter: tracing.EXPORTER_OTLP, 15 | tls: { 16 | insecure: true, 17 | }, 18 | headers: { 19 | "X-Scope-Orgid": orgid 20 | } 21 | }); 22 | 23 | const traceDefaults = { 24 | attributeSemantics: tracing.SEMANTICS_HTTP, 25 | attributes: {"one": "three"}, 26 | randomAttributes: {count: 2, cardinality: 5}, 27 | randomEvents: {count: 0.1, exceptionCount: 0.2, randomAttributes: {count: 6, cardinality: 20}}, 28 | resource: { randomAttributes: {count: 3} }, 29 | } 30 | 31 | const traceTemplates = [ 32 | { 33 | defaults: traceDefaults, 34 | spans: [ 35 | {service: "shop-backend", name: "list-articles", duration: {min: 200, max: 900}, resource: { attributes: {"namespace": "shop"} }}, 36 | {service: "shop-backend", name: "authenticate", duration: {min: 50, max: 100}, resource: { randomAttributes: {count: 4} }}, 37 | {service: "auth-service", name: "authenticate", resource: { randomAttributes: {count: 2}, attributes: {"namespace": "auth"} }}, 38 | {service: "shop-backend", name: "fetch-articles", parentIdx: 0}, 39 | { 40 | service: "article-service", 41 | name: "list-articles", 42 | links: [{attributes: {"link-type": "parent-child"}, randomAttributes: {count: 2, cardinality: 5}}], 43 | resource: { attributes: {"namespace": "shop" }} 44 | }, 45 | {service: "article-service", name: "select-articles", attributeSemantics: tracing.SEMANTICS_DB}, 46 | {service: "postgres", name: "query-articles", attributeSemantics: tracing.SEMANTICS_DB, randomAttributes: {count: 5}, resource: { attributes: {"namespace": "db"} }}, 47 | ] 48 | }, 49 | { 50 | defaults: { 51 | attributes: {"numbers": ["one", "two", "three"]}, 52 | attributeSemantics: tracing.SEMANTICS_HTTP, 53 | randomEvents: {count: 2, randomAttributes: {count: 3, cardinality: 10}}, 54 | }, 55 | spans: [ 56 | {service: "shop-backend", name: "article-to-cart", duration: {min: 400, max: 1200}}, 57 | {service: "shop-backend", name: "authenticate", duration: {min: 70, max: 200}}, 58 | {service: "auth-service", name: "authenticate"}, 59 | {service: "shop-backend", name: "get-article", parentIdx: 0}, 60 | {service: "article-service", name: "get-article"}, 61 | {service: "article-service", name: "select-articles", attributeSemantics: tracing.SEMANTICS_DB}, 62 | {service: "postgres", name: "query-articles", attributeSemantics: tracing.SEMANTICS_DB, randomAttributes: {count: 2}}, 63 | {service: "shop-backend", name: "place-articles", parentIdx: 0}, 64 | {service: "cart-service", name: "place-articles", attributes: {"article.count": 1, "http.status_code": 201}}, 65 | {service: "cart-service", name: "persist-cart"} 66 | ] 67 | }, 68 | { 69 | defaults: traceDefaults, 70 | spans: [ 71 | {service: "shop-backend", attributes: {"http.status_code": 403}}, 72 | {service: "shop-backend", name: "authenticate", attributes: {"http.request.header.accept": ["application/json"]}}, 73 | { 74 | service: "auth-service", 75 | name: "authenticate", 76 | attributes: {"http.status_code": 403}, 77 | randomEvents: {count: 0.5, exceptionCount: 2, randomAttributes: {count: 5, cardinality: 5}} 78 | }, 79 | ] 80 | }, 81 | { 82 | defaults: traceDefaults, 83 | spans: [ 84 | {service: "shop-backend"}, 85 | {service: "shop-backend", name: "authenticate", attributes: {"http.request.header.accept": ["application/json"]}}, 86 | {service: "auth-service", name: "authenticate"}, 87 | { 88 | service: "cart-service", 89 | name: "checkout", 90 | randomEvents: {count: 0.5, exceptionCount: 2, exceptionOnError: true, randomAttributes: {count: 5, cardinality: 5}} 91 | }, 92 | { 93 | service: "billing-service", 94 | name: "payment", 95 | randomLinks: {count: 0.5, randomAttributes: {count: 3, cardinality: 10}}, 96 | randomEvents: {exceptionOnError: true, randomAttributes: {count: 4}}} 97 | ] 98 | }, 99 | ] 100 | 101 | export default function () { 102 | const templateIndex = randomIntBetween(0, traceTemplates.length-1) 103 | const gen = new tracing.TemplatedGenerator(traceTemplates[templateIndex]) 104 | client.push(gen.traces()) 105 | 106 | sleep(randomIntBetween(1, 5)); 107 | } 108 | 109 | export function teardown() { 110 | client.shutdown(); 111 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/grafana/xk6-client-tracing 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | tool ( 8 | golang.org/x/tools/cmd/goimports 9 | gotest.tools/gotestsum 10 | ) 11 | 12 | require ( 13 | github.com/grafana/sobek v0.0.0-20250219104821-ed22af7a8d6c 14 | github.com/stretchr/testify v1.10.0 15 | go.k6.io/k6 v0.57.0 16 | go.opentelemetry.io/collector/component v0.120.0 17 | go.opentelemetry.io/collector/component/componenttest v0.120.0 18 | go.opentelemetry.io/collector/config/configgrpc v0.120.0 19 | go.opentelemetry.io/collector/config/confighttp v0.120.0 20 | go.opentelemetry.io/collector/config/configopaque v1.26.0 21 | go.opentelemetry.io/collector/config/configtls v1.26.0 22 | go.opentelemetry.io/collector/exporter v0.120.0 23 | go.opentelemetry.io/collector/exporter/otlpexporter v0.120.0 24 | go.opentelemetry.io/collector/exporter/otlphttpexporter v0.120.0 25 | go.opentelemetry.io/collector/pdata v1.26.0 26 | go.opentelemetry.io/otel/metric v1.34.0 27 | go.opentelemetry.io/otel/trace v1.34.0 28 | go.uber.org/zap v1.27.0 29 | ) 30 | 31 | require ( 32 | github.com/bitfield/gotestdox v0.2.2 // indirect 33 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 35 | github.com/dlclark/regexp2 v1.11.5 // indirect 36 | github.com/dnephin/pflag v1.0.7 // indirect 37 | github.com/evanw/esbuild v0.25.0 // indirect 38 | github.com/fatih/color v1.18.0 // indirect 39 | github.com/felixge/httpsnoop v1.0.4 // indirect 40 | github.com/fsnotify/fsnotify v1.8.0 // indirect 41 | github.com/go-logr/logr v1.4.2 // indirect 42 | github.com/go-logr/stdr v1.2.2 // indirect 43 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 44 | github.com/gogo/protobuf v1.3.2 // indirect 45 | github.com/golang/snappy v0.0.4 // indirect 46 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 // indirect 47 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect 50 | github.com/hashicorp/go-version v1.7.0 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/klauspost/compress v1.18.0 // indirect 54 | github.com/mailru/easyjson v0.9.0 // indirect 55 | github.com/mattn/go-colorable v0.1.14 // indirect 56 | github.com/mattn/go-isatty v0.0.20 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/mostynb/go-grpc-compression v1.2.3 // indirect 60 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd // indirect 61 | github.com/onsi/ginkgo v1.16.5 // indirect 62 | github.com/onsi/gomega v1.33.0 // indirect 63 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 64 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 65 | github.com/rs/cors v1.11.1 // indirect 66 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 67 | github.com/sirupsen/logrus v1.9.3 // indirect 68 | github.com/spf13/afero v1.12.0 // indirect 69 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 70 | go.opentelemetry.io/collector v0.120.0 // indirect 71 | go.opentelemetry.io/collector/client v1.26.0 // indirect 72 | go.opentelemetry.io/collector/config/configauth v0.120.0 // indirect 73 | go.opentelemetry.io/collector/config/configcompression v1.26.0 // indirect 74 | go.opentelemetry.io/collector/config/confignet v1.26.0 // indirect 75 | go.opentelemetry.io/collector/config/configretry v1.26.0 // indirect 76 | go.opentelemetry.io/collector/consumer v1.26.0 // indirect 77 | go.opentelemetry.io/collector/consumer/consumererror v0.120.0 // indirect 78 | go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.120.0 // indirect 79 | go.opentelemetry.io/collector/consumer/xconsumer v0.120.0 // indirect 80 | go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.120.0 // indirect 81 | go.opentelemetry.io/collector/exporter/xexporter v0.120.0 // indirect 82 | go.opentelemetry.io/collector/extension v0.120.0 // indirect 83 | go.opentelemetry.io/collector/extension/auth v0.120.0 // indirect 84 | go.opentelemetry.io/collector/extension/xextension v0.120.0 // indirect 85 | go.opentelemetry.io/collector/featuregate v1.26.0 // indirect 86 | go.opentelemetry.io/collector/pdata/pprofile v0.120.0 // indirect 87 | go.opentelemetry.io/collector/pipeline v0.120.0 // indirect 88 | go.opentelemetry.io/collector/pipeline/xpipeline v0.120.0 // indirect 89 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 // indirect 90 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect 91 | go.opentelemetry.io/otel v1.34.0 // indirect 92 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect 93 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect 94 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 // indirect 95 | go.opentelemetry.io/otel/sdk v1.34.0 // indirect 96 | go.opentelemetry.io/otel/sdk/metric v1.34.0 // indirect 97 | go.opentelemetry.io/proto/otlp v1.5.0 // indirect 98 | go.uber.org/multierr v1.11.0 // indirect 99 | golang.org/x/crypto v0.35.0 // indirect 100 | golang.org/x/mod v0.17.0 // indirect 101 | golang.org/x/net v0.35.0 // indirect 102 | golang.org/x/sync v0.11.0 // indirect 103 | golang.org/x/sys v0.30.0 // indirect 104 | golang.org/x/term v0.29.0 // indirect 105 | golang.org/x/text v0.22.0 // indirect 106 | golang.org/x/time v0.10.0 // indirect 107 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 108 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect 109 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect 110 | google.golang.org/grpc v1.70.0 // indirect 111 | google.golang.org/protobuf v1.36.5 // indirect 112 | gopkg.in/guregu/null.v3 v3.5.0 // indirect 113 | gopkg.in/yaml.v3 v3.0.1 // indirect 114 | gotest.tools/gotestsum v1.12.0 // indirect 115 | ) 116 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= 2 | github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 3 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 4 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 5 | github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= 6 | github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= 7 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 8 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 14 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 15 | github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= 16 | github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= 17 | github.com/evanw/esbuild v0.25.0 h1:jRR9D1pfdb669VzdN4w0jwsDfrKE098nKMaDMKvMPyU= 18 | github.com/evanw/esbuild v0.25.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 19 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 20 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 21 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 22 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 23 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 24 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 25 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 26 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 27 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 28 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 29 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 30 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 31 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 32 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 33 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 34 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 35 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 36 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 37 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 38 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 39 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 40 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 41 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 42 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 43 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 44 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 45 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 46 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 47 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 48 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 49 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 50 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 51 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 52 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 53 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 55 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 57 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 58 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 59 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 60 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= 61 | github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 62 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 63 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 64 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 65 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 66 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 67 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 68 | github.com/grafana/sobek v0.0.0-20250219104821-ed22af7a8d6c h1:AnjwFNvLSgko/nVMGp+dPkqBpI6Vdf4rXH60/JQgJlI= 69 | github.com/grafana/sobek v0.0.0-20250219104821-ed22af7a8d6c/go.mod h1:FmcutBFPLiGgroH42I4/HBahv7GxVjODcVWFTw1ISes= 70 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= 71 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= 72 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 73 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 74 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 75 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 76 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 79 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 80 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 81 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 82 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 83 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= 84 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 85 | github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= 86 | github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= 87 | github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= 88 | github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= 89 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 90 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 91 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 92 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 93 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 94 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 95 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 96 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 97 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 98 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 99 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 100 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 101 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 102 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 103 | github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa h1:lx8ZnNPwjkXSzOROz0cg69RlErRXs+L3eDkggASWKLo= 104 | github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa/go.mod h1:fhpOYavp5g2K74XDl/ao2y4KvhqVtKlkg1e+0UaQv7I= 105 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 106 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 107 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 108 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 109 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 110 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 111 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 112 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 113 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 114 | github.com/mostynb/go-grpc-compression v1.2.3 h1:42/BKWMy0KEJGSdWvzqIyOZ95YcR9mLPqKctH7Uo//I= 115 | github.com/mostynb/go-grpc-compression v1.2.3/go.mod h1:AghIxF3P57umzqM9yz795+y1Vjs47Km/Y2FE6ouQ7Lg= 116 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd h1:AC3N94irbx2kWGA8f/2Ks7EQl2LxKIRQYuT9IJDwgiI= 117 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd/go.mod h1:9vRHVuLCjoFfE3GT06X0spdOAO+Zzo4AMjdIwUHBvAk= 118 | github.com/mstoykov/envconfig v1.5.0 h1:E2FgWf73BQt0ddgn7aoITkQHmgwAcHup1s//MsS5/f8= 119 | github.com/mstoykov/envconfig v1.5.0/go.mod h1:vk/d9jpexY2Z9Bb0uB4Ndesss1Sr0Z9ZiGUrg5o9VGk= 120 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 121 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 122 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= 123 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 124 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 125 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 126 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 127 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 128 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 129 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 130 | github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= 131 | github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= 132 | github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= 133 | github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 134 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 135 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 136 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 137 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 138 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 139 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 140 | github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= 141 | github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 142 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 143 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 144 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 145 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 146 | github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= 147 | github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= 148 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 149 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 150 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 151 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 152 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 153 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 154 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 155 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 156 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 157 | go.k6.io/k6 v0.57.0 h1:l1jivNtbCQYNhgvl+O6SfzwabqNlazr8OLYjxm8lNGw= 158 | go.k6.io/k6 v0.57.0/go.mod h1:AXTOq8X59VqigGvoI59Al/+8F/5h4iHO0CoX0lNbq/4= 159 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 160 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 161 | go.opentelemetry.io/collector v0.120.0 h1:RmEFc4rPB0wg7fEDY0k8OenXznAtsVGzgAkjTAF44WQ= 162 | go.opentelemetry.io/collector v0.120.0/go.mod h1:uNDaRieHl04oQCvGFY4KzpDRqejgMNbea0u2+vk7P3k= 163 | go.opentelemetry.io/collector/client v1.26.0 h1:m/rXHfGzHx4RcETswnm5Y2r1uPv6q0lY+M4btNxbLnE= 164 | go.opentelemetry.io/collector/client v1.26.0/go.mod h1:H7dkvh+4BbglV1QiyI+AD/aWuqJ3iE5oiYr5oDKtBLw= 165 | go.opentelemetry.io/collector/component v0.120.0 h1:YHEQ6NuBI6FQHKW24OwrNg2IJ0EUIg4RIuwV5YQ6PSI= 166 | go.opentelemetry.io/collector/component v0.120.0/go.mod h1:Ya5O+5NWG9XdhJPnOVhKtBrNXHN3hweQbB98HH4KPNU= 167 | go.opentelemetry.io/collector/component/componenttest v0.120.0 h1:vKX85d3lpxj/RoiFQNvmIpX9lOS80FY5svzOYUyeYX0= 168 | go.opentelemetry.io/collector/component/componenttest v0.120.0/go.mod h1:QDLboWF2akEqAGyvje8Hc7GfXcrZvQ5FhmlWvD5SkzY= 169 | go.opentelemetry.io/collector/config/configauth v0.120.0 h1:5yJd4fYAxdbMnuEkTyfnKtZKEqNJVPyt+roDYDPdWIk= 170 | go.opentelemetry.io/collector/config/configauth v0.120.0/go.mod h1:n1rj/cJ+wi+4Cr7q9Z87sF2izYown4/ADDiPZMdLd6g= 171 | go.opentelemetry.io/collector/config/configcompression v1.26.0 h1:90J6ePTWwZbN6QRPawuGOmJG5H84KB4DzHdbd/kUZM4= 172 | go.opentelemetry.io/collector/config/configcompression v1.26.0/go.mod h1:QwbNpaOl6Me+wd0EdFuEJg0Cc+WR42HNjJtdq4TwE6w= 173 | go.opentelemetry.io/collector/config/configgrpc v0.120.0 h1:0MCcnNJ37f6xd7hYcw73ny/q2HXDtAvz2Cyxz2Od2+I= 174 | go.opentelemetry.io/collector/config/configgrpc v0.120.0/go.mod h1:TyM4S+HnPUp+4Nn0ueySrFWkCWNz6LO+0jtQ5opnKmo= 175 | go.opentelemetry.io/collector/config/confighttp v0.120.0 h1:ZOA59E7VsYSmMLGkNke6uOGq3yYK1hJ9OUa/swNeVtI= 176 | go.opentelemetry.io/collector/config/confighttp v0.120.0/go.mod h1:9GpKCdtmypk+DpuoJlAyV5LppiWazFahuJby+L5Rz2Q= 177 | go.opentelemetry.io/collector/config/confignet v1.26.0 h1:tzOY9pr0v38R9uyCTpqdAeeaT08RlAGyQ4VJlTyTev8= 178 | go.opentelemetry.io/collector/config/confignet v1.26.0/go.mod h1:HgpLwdRLzPTwbjpUXR0Wdt6pAHuYzaIr8t4yECKrEvo= 179 | go.opentelemetry.io/collector/config/configopaque v1.26.0 h1:lM9+fDvr5RWkTupoq8xi7qt0kvXoUX7UFN8D7Wb4zRI= 180 | go.opentelemetry.io/collector/config/configopaque v1.26.0/go.mod h1:GYQiC8IejBcwE8z0O4DwbBR/Hf6U7d8DTf+cszyqwFs= 181 | go.opentelemetry.io/collector/config/configretry v1.26.0 h1:DGuaZYkGXCr+Wd6+D65xZv7E9z/nyt/F//XbC4B/7M4= 182 | go.opentelemetry.io/collector/config/configretry v1.26.0/go.mod h1:8gzFQ0qzKLYvzP2sNPwsB9gwzKSEls649yANmt/d6yE= 183 | go.opentelemetry.io/collector/config/configtls v1.26.0 h1:aBNqX3Q3WpO20SG/CF6sKxD1rJllKom7gCOW6SeGcq4= 184 | go.opentelemetry.io/collector/config/configtls v1.26.0/go.mod h1:ppoLSWiwovldy4R9KCs6+XCWhvvBaF8eBhkUL460lxw= 185 | go.opentelemetry.io/collector/confmap v1.26.0 h1:+EVk0RaCBHs+7dYTwawd5n5tJiiUtErIy3YS3NIFP8o= 186 | go.opentelemetry.io/collector/confmap v1.26.0/go.mod h1:tmOa6iw3FJsEgfBHKALqvcdfRtf71JZGor0wSM5MoH8= 187 | go.opentelemetry.io/collector/confmap/xconfmap v0.120.0 h1:wt+9H/TLXhY6q40AVx+fn2XK/FhjXuwInwFq9X9+aik= 188 | go.opentelemetry.io/collector/confmap/xconfmap v0.120.0/go.mod h1:wkzt6fVdLqBP+ZvbJWCLbo68nedvmoK09wFpR17awgs= 189 | go.opentelemetry.io/collector/consumer v1.26.0 h1:0MwuzkWFLOm13qJvwW85QkoavnGpR4ZObqCs9g1XAvk= 190 | go.opentelemetry.io/collector/consumer v1.26.0/go.mod h1:I/ZwlWM0sbFLhbStpDOeimjtMbWpMFSoGdVmzYxLGDg= 191 | go.opentelemetry.io/collector/consumer/consumererror v0.120.0 h1:f46ZnKCGBdvkjtJBT0ruA9cxDnvuR1jeR0amq9qc6Mc= 192 | go.opentelemetry.io/collector/consumer/consumererror v0.120.0/go.mod h1:2Cx8948nywlM1MFJgqLrIJ7N/pfxZsMF0qq+n9oFJz0= 193 | go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.120.0 h1:bFQqRuj7WRyM/0fvLNo6aIt7Xic3ArlkqFGfts8jxs8= 194 | go.opentelemetry.io/collector/consumer/consumererror/xconsumererror v0.120.0/go.mod h1:Xw717Yx3laLJIfXVXgzQjsubLJnh5ugHYg1N87gdNVE= 195 | go.opentelemetry.io/collector/consumer/consumertest v0.120.0 h1:iPFmXygDsDOjqwdQ6YZcTmpiJeQDJX+nHvrjTPsUuv4= 196 | go.opentelemetry.io/collector/consumer/consumertest v0.120.0/go.mod h1:HeSnmPfAEBnjsRR5UY1fDTLlSrYsMsUjufg1ihgnFJ0= 197 | go.opentelemetry.io/collector/consumer/xconsumer v0.120.0 h1:dzM/3KkFfMBIvad+NVXDV+mA+qUpHyu5c70TFOjDg68= 198 | go.opentelemetry.io/collector/consumer/xconsumer v0.120.0/go.mod h1:eOf7RX9CYC7bTZQFg0z2GHdATpQDxI0DP36F9gsvXOQ= 199 | go.opentelemetry.io/collector/exporter v0.120.0 h1:8PIJTV0VW1gyr8XuiEMi/aq+baCMdk1hjSrAYiG8aKk= 200 | go.opentelemetry.io/collector/exporter v0.120.0/go.mod h1:JZCNkv0K+Gwdnfwby7Nxc1/gsmy468SBIjI/6fQdxuk= 201 | go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.120.0 h1:Kk+JQekTNJRuORZFe33uDZI5FsOQxmWLl5/Fc9ZFotI= 202 | go.opentelemetry.io/collector/exporter/exporterhelper/xexporterhelper v0.120.0/go.mod h1:1GYlI2dvT0yl+n3KuRI0x+RQr+ppzzGsEUHHY4citk0= 203 | go.opentelemetry.io/collector/exporter/exportertest v0.120.0 h1:7ABriAYXGxvUdCXxe0LpsrMGQ+BP5z/gadm1gRWbD4o= 204 | go.opentelemetry.io/collector/exporter/exportertest v0.120.0/go.mod h1:t0hONsvJp5MM1EF1l83voJHcharIPdnpUBP42UhCoCY= 205 | go.opentelemetry.io/collector/exporter/otlpexporter v0.120.0 h1:oxfHKZHEwN87fycLNt97P1MaijYdR15tsEd4VkZxxss= 206 | go.opentelemetry.io/collector/exporter/otlpexporter v0.120.0/go.mod h1:pI9POQR3jzhOY3+hNNhjpo/yTUrmHHdqvQqyUEO0pCw= 207 | go.opentelemetry.io/collector/exporter/otlphttpexporter v0.120.0 h1:sdP4J5iBHrM1Eudpmf3v4/tGKBilIVN+5qZxCI0ZCDo= 208 | go.opentelemetry.io/collector/exporter/otlphttpexporter v0.120.0/go.mod h1:x+PbUnMkiE5uZti3SSKt6gIJo4WSgtofkzdZ5xjbuWw= 209 | go.opentelemetry.io/collector/exporter/xexporter v0.120.0 h1:HSe3a+0lt/o/g8GgNKgkw9y9vULN4QeY6NeKms8j/GI= 210 | go.opentelemetry.io/collector/exporter/xexporter v0.120.0/go.mod h1:P/87SRTCd/PnQhwAQbELAxotp5gIewT/vpOfEWJZPLk= 211 | go.opentelemetry.io/collector/extension v0.120.0 h1:CA2e6jF5Sz6PE+yxGbJUn0QTMwTo28MO8FNBhdKAABw= 212 | go.opentelemetry.io/collector/extension v0.120.0/go.mod h1:o2/Kk61I1G9XOdD8W4Tbrg05jD4P/QF0ecxYTcT8OZ8= 213 | go.opentelemetry.io/collector/extension/auth v0.120.0 h1:Z4mgQay67BC43F3yK50V/hLdmegBNyMt1upJRV6YW4g= 214 | go.opentelemetry.io/collector/extension/auth v0.120.0/go.mod h1:2DyrUZYNlO3ExAVhflUwvifpxb077Q2aLndcPfkZIzM= 215 | go.opentelemetry.io/collector/extension/auth/authtest v0.120.0 h1:28gD24eaXhHWvquQWWLDpg/L42QOuohuKI7XAYG1jc8= 216 | go.opentelemetry.io/collector/extension/auth/authtest v0.120.0/go.mod h1:+rtuoMo4ZEyWcoUfKQAZIT3Sx1syYRJatLMVWzDPZaE= 217 | go.opentelemetry.io/collector/extension/extensiontest v0.120.0 h1:DSN2cuuQ+CUVEgEStX04lG4rg/6oZeM2zyeX5wXeGWg= 218 | go.opentelemetry.io/collector/extension/extensiontest v0.120.0/go.mod h1:MTFigcQ7hblDUv12b3RbfYvtmzUNZzLiDoug11ezJWQ= 219 | go.opentelemetry.io/collector/extension/xextension v0.120.0 h1:2lwasSQI3Fk6zto7u1uaMqDHESZtdq6a9kaAdCPwwO8= 220 | go.opentelemetry.io/collector/extension/xextension v0.120.0/go.mod h1:9QT+Rq6YniuuKklpeAYpvp9ezPn2bjLOqzsBiFk55DE= 221 | go.opentelemetry.io/collector/featuregate v1.26.0 h1:NIZdJby6jL9tEHI25ddeUNgc09Q0Fof31YHF1CSVp4Y= 222 | go.opentelemetry.io/collector/featuregate v1.26.0/go.mod h1:Y/KsHbvREENKvvN9RlpiWk/IGBK+CATBYzIIpU7nccc= 223 | go.opentelemetry.io/collector/pdata v1.26.0 h1:o7nP0RTQOG0LXk55ZZjLrxwjX8x3wHF7Z7xPeOaskEA= 224 | go.opentelemetry.io/collector/pdata v1.26.0/go.mod h1:18e8/xDZsqyj00h/5HM5GLdJgBzzG9Ei8g9SpNoiMtI= 225 | go.opentelemetry.io/collector/pdata/pprofile v0.120.0 h1:lQl74z41MN9a0M+JFMZbJVesjndbwHXwUleVrVcTgc8= 226 | go.opentelemetry.io/collector/pdata/pprofile v0.120.0/go.mod h1:4zwhklS0qhjptF5GUJTWoCZSTYE+2KkxYrQMuN4doVI= 227 | go.opentelemetry.io/collector/pdata/testdata v0.120.0 h1:Zp0LBOv3yzv/lbWHK1oht41OZ4WNbaXb70ENqRY7HnE= 228 | go.opentelemetry.io/collector/pdata/testdata v0.120.0/go.mod h1:PfezW5Rzd13CWwrElTZRrjRTSgMGUOOGLfHeBjj+LwY= 229 | go.opentelemetry.io/collector/pipeline v0.120.0 h1:QQQbnLCYiuOqmxIRQ11cvFGt+SXq0rypK3fW8qMkzqQ= 230 | go.opentelemetry.io/collector/pipeline v0.120.0/go.mod h1:TO02zju/K6E+oFIOdi372Wk0MXd+Szy72zcTsFQwXl4= 231 | go.opentelemetry.io/collector/pipeline/xpipeline v0.120.0 h1:klY22BaRMO1+JmjUu0Af961hpHA5qnOTAVR7tN+UTW8= 232 | go.opentelemetry.io/collector/pipeline/xpipeline v0.120.0/go.mod h1:K/7Ki7toZQpNV0GF7TbrOEoo8dP3dDXKKSRNnTyEsBE= 233 | go.opentelemetry.io/collector/receiver v0.120.0 h1:JTnPqmBLRXpOyLPh8Kch/5C8SivnpYK9Lzy4PvtEnLQ= 234 | go.opentelemetry.io/collector/receiver v0.120.0/go.mod h1:jpYY55wTVE0FqiBIJrNv2HrvSUnGEjLS/3CWGA+CeL4= 235 | go.opentelemetry.io/collector/receiver/receivertest v0.120.0 h1:Op9yCT0kGvqPF0BB83+iOcsxJJHPCLeL4f4/Op1MBoI= 236 | go.opentelemetry.io/collector/receiver/receivertest v0.120.0/go.mod h1:lpFA4FzcHWki7rLzsNncYmDZ4f7Eik8JY1Mmsaw5uMw= 237 | go.opentelemetry.io/collector/receiver/xreceiver v0.120.0 h1:+gHYd9rTBRKSQfWsTzV2wlwfaVL/LZSz5wu4sygZH7w= 238 | go.opentelemetry.io/collector/receiver/xreceiver v0.120.0/go.mod h1:dkHpL1QqLi/G+60VZnfFpZQf9qoxDVnp6G9FuAcMgfk= 239 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE= 240 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4= 241 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= 242 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= 243 | go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= 244 | go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= 245 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= 246 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= 247 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= 248 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= 249 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs= 250 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4= 251 | go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= 252 | go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= 253 | go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= 254 | go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= 255 | go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= 256 | go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= 257 | go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= 258 | go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= 259 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 260 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 261 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 262 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 263 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 264 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 265 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 266 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 267 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 268 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 269 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 270 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 271 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 272 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 273 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 274 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 275 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 276 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 277 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 278 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 279 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 280 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 281 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 282 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 283 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 284 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 285 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 286 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 287 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 288 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 289 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 290 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 291 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 292 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 293 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 294 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 295 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 296 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 297 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 298 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 299 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 300 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 301 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 302 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 303 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 304 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 305 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 306 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 307 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 308 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 309 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 310 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 312 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 313 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 314 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 315 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 316 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 317 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 318 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 319 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 320 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 327 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 328 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 329 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 330 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 331 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 332 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 333 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 334 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 335 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 336 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 337 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 338 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 339 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 340 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 341 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 342 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 343 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 344 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 345 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 346 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 347 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 348 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 349 | golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= 350 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 351 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 352 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 353 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 354 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 355 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 356 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 357 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 358 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 359 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 360 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 361 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 362 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 363 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 364 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 365 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 366 | golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= 367 | golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 368 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 369 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 370 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 371 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 372 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 373 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 374 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 375 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 376 | golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= 377 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 378 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 379 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= 380 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 381 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 382 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 383 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 384 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 385 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= 386 | google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= 387 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= 388 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= 389 | google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= 390 | google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= 391 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 392 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 393 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 394 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 395 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 396 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 397 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 398 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 399 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 400 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 401 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 402 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 403 | gopkg.in/guregu/null.v3 v3.5.0 h1:xTcasT8ETfMcUHn0zTvIYtQud/9Mx5dJqD554SZct0o= 404 | gopkg.in/guregu/null.v3 v3.5.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= 405 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 406 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 407 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 408 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 409 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 410 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 411 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 412 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 413 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 414 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 415 | gotest.tools/gotestsum v1.12.0 h1:CmwtaGDkHxrZm4Ib0Vob89MTfpc3GrEFMJKovliPwGk= 416 | gotest.tools/gotestsum v1.12.0/go.mod h1:fAvqkSptospfSbQw26CTYzNwnsE/ztqLeyhP0h67ARY= 417 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 418 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 419 | -------------------------------------------------------------------------------- /golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 2m 3 | 4 | issues-exit-code: 1 5 | tests: true 6 | 7 | linters: 8 | enable: 9 | - errcheck 10 | - goconst 11 | - gofmt 12 | - goimports 13 | - gosimple 14 | - govet 15 | - ineffassign 16 | - misspell 17 | - revive 18 | - staticcheck 19 | - typecheck 20 | - unconvert 21 | - unparam 22 | - unused 23 | 24 | linter-settings: 25 | 26 | issues: 27 | exclude: 28 | - "var-naming: don't use an underscore in package name" 29 | - "redefines-builtin-id: redefinition of the built-in function max" 30 | - "redefines-builtin-id: redefinition of the built-in function min" 31 | -------------------------------------------------------------------------------- /pkg/random/random.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "encoding/binary" 6 | "fmt" 7 | "math/rand/v2" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "go.opentelemetry.io/collector/pdata/pcommon" 13 | ) 14 | 15 | var ( 16 | letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 17 | httpStatusesSuccess = []int64{200, 201, 202, 204} 18 | httpStatusesError = []int64{400, 401, 403, 404, 405, 406, 408, 409, 410, 411, 412, 413, 414, 415, 417, 428, 427, 500, 501, 502} 19 | httpMethods = []string{http.MethodGet, http.MethodDelete, http.MethodPost, http.MethodPut, http.MethodPatch} 20 | httpContentTypes = []string{"application/json", "application/xml", "application/x-www-form-urlencoded", "text/plain", "text/html"} 21 | operations = []string{"get", "list", "query", "search", "set", "add", "create", "update", "send", "remove", "delete"} 22 | serviceSuffix = []string{"", "", "service", "backend", "api", "proxy", "engine"} 23 | dbNames = []string{"redis", "mysql", "postgres", "memcached", "mongodb", "elasticsearch"} 24 | resources = []string{ 25 | "order", "payment", "customer", "product", "stock", "inventory", 26 | "shipping", "billing", "checkout", "cart", "search", "analytics"} 27 | 28 | // rnd contains rand.Rand instance protected by a mutex 29 | rnd = struct { 30 | sync.Mutex 31 | *rand.Rand 32 | }{} 33 | ) 34 | 35 | func init() { 36 | var seed [32]byte 37 | _, err := crand.Read(seed[:]) 38 | if err != nil { 39 | panic(err) 40 | } 41 | rnd.Rand = rand.New(rand.NewChaCha8(seed)) 42 | } 43 | 44 | func Float32() float32 { 45 | rnd.Lock() 46 | defer rnd.Unlock() 47 | return rnd.Float32() 48 | } 49 | 50 | func IntN(n int) int { 51 | rnd.Lock() 52 | defer rnd.Unlock() 53 | return rnd.IntN(n) 54 | } 55 | 56 | func SelectElement[T any](elements []T) T { 57 | rnd.Lock() 58 | defer rnd.Unlock() 59 | return elements[rnd.IntN(len(elements))] 60 | } 61 | 62 | func String(n int) string { 63 | s := make([]rune, n) 64 | for i := range s { 65 | s[i] = SelectElement(letters) 66 | } 67 | return string(s) 68 | } 69 | 70 | func K6String(n int) string { 71 | return "k6." + String(n) 72 | } 73 | 74 | func IntBetween(min, max int) int { 75 | rnd.Lock() 76 | defer rnd.Unlock() 77 | n := rnd.IntN(max - min) 78 | return min + n 79 | } 80 | 81 | func Duration(min, max time.Duration) time.Duration { 82 | rnd.Lock() 83 | defer rnd.Unlock() 84 | n := rnd.Int64N(int64(max) - int64(min)) 85 | return min + time.Duration(n) 86 | } 87 | 88 | func IPAddr() string { 89 | rnd.Lock() 90 | defer rnd.Unlock() 91 | return fmt.Sprintf("192.168.%d.%d", rnd.IntN(255), rnd.IntN(255)) 92 | } 93 | 94 | func Port() int { 95 | return IntBetween(8000, 9000) 96 | } 97 | 98 | func HTTPStatusSuccess() int64 { 99 | return SelectElement(httpStatusesSuccess) 100 | } 101 | 102 | func HTTPStatusErr() int64 { 103 | return SelectElement(httpStatusesError) 104 | } 105 | 106 | func HTTPMethod() string { 107 | return SelectElement(httpMethods) 108 | } 109 | 110 | func HTTPContentType() []any { 111 | return []any{SelectElement(httpContentTypes)} 112 | } 113 | 114 | func DBService() string { 115 | return SelectElement(dbNames) 116 | } 117 | 118 | func Service() string { 119 | resource := SelectElement(resources) 120 | return ServiceForResource(resource) 121 | } 122 | 123 | func ServiceForResource(resource string) string { 124 | name := resource 125 | suffix := SelectElement(serviceSuffix) 126 | if suffix != "" { 127 | name = name + "-" + suffix 128 | } 129 | return name 130 | } 131 | 132 | func Operation() string { 133 | resource := SelectElement(resources) 134 | return OperationForResource(resource) 135 | } 136 | 137 | func OperationForResource(resource string) string { 138 | op := SelectElement(operations) 139 | return op + "-" + resource 140 | } 141 | 142 | func TraceID() pcommon.TraceID { 143 | rnd.Lock() 144 | defer rnd.Unlock() 145 | 146 | var b [16]byte 147 | binary.BigEndian.PutUint64(b[:8], rnd.Uint64()) 148 | binary.BigEndian.PutUint64(b[8:], rnd.Uint64()) 149 | return b 150 | } 151 | 152 | func SpanID() pcommon.SpanID { 153 | rnd.Lock() 154 | defer rnd.Unlock() 155 | 156 | var b [8]byte 157 | binary.BigEndian.PutUint64(b[:], rnd.Uint64()) 158 | return b 159 | } 160 | 161 | func EventName() string { 162 | return "event_k6." + String(10) 163 | } 164 | -------------------------------------------------------------------------------- /pkg/random/random_test.go: -------------------------------------------------------------------------------- 1 | package random 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "go.opentelemetry.io/collector/pdata/pcommon" 11 | ) 12 | 13 | const ( 14 | testRounds = 10 15 | ) 16 | 17 | func TestSelectElement(t *testing.T) { 18 | var prev string 19 | var eqCount int 20 | 21 | for i := 0; i < testRounds; i++ { 22 | res := SelectElement(resources) 23 | if res == prev { 24 | eqCount++ 25 | } 26 | 27 | assert.Contains(t, resources, res) 28 | prev = res 29 | } 30 | 31 | assert.Less(t, eqCount, 4, "too many equal selections") 32 | } 33 | 34 | func TestString(t *testing.T) { 35 | for n := 5; n <= 20; n += 5 { 36 | t.Run(fmt.Sprintf("length_%d", n), func(t *testing.T) { 37 | var prev string 38 | for i := 0; i < testRounds; i++ { 39 | s := String(n) 40 | assert.Len(t, s, n) 41 | assert.NotEqual(t, prev, s) 42 | prev = s 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestK6String(t *testing.T) { 49 | for n := 5; n <= 20; n += 5 { 50 | t.Run(fmt.Sprintf("length_%d", n), func(t *testing.T) { 51 | var prev string 52 | for i := 0; i < testRounds; i++ { 53 | s := K6String(n) 54 | assert.Len(t, s, n+3) 55 | assert.Equal(t, "k6.", s[:3]) 56 | assert.NotEqual(t, prev, s) 57 | prev = s 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestIntBetween(t *testing.T) { 64 | const ( 65 | min = 15 66 | max = 25 67 | ) 68 | 69 | var prev, eqCount int 70 | for i := 0; i < testRounds; i++ { 71 | n := IntBetween(min, max) 72 | if n == prev { 73 | eqCount++ 74 | } 75 | 76 | assert.GreaterOrEqual(t, n, min) 77 | assert.Less(t, n, max) 78 | prev = n 79 | } 80 | 81 | assert.Less(t, eqCount, 4, "too many equal random numbers") 82 | } 83 | 84 | func TestDBService(t *testing.T) { 85 | db := DBService() 86 | 87 | assert.Contains(t, dbNames, db) 88 | } 89 | 90 | func TestOperation(t *testing.T) { 91 | op := Operation() 92 | 93 | parts := strings.Split(op, "-") 94 | require.Equal(t, 2, len(parts)) 95 | assert.Contains(t, operations, parts[0]) 96 | assert.Contains(t, resources, parts[1]) 97 | } 98 | 99 | func TestService(t *testing.T) { 100 | srv := Service() 101 | 102 | parts := strings.Split(srv, "-") 103 | assert.Contains(t, resources, parts[0]) 104 | if len(parts) > 1 { 105 | assert.Contains(t, serviceSuffix, parts[1]) 106 | } 107 | } 108 | 109 | func TestSpanID(t *testing.T) { 110 | var prev pcommon.SpanID 111 | for i := 0; i < testRounds; i++ { 112 | id := SpanID() 113 | assert.False(t, id.IsEmpty()) 114 | assert.NotEqual(t, prev, id) 115 | prev = id 116 | } 117 | } 118 | 119 | func TestTraceID(t *testing.T) { 120 | var prev pcommon.TraceID 121 | for i := 0; i < testRounds; i++ { 122 | id := TraceID() 123 | assert.False(t, id.IsEmpty()) 124 | assert.NotEqual(t, prev, id) 125 | prev = id 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pkg/tracegen/parameterized.go: -------------------------------------------------------------------------------- 1 | package tracegen 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strconv" 7 | "time" 8 | "unsafe" 9 | 10 | "github.com/grafana/xk6-client-tracing/pkg/random" 11 | "go.opentelemetry.io/collector/pdata/pcommon" 12 | "go.opentelemetry.io/collector/pdata/ptrace" 13 | ) 14 | 15 | const ( 16 | defaultTraceCount = 1 17 | defaultSpanCount = 10 18 | defaultSpanSize = 1000 19 | defaultResourceSize = 0 20 | ) 21 | 22 | type TraceParams struct { 23 | ID string `json:"id"` 24 | ParentID string `json:"parent_id"` 25 | RandomServiceName bool `json:"random_service_name"` 26 | ResourceSize int `json:"resource_size"` 27 | Count int `json:"count"` 28 | Spans SpanParams `json:"spans"` 29 | } 30 | 31 | type SpanParams struct { 32 | Count int `json:"count"` 33 | Size int `json:"size"` 34 | RandomName bool `json:"random_name"` 35 | FixedAttrs map[string]interface{} `json:"fixed_attrs"` 36 | } 37 | 38 | func (tp *TraceParams) setDefaults() { 39 | if tp.Count == 0 { 40 | tp.Count = defaultTraceCount 41 | } 42 | if tp.ResourceSize <= 0 { 43 | tp.ResourceSize = defaultResourceSize 44 | } 45 | if tp.Spans.Count == 0 { 46 | tp.Spans.Count = defaultSpanCount 47 | } 48 | if tp.Spans.Size <= 0 { 49 | tp.Spans.Size = defaultSpanSize 50 | } 51 | } 52 | 53 | func NewParameterizedGenerator(traceParams []*TraceParams) *ParameterizedGenerator { 54 | for _, tp := range traceParams { 55 | tp.setDefaults() 56 | } 57 | 58 | return &ParameterizedGenerator{ 59 | traceParams: traceParams, 60 | } 61 | } 62 | 63 | type ParameterizedGenerator struct { 64 | traceParams []*TraceParams 65 | } 66 | 67 | func (g *ParameterizedGenerator) Traces() ptrace.Traces { 68 | traceData := ptrace.NewTraces() 69 | 70 | resourceSpans := traceData.ResourceSpans() 71 | resourceSpans.EnsureCapacity(len(g.traceParams)) 72 | 73 | for _, te := range g.traceParams { 74 | rspan := resourceSpans.AppendEmpty() 75 | serviceName := random.Service() 76 | if te.RandomServiceName { 77 | serviceName += "." + random.String(5) 78 | } 79 | resourceAttributes := g.constructAttributes(te.ResourceSize) 80 | resourceAttributes.CopyTo(rspan.Resource().Attributes()) 81 | rspan.Resource().Attributes().PutStr("k6", "true") 82 | rspan.Resource().Attributes().PutStr("service.name", serviceName) 83 | 84 | ilss := rspan.ScopeSpans() 85 | ilss.EnsureCapacity(1) 86 | ils := ilss.AppendEmpty() 87 | ils.Scope().SetName("k6-scope-name/" + random.String(15)) 88 | ils.Scope().SetVersion("k6-scope-version:v" + strconv.Itoa(random.IntBetween(0, 99)) + "." + strconv.Itoa(random.IntBetween(0, 99))) 89 | 90 | for range te.Count { 91 | // Randomize traceID every time if we're generating multiple traces 92 | if te.ID == "" || te.Count > 1 { 93 | te.ID = random.TraceID().String() 94 | te.ParentID = "" 95 | } 96 | 97 | // Spans 98 | sps := ils.Spans() 99 | sps.EnsureCapacity(te.Spans.Count) 100 | for e := range te.Spans.Count { 101 | if e == 0 { 102 | g.generateSpan(te, sps.AppendEmpty()) 103 | idxSpan := sps.At(0) 104 | te.ParentID = idxSpan.SpanID().String() 105 | } else { 106 | g.generateSpan(te, sps.AppendEmpty()) 107 | } 108 | } 109 | } 110 | } 111 | 112 | return traceData 113 | } 114 | 115 | func (g *ParameterizedGenerator) generateSpan(t *TraceParams, dest ptrace.Span) { 116 | endTime := time.Now().Round(time.Second) 117 | startTime := endTime.Add(-time.Duration(random.IntN(500)+10) * time.Millisecond) 118 | 119 | var traceID pcommon.TraceID 120 | b, _ := hex.DecodeString(t.ID) 121 | copy(traceID[:], b) 122 | 123 | spanName := random.Operation() 124 | if t.Spans.RandomName { 125 | spanName += "." + random.String(5) 126 | } 127 | 128 | span := ptrace.NewSpan() 129 | span.SetTraceID(traceID) 130 | if t.ParentID != "" { 131 | var parentID pcommon.SpanID 132 | p, _ := hex.DecodeString(t.ParentID) 133 | copy(parentID[:], p) 134 | span.SetParentSpanID(parentID) 135 | } 136 | span.SetSpanID(random.SpanID()) 137 | span.SetName(spanName) 138 | span.SetKind(ptrace.SpanKindClient) 139 | span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) 140 | span.SetEndTimestamp(pcommon.NewTimestampFromTime(endTime)) 141 | span.TraceState().FromRaw("ot=x:y") 142 | 143 | event := span.Events().AppendEmpty() 144 | event.SetName(random.K6String(12)) 145 | event.SetTimestamp(pcommon.NewTimestampFromTime(startTime)) 146 | event.Attributes().PutStr(random.K6String(5), random.K6String(12)) 147 | 148 | link := span.Links().AppendEmpty() 149 | link.SetTraceID(traceID) 150 | link.SetSpanID(random.SpanID()) 151 | link.Attributes().PutStr(random.K6String(12), random.K6String(12)) 152 | 153 | status := span.Status() 154 | status.SetCode(1) 155 | status.SetMessage("OK") 156 | 157 | attrs := g.constructAttributes(t.Spans.Size) 158 | g.constructSpanAttributes(t.Spans.FixedAttrs, attrs) 159 | 160 | attrs.CopyTo(span.Attributes()) 161 | span.CopyTo(dest) 162 | } 163 | 164 | func (g *ParameterizedGenerator) constructSpanAttributes(attributes map[string]interface{}, dst pcommon.Map) { 165 | attrs := pcommon.NewMap() 166 | for key, value := range attributes { 167 | if cast, ok := value.(int); ok { 168 | attrs.PutInt(key, int64(cast)) 169 | } else if cast, ok := value.(int64); ok { 170 | attrs.PutInt(key, cast) 171 | } else { 172 | attrs.PutStr(key, fmt.Sprintf("%v", value)) 173 | } 174 | } 175 | attrs.CopyTo(dst) 176 | } 177 | 178 | func (g *ParameterizedGenerator) constructAttributes(size int) pcommon.Map { 179 | attrs := pcommon.NewMap() 180 | 181 | // Fill the span with some random data 182 | var currentSize int64 183 | for { 184 | if currentSize >= int64(size) { 185 | break 186 | } 187 | 188 | rKey := random.K6String(random.IntN(15) + 1) 189 | rVal := random.K6String(random.IntN(15) + 1) 190 | attrs.PutStr(rKey, rVal) 191 | 192 | currentSize += int64(unsafe.Sizeof(rKey)) + int64(unsafe.Sizeof(rVal)) 193 | } 194 | 195 | return attrs 196 | } 197 | -------------------------------------------------------------------------------- /pkg/tracegen/parameterized_test.go: -------------------------------------------------------------------------------- 1 | package tracegen 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "go.opentelemetry.io/collector/pdata/ptrace" 9 | ) 10 | 11 | func TestParameterizedGenerator_Traces(t *testing.T) { 12 | // Create a simple trace parameter with minimal configuration 13 | traceParams := []*TraceParams{ 14 | { 15 | ID: "1234567890abcdef1234567890abcdef", 16 | Count: 1, 17 | RandomServiceName: false, 18 | ResourceSize: 2, 19 | Spans: SpanParams{ 20 | Count: 2, 21 | Size: 2, 22 | RandomName: false, 23 | FixedAttrs: map[string]interface{}{ 24 | "test.attr": "test.value", 25 | }, 26 | }, 27 | }, 28 | } 29 | 30 | generator := NewParameterizedGenerator(traceParams) 31 | traces := generator.Traces() 32 | 33 | // Basic validation 34 | require.Equal(t, 1, traces.ResourceSpans().Len(), "Should have one resource span") 35 | 36 | // Validate resource span 37 | rs := traces.ResourceSpans().At(0) 38 | attrs := rs.Resource().Attributes() 39 | _, hasServiceName := attrs.Get("service.name") 40 | _, hasK6 := attrs.Get("k6") 41 | assert.True(t, hasServiceName, "Should have service.name attribute") 42 | assert.True(t, hasK6, "Should have k6 attribute") 43 | k6Val, _ := attrs.Get("k6") 44 | assert.Equal(t, "true", k6Val.Str(), "k6 attribute should be true") 45 | 46 | // Validate scope spans 47 | require.Equal(t, 1, rs.ScopeSpans().Len(), "Should have one scope span") 48 | ils := rs.ScopeSpans().At(0) 49 | assert.Contains(t, ils.Scope().Name(), "k6-scope-name/", "Scope name should have prefix") 50 | assert.Contains(t, ils.Scope().Version(), "k6-scope-version:v", "Scope version should have prefix") 51 | 52 | // Validate spans 53 | require.Equal(t, 2, ils.Spans().Len(), "Should have two spans") 54 | 55 | // Validate first span (parent) 56 | span1 := ils.Spans().At(0) 57 | assert.Equal(t, "1234567890abcdef1234567890abcdef", span1.TraceID().String(), "TraceID should match") 58 | assert.Equal(t, ptrace.SpanKindClient, span1.Kind(), "Span kind should be client") 59 | span1Attrs := span1.Attributes() 60 | _, hasTestAttr := span1Attrs.Get("test.attr") 61 | assert.True(t, hasTestAttr, "Should have fixed attribute") 62 | testAttrVal, _ := span1Attrs.Get("test.attr") 63 | assert.Equal(t, "test.value", testAttrVal.Str(), "Fixed attribute value should match") 64 | assert.Equal(t, 1, span1.Events().Len(), "Should have one event") 65 | assert.Equal(t, 1, span1.Links().Len(), "Should have one link") 66 | 67 | // Validate second span (child) 68 | span2 := ils.Spans().At(1) 69 | assert.Equal(t, "1234567890abcdef1234567890abcdef", span2.TraceID().String(), "TraceID should match") 70 | assert.Equal(t, span1.SpanID(), span2.ParentSpanID(), "Parent span ID should match first span's ID") 71 | assert.Equal(t, ptrace.SpanKindClient, span2.Kind(), "Span kind should be client") 72 | span2Attrs := span2.Attributes() 73 | _, hasTestAttr2 := span2Attrs.Get("test.attr") 74 | assert.True(t, hasTestAttr2, "Should have fixed attribute") 75 | testAttrVal2, _ := span2Attrs.Get("test.attr") 76 | assert.Equal(t, "test.value", testAttrVal2.Str(), "Fixed attribute value should match") 77 | assert.Equal(t, 1, span2.Events().Len(), "Should have one event") 78 | assert.Equal(t, 1, span2.Links().Len(), "Should have one link") 79 | } 80 | -------------------------------------------------------------------------------- /pkg/tracegen/templated.go: -------------------------------------------------------------------------------- 1 | package tracegen 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/grafana/xk6-client-tracing/pkg/random" 13 | "github.com/grafana/xk6-client-tracing/pkg/util" 14 | "go.opentelemetry.io/collector/pdata/pcommon" 15 | "go.opentelemetry.io/collector/pdata/ptrace" 16 | ) 17 | 18 | // OTelSemantics describes a specific set of OpenTelemetry semantic conventions. 19 | type OTelSemantics string 20 | 21 | const ( 22 | SemanticsHTTP OTelSemantics = "http" 23 | SemanticsDB OTelSemantics = "db" 24 | 25 | defaultMinDuration = time.Millisecond * 500 26 | defaultMaxDuration = time.Millisecond * 800 27 | defaultRandomAttributeCardinality = 20 28 | randomAttributeKeySize = 15 29 | randomAttributeValueSize = 30 30 | ) 31 | 32 | // Range represents and interval with the given upper and lower bound [Max, Min) 33 | type Range struct { 34 | Min int64 35 | Max int64 36 | } 37 | 38 | // AttributeParams describe how random attributes should be created. 39 | type AttributeParams struct { 40 | // Count the number of attributes to create. 41 | Count int 42 | // Cardinality how many distinct values are created for each attribute. 43 | Cardinality *int 44 | } 45 | 46 | // SpanDefaults contains template parameters that are applied to all generated spans. 47 | type SpanDefaults struct { 48 | // AttributeSemantics whether to create attributes that follow specific OpenTelemetry semantics. 49 | AttributeSemantics *OTelSemantics `js:"attributeSemantics"` 50 | // Attributes that are added to each span. 51 | Attributes map[string]interface{} `js:"attributes"` 52 | // RandomAttributes random attributes generated for each span. 53 | RandomAttributes *AttributeParams `js:"randomAttributes"` 54 | // Random events generated for each span 55 | RandomEvents *EventParams `js:"randomEvents"` 56 | // Random links generated for each span 57 | RandomLinks *LinkParams `js:"randomLinks"` 58 | // Resource controls the default attributes for all resources. 59 | Resource *ResourceTemplate `js:"resource"` 60 | } 61 | 62 | // SpanTemplate parameters that define how a span is created. 63 | type SpanTemplate struct { 64 | // Service is used to set the service.name attribute of the corresponding resource span. 65 | Service string `js:"service"` 66 | // Name represents the name of the span. If empty, the name will be randomly generated. 67 | Name *string `js:"name"` 68 | // ParentIDX defines the index of the parent span in TraceTemplate.Spans. ParentIDX must be smaller than the 69 | // own index. If empty, the parent is the span with the position directly before this span in TraceTemplate.Spans. 70 | ParentIDX *int `js:"parentIdx"` 71 | // Duration defines the interval for the generated span duration. If missing, a random duration is generated that 72 | // is shorter than the duration of the parent span. 73 | Duration *Range `js:"duration"` 74 | // AttributeSemantics can be set in order to generate attributes that follow a certain OpenTelemetry semantic 75 | // convention. So far only semantic convention for HTTP requests is supported. 76 | AttributeSemantics *OTelSemantics `js:"attributeSemantics"` 77 | // Attributes that are added to this span. 78 | Attributes map[string]interface{} `js:"attributes"` 79 | // RandomAttributes parameters to configure the creation of random attributes. If missing, no random attributes 80 | // are added to the span. 81 | RandomAttributes *AttributeParams `js:"randomAttributes"` 82 | // List of events for the span with specific parameters 83 | Events []Event `js:"events"` 84 | // List of links for the span with specific parameters 85 | Links []Link `js:"links"` 86 | // Generate random events for the span 87 | RandomEvents *EventParams `js:"randomEvents"` 88 | // Generate random links for the span 89 | RandomLinks *LinkParams `js:"randomLinks"` 90 | // Resource controls the attributes generated for the resource. Spans with the same Service will have the same 91 | // resource. Multiple resource definitions will be merged. 92 | Resource *ResourceTemplate `js:"resource"` 93 | } 94 | 95 | type ResourceTemplate struct { 96 | // Attributes that are added to this resource. 97 | Attributes map[string]interface{} `js:"attributes"` 98 | // RandomAttributes parameters to configure the creation of random attributes. If missing, no random attributes 99 | // are added to the resource. 100 | RandomAttributes *AttributeParams `js:"randomAttributes"` 101 | } 102 | 103 | // TraceTemplate describes how all a trace and it's spans are generated. 104 | type TraceTemplate struct { 105 | // Defaults parameters that are applied to each generated span. 106 | Defaults SpanDefaults `js:"defaults"` 107 | // Spans parameters for the individual spans of a trace. 108 | Spans []SpanTemplate `js:"spans"` 109 | } 110 | 111 | type Link struct { 112 | // Attributes for this link 113 | Attributes map[string]interface{} `js:"attributes"` 114 | // Generate random attributes for this link 115 | RandomAttributes *AttributeParams `js:"randomAttributes"` 116 | } 117 | 118 | type Event struct { 119 | // Name of event 120 | Name string `js:"name"` 121 | // Attributes for this event 122 | Attributes map[string]interface{} `js:"attributes"` 123 | // Generate random attributes for this event 124 | RandomAttributes *AttributeParams `js:"randomAttributes"` 125 | } 126 | 127 | type LinkParams struct { 128 | // Count of random links per each span (default: 1) 129 | Count float32 `js:"count"` 130 | // Generate random attributes for this link 131 | RandomAttributes *AttributeParams `js:"randomAttributes"` 132 | } 133 | 134 | type EventParams struct { 135 | // Count of random events per each span 136 | Count float32 `js:"count"` 137 | // ExceptionCount indicates how many exception events to add to the span 138 | ExceptionCount float32 `js:"exceptionCount"` 139 | // ExceptionOnError generates exceptions if status code of the span is >= 400 140 | ExceptionOnError bool `js:"exceptionOnError"` 141 | // Generate random attributes for this event 142 | RandomAttributes *AttributeParams `js:"randomAttributes"` 143 | } 144 | 145 | // NewTemplatedGenerator creates a new trace generator. 146 | func NewTemplatedGenerator(template *TraceTemplate) (*TemplatedGenerator, error) { 147 | gen := &TemplatedGenerator{} 148 | err := gen.initialize(template) 149 | if err != nil { 150 | return nil, fmt.Errorf("fail to create new templated generator: %w", err) 151 | } 152 | return gen, nil 153 | } 154 | 155 | // TemplatedGenerator a trace generator that creates randomized traces based on a given TraceTemplate. 156 | // The generator interprets the template parameters such that realistically looking traces with consistent 157 | // spans and attributes are generated. 158 | type TemplatedGenerator struct { 159 | randomAttributes map[string][]interface{} 160 | resources map[string]*internalResourceTemplate 161 | spans []*internalSpanTemplate 162 | } 163 | 164 | type internalSpanTemplate struct { 165 | idx int 166 | resource *internalResourceTemplate 167 | parent *internalSpanTemplate 168 | name string 169 | kind ptrace.SpanKind 170 | duration *Range 171 | attributeSemantics *OTelSemantics 172 | attributes map[string]interface{} 173 | randomAttributes map[string][]interface{} 174 | events []internalEventTemplate 175 | links []internalLinkTemplate 176 | } 177 | 178 | type internalResourceTemplate struct { 179 | service string 180 | hostName string 181 | hostIP string 182 | transport string 183 | hostPort int 184 | attributes map[string]interface{} 185 | randomAttributes map[string][]interface{} 186 | } 187 | 188 | type internalLinkTemplate struct { 189 | rate float32 190 | attributes map[string]interface{} 191 | randomAttributes map[string][]interface{} 192 | } 193 | 194 | type internalEventTemplate struct { 195 | rate float32 196 | exceptionOnError bool 197 | name string 198 | attributes map[string]interface{} 199 | randomAttributes map[string][]interface{} 200 | } 201 | 202 | // Traces implements Generator for TemplatedGenerator 203 | func (g *TemplatedGenerator) Traces() ptrace.Traces { 204 | var ( 205 | traceID = random.TraceID() 206 | traceData = ptrace.NewTraces() 207 | resSpanSlice = traceData.ResourceSpans() 208 | resSpanMap = map[string]ptrace.ResourceSpans{} 209 | spans []ptrace.Span 210 | ) 211 | 212 | randomTraceAttributes := make(map[string]interface{}, len(g.randomAttributes)) 213 | for k, v := range g.randomAttributes { 214 | randomTraceAttributes[k] = random.SelectElement(v) 215 | } 216 | 217 | for _, tmpl := range g.spans { 218 | // get or generate the corresponding ResourceSpans 219 | resSpans, found := resSpanMap[tmpl.resource.service] 220 | if !found { 221 | resSpans = g.generateResourceSpans(resSpanSlice, tmpl.resource) 222 | resSpanMap[tmpl.resource.service] = resSpans 223 | } 224 | scopeSpans := resSpans.ScopeSpans().At(0) 225 | 226 | // generate new span 227 | var parent *ptrace.Span 228 | if tmpl.parent != nil { 229 | parent = &spans[tmpl.parent.idx] 230 | } 231 | s := g.generateSpan(scopeSpans, tmpl, parent, traceID) 232 | 233 | // attributes 234 | for k, v := range randomTraceAttributes { 235 | if _, found := s.Attributes().Get(k); !found { 236 | _ = s.Attributes().PutEmpty(k).FromRaw(v) 237 | } 238 | } 239 | 240 | spans = append(spans, s) 241 | } 242 | 243 | return traceData 244 | } 245 | 246 | func (g *TemplatedGenerator) generateResourceSpans(resSpanSlice ptrace.ResourceSpansSlice, tmpl *internalResourceTemplate) ptrace.ResourceSpans { 247 | resSpans := resSpanSlice.AppendEmpty() 248 | resSpans.Resource().Attributes().PutStr("k6", "true") 249 | resSpans.Resource().Attributes().PutStr("service.name", tmpl.service) 250 | 251 | for k, v := range tmpl.attributes { 252 | _ = resSpans.Resource().Attributes().PutEmpty(k).FromRaw(v) 253 | } 254 | for k, v := range tmpl.randomAttributes { 255 | _ = resSpans.Resource().Attributes().PutEmpty(k).FromRaw(random.SelectElement(v)) 256 | } 257 | 258 | scopeSpans := resSpans.ScopeSpans().AppendEmpty() 259 | scopeSpans.Scope().SetName("k6-scope-name/" + random.String(15)) 260 | scopeSpans.Scope().SetVersion("k6-scope-version:v" + strconv.Itoa(random.IntBetween(0, 99)) + "." + strconv.Itoa(random.IntBetween(0, 99))) 261 | return resSpans 262 | } 263 | 264 | func (g *TemplatedGenerator) generateSpan(scopeSpans ptrace.ScopeSpans, tmpl *internalSpanTemplate, parent *ptrace.Span, traceID pcommon.TraceID) ptrace.Span { 265 | span := scopeSpans.Spans().AppendEmpty() 266 | 267 | span.SetTraceID(traceID) 268 | span.SetSpanID(random.SpanID()) 269 | if parent != nil { 270 | span.SetParentSpanID(parent.SpanID()) 271 | } 272 | span.SetName(tmpl.name) 273 | span.SetKind(tmpl.kind) 274 | 275 | // set start and end time 276 | var start time.Time 277 | var duration time.Duration 278 | if parent == nil { 279 | start = time.Now().Add(-5 * time.Second) 280 | if tmpl.duration == nil { 281 | duration = random.Duration(defaultMinDuration, defaultMaxDuration) 282 | } 283 | } else { 284 | pStart := parent.StartTimestamp().AsTime() 285 | pDuration := parent.EndTimestamp().AsTime().Sub(pStart) 286 | start = pStart.Add(random.Duration(pDuration/20, pDuration/10)) 287 | if tmpl.duration == nil { 288 | duration = random.Duration(pDuration/2, pDuration-pDuration/10) 289 | } 290 | } 291 | if tmpl.duration != nil { 292 | duration = random.Duration(time.Duration(tmpl.duration.Min)*time.Millisecond, time.Duration(tmpl.duration.Max)*time.Millisecond) 293 | } 294 | end := start.Add(duration) 295 | span.SetStartTimestamp(pcommon.NewTimestampFromTime(start)) 296 | span.SetEndTimestamp(pcommon.NewTimestampFromTime(end)) 297 | 298 | // add attributes 299 | for k, v := range tmpl.attributes { 300 | _ = span.Attributes().PutEmpty(k).FromRaw(v) 301 | } 302 | 303 | for k, v := range tmpl.randomAttributes { 304 | _ = span.Attributes().PutEmpty(k).FromRaw(random.SelectElement(v)) 305 | } 306 | 307 | g.generateNetworkAttributes(tmpl, &span, parent) 308 | if tmpl.attributeSemantics != nil { 309 | switch *tmpl.attributeSemantics { 310 | case SemanticsHTTP: 311 | g.generateHTTPAttributes(tmpl, &span, parent) 312 | } 313 | } 314 | 315 | // generate events 316 | var hasError bool 317 | if st, found := span.Attributes().Get("http.status_code"); found { 318 | hasError = st.Int() >= 400 319 | } else if st, found = span.Attributes().Get("http.response.status_code"); found { 320 | hasError = st.Int() >= 400 321 | } 322 | 323 | span.Events().EnsureCapacity(len(tmpl.events)) 324 | for _, e := range tmpl.events { 325 | if e.rate > 0 && random.Float32() > e.rate { 326 | continue 327 | } 328 | if e.exceptionOnError && !hasError { 329 | continue 330 | } 331 | 332 | event := span.Events().AppendEmpty() 333 | event.Attributes().EnsureCapacity(len(e.attributes) + len(e.randomAttributes)) 334 | 335 | event.SetName(e.name) 336 | eventTime := start.Add(random.Duration(0, duration)) 337 | event.SetTimestamp(pcommon.NewTimestampFromTime(eventTime)) 338 | 339 | for k, v := range e.attributes { 340 | _ = event.Attributes().PutEmpty(k).FromRaw(v) 341 | } 342 | for k, v := range e.randomAttributes { 343 | _ = event.Attributes().PutEmpty(k).FromRaw(random.SelectElement(v)) 344 | } 345 | } 346 | 347 | // generate links 348 | span.Links().EnsureCapacity(len(tmpl.links)) 349 | for _, l := range tmpl.links { 350 | if l.rate > 0 && random.Float32() > l.rate { 351 | continue 352 | } 353 | 354 | link := span.Links().AppendEmpty() 355 | link.Attributes().EnsureCapacity(len(l.attributes) + len(l.randomAttributes)) 356 | for k, v := range l.randomAttributes { 357 | _ = link.Attributes().PutEmpty(k).FromRaw(random.SelectElement(v)) 358 | } 359 | for k, v := range l.attributes { 360 | _ = link.Attributes().PutEmpty(k).FromRaw(v) 361 | } 362 | 363 | // default to linking to parent span if exist 364 | // TODO: support linking to other existing spans 365 | if parent != nil { 366 | link.SetTraceID(traceID) 367 | link.SetSpanID(parent.SpanID()) 368 | } else { 369 | link.SetTraceID(random.TraceID()) 370 | link.SetSpanID(random.SpanID()) 371 | } 372 | } 373 | 374 | return span 375 | } 376 | 377 | func (g *TemplatedGenerator) generateNetworkAttributes(tmpl *internalSpanTemplate, span, parent *ptrace.Span) { 378 | if tmpl.kind == ptrace.SpanKindInternal { 379 | return 380 | } 381 | 382 | putIfNotExists(span.Attributes(), "net.transport", tmpl.resource.transport) 383 | putIfNotExists(span.Attributes(), "net.sock.family", "inet") 384 | if tmpl.kind == ptrace.SpanKindClient { 385 | putIfNotExists(span.Attributes(), "net.peer.port", random.Port()) 386 | } else if tmpl.kind == ptrace.SpanKindServer { 387 | putIfNotExists(span.Attributes(), "net.sock.host.addr", tmpl.resource.hostIP) 388 | putIfNotExists(span.Attributes(), "net.host.name", tmpl.resource.hostName) 389 | putIfNotExists(span.Attributes(), "net.host.port", tmpl.resource.hostPort) 390 | 391 | if parent != nil && parent.Kind() == ptrace.SpanKindClient { 392 | ip, _ := span.Attributes().Get("net.sock.host.addr") 393 | putIfNotExists(parent.Attributes(), "net.sock.peer.addr", ip.Str()) 394 | name, _ := span.Attributes().Get("net.host.name") 395 | putIfNotExists(parent.Attributes(), "net.peer.name", name.Str()) 396 | } 397 | } 398 | } 399 | 400 | func (g *TemplatedGenerator) generateHTTPAttributes(tmpl *internalSpanTemplate, span, parent *ptrace.Span) { 401 | if tmpl.kind == ptrace.SpanKindInternal { 402 | return 403 | } 404 | parentAttr := pcommon.NewMap() 405 | if parent != nil { 406 | parentAttr = parent.Attributes() 407 | } 408 | 409 | putIfNotExists(span.Attributes(), "http.flavor", "1.1") 410 | 411 | if tmpl.kind == ptrace.SpanKindServer { 412 | var method string 413 | if m, found := span.Attributes().Get("http.method"); found { 414 | method = m.Str() 415 | } else if m, found = parentAttr.Get("http.method"); found { 416 | method = m.Str() 417 | } else { 418 | method = random.HTTPMethod() 419 | span.Attributes().PutStr("http.method", method) 420 | } 421 | 422 | var contentType []any 423 | if ct, found := span.Attributes().Get("http.response.header.content-type"); found { 424 | contentType = ct.Slice().AsRaw() 425 | } else { 426 | contentType = random.HTTPContentType() 427 | _ = span.Attributes().PutEmptySlice("http.response.header.content-type").FromRaw(contentType) 428 | } 429 | 430 | var status int64 431 | if st, found := span.Attributes().Get("http.status_code"); found { 432 | status = st.Int() 433 | } else if st, found = parentAttr.Get("http.status_code"); found { 434 | status = st.Int() 435 | } else { 436 | status = random.HTTPStatusSuccess() 437 | span.Attributes().PutInt("http.status_code", status) 438 | } 439 | if status >= 500 { 440 | span.Status().SetCode(ptrace.StatusCodeError) 441 | span.Status().SetMessage(http.StatusText(int(status))) 442 | } 443 | 444 | var requestURL *url.URL 445 | if u, found := span.Attributes().Get("http.url"); found { 446 | requestURL, _ = url.ParseRequestURI(u.Str()) 447 | } else if u, found = parentAttr.Get("http.url"); found { 448 | requestURL, _ = url.ParseRequestURI(u.Str()) 449 | } else { 450 | requestURL, _ = url.ParseRequestURI(fmt.Sprintf("https://%s:%d/%s", tmpl.resource.hostName, tmpl.resource.hostPort, tmpl.name)) 451 | span.Attributes().PutStr("http.url", requestURL.String()) 452 | } 453 | span.Attributes().PutStr("http.scheme", requestURL.Scheme) 454 | span.Attributes().PutStr("http.target", requestURL.Path) 455 | 456 | putIfNotExists(span.Attributes(), "http.response_content_length", random.IntBetween(100_000, 1_000_000)) 457 | if method == http.MethodPatch || method == http.MethodPost || method == http.MethodPut { 458 | putIfNotExists(span.Attributes(), "http.request_content_length", random.IntBetween(10_000, 100_000)) 459 | } 460 | 461 | if parent != nil && parent.Kind() == ptrace.SpanKindClient { 462 | if status >= 400 { 463 | parent.Status().SetCode(ptrace.StatusCodeError) 464 | parent.Status().SetMessage(http.StatusText(int(status))) 465 | } 466 | putIfNotExists(parent.Attributes(), "http.method", method) 467 | putIfNotExists(parent.Attributes(), "http.request.header.accept", contentType) 468 | putIfNotExists(parent.Attributes(), "http.status_code", status) 469 | putIfNotExists(parent.Attributes(), "http.url", requestURL.String()) 470 | res, _ := span.Attributes().Get("http.response_content_length") 471 | putIfNotExists(parent.Attributes(), "http.response_content_length", res.Int()) 472 | if req, found := span.Attributes().Get("http.request_content_length"); found { 473 | putIfNotExists(span.Attributes(), "http.request_content_length", req.Int()) 474 | } 475 | } 476 | } 477 | } 478 | 479 | func (g *TemplatedGenerator) initialize(template *TraceTemplate) error { 480 | g.resources = map[string]*internalResourceTemplate{} 481 | g.randomAttributes = initializeRandomAttributes(template.Defaults.RandomAttributes) 482 | 483 | for i, tmpl := range template.Spans { 484 | // span templates must have a service 485 | if tmpl.Service == "" { 486 | return errors.New("trace template invalid: span template must have a name") 487 | } 488 | 489 | // get or generate the corresponding ResourceSpans 490 | res, found := g.resources[tmpl.Service] 491 | if !found { 492 | res = g.initializeResource(&tmpl, &template.Defaults) 493 | g.resources[tmpl.Service] = res 494 | } else { 495 | g.amendInitializedResource(res, &tmpl) 496 | } 497 | 498 | // span template parent index must reference a previous span 499 | if tmpl.ParentIDX != nil && (*tmpl.ParentIDX >= i || *tmpl.ParentIDX < 0) { 500 | return errors.New("trace template invalid: span index must be greater than span parent index") 501 | } 502 | 503 | // initialize span using information from the parent span, the template and child template 504 | parentIdx := i - 1 505 | if tmpl.ParentIDX != nil { 506 | parentIdx = *tmpl.ParentIDX 507 | } 508 | var parent *internalSpanTemplate 509 | if parentIdx >= 0 { 510 | parent = g.spans[parentIdx] 511 | } 512 | 513 | var child *SpanTemplate 514 | for j := i + 1; j < len(template.Spans); j++ { 515 | n := template.Spans[j] 516 | if n.ParentIDX == nil || *n.ParentIDX == i { 517 | child = &n 518 | break 519 | } 520 | } 521 | 522 | span, err := g.initializeSpan(i, parent, &template.Defaults, &tmpl, child) 523 | if err != nil { 524 | return err 525 | } 526 | g.spans = append(g.spans, span) 527 | } 528 | 529 | return nil 530 | } 531 | 532 | func (g *TemplatedGenerator) initializeResource(tmpl *SpanTemplate, defaults *SpanDefaults) *internalResourceTemplate { 533 | res := internalResourceTemplate{ 534 | service: tmpl.Service, 535 | hostName: fmt.Sprintf("%s.local", tmpl.Service), 536 | hostIP: random.IPAddr(), 537 | hostPort: random.Port(), 538 | transport: "ip_tcp", 539 | } 540 | 541 | // use defaults if no resource attributes are set 542 | if tmpl.Resource == nil { 543 | tmpl.Resource = defaults.Resource 544 | } 545 | 546 | if tmpl.Resource != nil { 547 | res.randomAttributes = initializeRandomAttributes(tmpl.Resource.RandomAttributes) 548 | res.attributes = tmpl.Resource.Attributes 549 | } 550 | 551 | return &res 552 | } 553 | 554 | func (g *TemplatedGenerator) amendInitializedResource(res *internalResourceTemplate, tmpl *SpanTemplate) { 555 | if tmpl.Resource == nil { 556 | return 557 | } 558 | 559 | if tmpl.Resource.RandomAttributes != nil { 560 | randAttr := initializeRandomAttributes(tmpl.Resource.RandomAttributes) 561 | res.randomAttributes = util.MergeMaps(res.randomAttributes, randAttr) 562 | } 563 | if tmpl.Resource.Attributes != nil { 564 | res.attributes = util.MergeMaps(res.attributes, tmpl.Resource.Attributes) 565 | } 566 | } 567 | 568 | func (g *TemplatedGenerator) initializeSpan(idx int, parent *internalSpanTemplate, defaults *SpanDefaults, tmpl, child *SpanTemplate) (*internalSpanTemplate, error) { 569 | res := g.resources[tmpl.Service] 570 | span := internalSpanTemplate{ 571 | idx: idx, 572 | parent: parent, 573 | resource: res, 574 | duration: tmpl.Duration, 575 | } 576 | 577 | // apply defaults 578 | if tmpl.AttributeSemantics == nil { 579 | span.attributeSemantics = defaults.AttributeSemantics 580 | } 581 | span.attributes = util.MergeMaps(defaults.Attributes, tmpl.Attributes) 582 | 583 | // set span name 584 | if tmpl.Name != nil { 585 | span.name = *tmpl.Name 586 | } else { 587 | span.name = random.Operation() 588 | } 589 | 590 | kind, err := initializeSpanKind(parent, tmpl, child) 591 | if err != nil { 592 | return nil, err 593 | } 594 | span.kind = kind 595 | 596 | span.randomAttributes = initializeRandomAttributes(tmpl.RandomAttributes) 597 | 598 | // initialize links for span 599 | span.links = g.initializeLinks(tmpl.Links, tmpl.RandomLinks, defaults.RandomLinks) 600 | 601 | // initialize events for the span 602 | span.events = g.initializeEvents(tmpl.Events, tmpl.RandomEvents, defaults.RandomEvents) 603 | 604 | return &span, nil 605 | } 606 | 607 | func initializeSpanKind(parent *internalSpanTemplate, tmpl, child *SpanTemplate) (ptrace.SpanKind, error) { 608 | var kind ptrace.SpanKind 609 | if k, found := tmpl.Attributes["span.kind"]; found { 610 | kindStr, ok := k.(string) 611 | if !ok { 612 | return ptrace.SpanKindUnspecified, fmt.Errorf("attribute span.kind expected to be a string, but was %T", k) 613 | } 614 | kind = spanKindFromString(kindStr) 615 | } else { 616 | if parent == nil { 617 | if child == nil || tmpl.Service == child.Service { 618 | kind = ptrace.SpanKindServer 619 | } else { 620 | kind = ptrace.SpanKindClient 621 | } 622 | } else { 623 | parentService := parent.resource.service 624 | if tmpl.Service != parentService { 625 | kind = ptrace.SpanKindServer 626 | } else if child != nil && tmpl.Service != child.Service { 627 | kind = ptrace.SpanKindClient 628 | } else { 629 | kind = ptrace.SpanKindInternal 630 | } 631 | } 632 | } 633 | return kind, nil 634 | } 635 | 636 | func spanKindFromString(s string) ptrace.SpanKind { 637 | s = strings.ToLower(s) 638 | s = strings.TrimPrefix(s, "span_kind_") 639 | switch s { 640 | case "internal": 641 | return ptrace.SpanKindInternal 642 | case "server": 643 | return ptrace.SpanKindServer 644 | case "client": 645 | return ptrace.SpanKindClient 646 | case "producer": 647 | return ptrace.SpanKindProducer 648 | case "consumer": 649 | return ptrace.SpanKindConsumer 650 | default: 651 | return ptrace.SpanKindUnspecified 652 | } 653 | } 654 | 655 | func putIfNotExists(m pcommon.Map, k string, v interface{}) { 656 | if _, found := m.Get(k); !found { 657 | _ = m.PutEmpty(k).FromRaw(v) 658 | } 659 | } 660 | 661 | func initializeRandomAttributes(attributeParams *AttributeParams) map[string][]interface{} { 662 | if attributeParams == nil { 663 | return map[string][]interface{}{} 664 | } 665 | 666 | if attributeParams.Cardinality == nil { 667 | tmp := defaultRandomAttributeCardinality 668 | attributeParams.Cardinality = &tmp 669 | } 670 | 671 | attributes := make(map[string][]interface{}, attributeParams.Count) 672 | for i := 0; i < attributeParams.Count; i++ { 673 | key := random.K6String(randomAttributeKeySize) 674 | values := make([]interface{}, 0, *attributeParams.Cardinality) 675 | for j := 0; j < *attributeParams.Cardinality; j++ { 676 | values = append(values, random.String(randomAttributeValueSize)) 677 | } 678 | attributes[key] = values 679 | } 680 | 681 | return attributes 682 | } 683 | 684 | func (g *TemplatedGenerator) initializeEvents(tmplEvents []Event, randomEvents, defaultRandomEvents *EventParams) []internalEventTemplate { 685 | internalEvents := make([]internalEventTemplate, 0, len(tmplEvents)) 686 | for _, e := range tmplEvents { 687 | event := internalEventTemplate{ 688 | name: e.Name, 689 | attributes: e.Attributes, 690 | randomAttributes: initializeRandomAttributes(e.RandomAttributes), 691 | } 692 | internalEvents = append(internalEvents, event) 693 | } 694 | 695 | if randomEvents == nil { 696 | if defaultRandomEvents == nil { 697 | return internalEvents 698 | } 699 | randomEvents = defaultRandomEvents 700 | } 701 | 702 | // normal random events 703 | if randomEvents.Count == 0 { // default count is 1 704 | randomEvents.Count = 1 705 | } 706 | if randomEvents.Count < 1 { 707 | event := internalEventTemplate{ 708 | rate: randomEvents.Count, 709 | name: random.EventName(), 710 | randomAttributes: initializeRandomAttributes(randomEvents.RandomAttributes), 711 | } 712 | internalEvents = append(internalEvents, event) 713 | } else { 714 | for i := 0; i < int(randomEvents.Count); i++ { 715 | event := internalEventTemplate{ 716 | name: random.EventName(), 717 | randomAttributes: initializeRandomAttributes(randomEvents.RandomAttributes), 718 | } 719 | internalEvents = append(internalEvents, event) 720 | } 721 | } 722 | 723 | // random exception events 724 | if randomEvents.ExceptionCount == 0 && randomEvents.ExceptionOnError { 725 | randomEvents.ExceptionCount = 1 // default exception count is 1, if ExceptionOnError is true 726 | } 727 | if randomEvents.ExceptionCount < 1 { 728 | event := internalEventTemplate{ 729 | rate: randomEvents.ExceptionCount, 730 | name: "exception", 731 | attributes: map[string]interface{}{ 732 | "exception.escape": false, 733 | "exception.message": generateRandomExceptionMsg(), 734 | "exception.stacktrace": generateRandomExceptionStackTrace(), 735 | "exception.type": "error.type_" + random.K6String(10), 736 | }, 737 | randomAttributes: initializeRandomAttributes(randomEvents.RandomAttributes), 738 | exceptionOnError: randomEvents.ExceptionOnError, 739 | } 740 | internalEvents = append(internalEvents, event) 741 | } else { 742 | for i := 0; i < int(randomEvents.ExceptionCount); i++ { 743 | event := internalEventTemplate{ 744 | name: "exception", 745 | attributes: map[string]interface{}{ 746 | "exception.escape": false, 747 | "exception.message": generateRandomExceptionMsg(), 748 | "exception.stacktrace": generateRandomExceptionStackTrace(), 749 | "exception.type": "error.type_" + random.K6String(10), 750 | }, 751 | randomAttributes: initializeRandomAttributes(randomEvents.RandomAttributes), 752 | exceptionOnError: randomEvents.ExceptionOnError, 753 | } 754 | internalEvents = append(internalEvents, event) 755 | } 756 | } 757 | 758 | return internalEvents 759 | } 760 | 761 | func generateRandomExceptionMsg() string { 762 | return "error: " + random.K6String(20) 763 | } 764 | 765 | func generateRandomExceptionStackTrace() string { 766 | var ( 767 | panics = []string{"runtime error: index out of range", "runtime error: can't divide by 0"} 768 | functions = []string{"main.main()", "trace.makespan()", "account.login()", "payment.collect()"} 769 | ) 770 | 771 | return "panic: " + random.SelectElement(panics) + "\n" + random.SelectElement(functions) 772 | } 773 | 774 | func (g *TemplatedGenerator) initializeLinks(linkTemplates []Link, randomLinks, defaultRandomLinks *LinkParams) []internalLinkTemplate { 775 | internalLinks := make([]internalLinkTemplate, 0, len(linkTemplates)) 776 | 777 | for _, lt := range linkTemplates { 778 | link := internalLinkTemplate{ 779 | attributes: lt.Attributes, 780 | randomAttributes: initializeRandomAttributes(lt.RandomAttributes), 781 | } 782 | internalLinks = append(internalLinks, link) 783 | } 784 | 785 | if randomLinks == nil { 786 | if defaultRandomLinks == nil { 787 | return internalLinks 788 | } 789 | randomLinks = defaultRandomLinks 790 | } 791 | if randomLinks.Count == 0 { // default count is 1 792 | randomLinks.Count = 1 793 | } 794 | 795 | if randomLinks.Count < 1 { 796 | link := internalLinkTemplate{ 797 | rate: randomLinks.Count, 798 | randomAttributes: initializeRandomAttributes(randomLinks.RandomAttributes), 799 | } 800 | internalLinks = append(internalLinks, link) 801 | } else { 802 | for i := 0; i < int(randomLinks.Count); i++ { 803 | link := internalLinkTemplate{ 804 | randomAttributes: initializeRandomAttributes(randomLinks.RandomAttributes), 805 | } 806 | internalLinks = append(internalLinks, link) 807 | } 808 | } 809 | 810 | return internalLinks 811 | } 812 | -------------------------------------------------------------------------------- /pkg/tracegen/templated_test.go: -------------------------------------------------------------------------------- 1 | package tracegen 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | "go.opentelemetry.io/collector/pdata/pcommon" 10 | "go.opentelemetry.io/collector/pdata/ptrace" 11 | ) 12 | 13 | const testRounds = 5 14 | 15 | func TestTemplatedGenerator_Traces(t *testing.T) { 16 | attributeSemantics := []OTelSemantics{SemanticsHTTP} 17 | template := TraceTemplate{ 18 | Defaults: SpanDefaults{ 19 | Attributes: map[string]interface{}{"fixed.attr": "some-value"}, 20 | RandomAttributes: &AttributeParams{Count: 3}, 21 | }, 22 | Spans: []SpanTemplate{ 23 | {Service: "test-service", Name: ptr("perform-test"), RandomAttributes: &AttributeParams{Count: 2}}, 24 | {Service: "test-service"}, 25 | {Service: "test-service", Name: ptr("get_test_data")}, 26 | {Service: "test-data", Name: ptr("list_test_data"), Attributes: map[string]interface{}{"http.status_code": 400}}, 27 | }, 28 | } 29 | 30 | for _, semantics := range attributeSemantics { 31 | template.Defaults.AttributeSemantics = &semantics 32 | gen, err := NewTemplatedGenerator(&template) 33 | assert.NoError(t, err) 34 | 35 | for range testRounds { 36 | count := 0 37 | for i, span := range iterSpans(gen.Traces()) { 38 | count++ 39 | requireAttributeCountGreaterOrEqual(t, span.Attributes(), 3, "k6.") 40 | if template.Spans[i].Name != nil { 41 | assert.Equal(t, *template.Spans[i].Name, span.Name()) 42 | } 43 | if span.Kind() != ptrace.SpanKindInternal { 44 | requireAttributeCountGreaterOrEqual(t, span.Attributes(), 3, "net.") 45 | if *template.Defaults.AttributeSemantics == SemanticsHTTP { 46 | requireAttributeCountGreaterOrEqual(t, span.Attributes(), 5, "http.") 47 | } 48 | } 49 | } 50 | assert.Equal(t, len(template.Spans), count, "unexpected number of spans") 51 | } 52 | } 53 | } 54 | 55 | func TestTemplatedGenerator_Resource(t *testing.T) { 56 | template := TraceTemplate{ 57 | Defaults: SpanDefaults{ 58 | Attributes: map[string]interface{}{"span-attr": "val-01"}, 59 | Resource: &ResourceTemplate{RandomAttributes: &AttributeParams{Count: 2}}, 60 | }, 61 | Spans: []SpanTemplate{ 62 | {Service: "test-service-a", Name: ptr("action-a-a"), Resource: &ResourceTemplate{ 63 | Attributes: map[string]interface{}{"res-attr-01": "res-val-01"}, 64 | RandomAttributes: &AttributeParams{Count: 5}, 65 | }}, 66 | {Service: "test-service-a", Name: ptr("action-a-b"), Resource: &ResourceTemplate{ 67 | Attributes: map[string]interface{}{"res-attr-02": "res-val-02"}, 68 | }}, 69 | {Service: "test-service-b", Name: ptr("action-b-a"), Resource: &ResourceTemplate{ 70 | Attributes: map[string]interface{}{"res-attr-03": "res-val-03"}, 71 | }}, 72 | {Service: "test-service-b", Name: ptr("action-b-b")}, 73 | }, 74 | } 75 | 76 | gen, err := NewTemplatedGenerator(&template) 77 | require.NoError(t, err) 78 | 79 | for range testRounds { 80 | for _, res := range iterResources(gen.Traces()) { 81 | srv, found := res.Attributes().Get("service.name") 82 | require.True(t, found, "service.name not found") 83 | 84 | switch srv.Str() { 85 | case "test-service-a": 86 | requireAttributeCountEqual(t, res.Attributes(), 5, "k6.") 87 | requireAttributeEqual(t, res.Attributes(), "res-attr-01", "res-val-01") 88 | requireAttributeEqual(t, res.Attributes(), "res-attr-02", "res-val-02") 89 | case "test-service-b": 90 | requireAttributeCountEqual(t, res.Attributes(), 3, "k6.") 91 | requireAttributeEqual(t, res.Attributes(), "res-attr-03", "res-val-03") 92 | default: 93 | require.Fail(t, "unexpected service name %s", srv.Str()) 94 | } 95 | } 96 | } 97 | } 98 | 99 | func TestTemplatedGenerator_EventsLinks(t *testing.T) { 100 | attributeSemantics := []OTelSemantics{SemanticsHTTP} 101 | template := TraceTemplate{ 102 | Defaults: SpanDefaults{ 103 | Attributes: map[string]interface{}{"fixed.attr": "some-value"}, 104 | RandomAttributes: &AttributeParams{Count: 3}, 105 | RandomLinks: &LinkParams{Count: 0.5, RandomAttributes: &AttributeParams{Count: 3}}, 106 | RandomEvents: &EventParams{ExceptionOnError: true, Count: 0.5, RandomAttributes: &AttributeParams{Count: 3}}, 107 | }, 108 | Spans: []SpanTemplate{ 109 | // do not change order of the first one 110 | {Service: "test-service", Name: ptr("only_default")}, 111 | {Service: "test-service", Name: ptr("default_and_template"), Events: []Event{{Name: "event-name", RandomAttributes: &AttributeParams{Count: 2}}}, Links: []Link{{Attributes: map[string]interface{}{"link-attr-key": "link-attr-value"}}}}, 112 | {Service: "test-service", Name: ptr("default_and_random"), RandomEvents: &EventParams{Count: 2, RandomAttributes: &AttributeParams{Count: 1}}, RandomLinks: &LinkParams{Count: 2, RandomAttributes: &AttributeParams{Count: 1}}}, 113 | {Service: "test-service", Name: ptr("default_template_random"), Events: []Event{{Name: "event-name", RandomAttributes: &AttributeParams{Count: 2}}}, Links: []Link{{Attributes: map[string]interface{}{"link-attr-key": "link-attr-value"}}}, RandomEvents: &EventParams{Count: 2, RandomAttributes: &AttributeParams{Count: 1}}, RandomLinks: &LinkParams{Count: 2, RandomAttributes: &AttributeParams{Count: 1}}}, 114 | {Service: "test-service", Name: ptr("default_generate_on_error"), Attributes: map[string]interface{}{"http.status_code": 400}}, 115 | }, 116 | } 117 | 118 | for _, semantics := range attributeSemantics { 119 | template.Defaults.AttributeSemantics = &semantics 120 | gen, err := NewTemplatedGenerator(&template) 121 | assert.NoError(t, err) 122 | 123 | for range testRounds { 124 | count := 0 125 | for _, span := range iterSpans(gen.Traces()) { 126 | count++ 127 | events := span.Events() 128 | links := span.Links() 129 | checkEventsLinksLength := func(expectedTemplate, expectedRandom int, spanName string) { 130 | expected := expectedTemplate + expectedRandom 131 | // because default rate is 0.5 132 | assert.GreaterOrEqual(t, events.Len(), expected, "test name: %s events", spanName) 133 | assert.GreaterOrEqual(t, links.Len(), expected, "test name: %s links", spanName) 134 | assert.LessOrEqual(t, events.Len(), expected+1, "test name: %s events", spanName) 135 | assert.LessOrEqual(t, links.Len(), expected+1, "test name: %s links", spanName) 136 | } 137 | 138 | checkLinks := func() { 139 | for i := 0; i < links.Len(); i++ { 140 | link := links.At(i) 141 | assert.Equal(t, span.TraceID(), link.TraceID()) 142 | assert.Equal(t, span.ParentSpanID(), link.SpanID()) 143 | } 144 | } 145 | 146 | switch span.Name() { 147 | case "only_default": 148 | checkEventsLinksLength(0, 0, span.Name()) 149 | if events.Len() > 0 { 150 | // check default event with 3 random attributes 151 | event := events.At(0) 152 | assert.Equal(t, 3, len(event.Attributes().AsRaw())) 153 | } 154 | if links.Len() > 0 { 155 | // check default link with 3 random attributes 156 | // and not matching trace id and parent span id because this is 157 | // the first span, there is no previous span 158 | link := links.At(0) 159 | assert.Equal(t, 3, len(link.Attributes().AsRaw())) 160 | assert.NotEqual(t, span.TraceID(), link.TraceID()) 161 | assert.NotEqual(t, span.ParentSpanID(), link.SpanID()) 162 | } 163 | case "default_and_template": 164 | checkEventsLinksLength(1, 0, span.Name()) 165 | checkLinks() 166 | case "default_and_random": 167 | checkEventsLinksLength(0, 2, span.Name()) 168 | checkLinks() 169 | case "default_template_random": 170 | checkEventsLinksLength(1, 2, span.Name()) 171 | checkLinks() 172 | case "default_generate_on_error": 173 | // there should be at least one event 174 | assert.GreaterOrEqual(t, events.Len(), 0, "test name: %s events", "default generate on error") 175 | found := false 176 | for i := 0; i < events.Len(); i++ { 177 | event := events.At(i) 178 | if event.Name() == "exception" { 179 | found = true 180 | assert.NotNil(t, event.Attributes().AsRaw()["exception.escape"]) 181 | assert.NotNil(t, event.Attributes().AsRaw()["exception.message"]) 182 | assert.NotNil(t, event.Attributes().AsRaw()["exception.stacktrace"]) 183 | assert.NotNil(t, event.Attributes().AsRaw()["exception.type"]) 184 | } 185 | } 186 | assert.True(t, found, "exception event not found") 187 | } 188 | } 189 | assert.Equal(t, len(template.Spans), count, "unexpected number of spans") 190 | } 191 | } 192 | } 193 | 194 | func iterSpans(traces ptrace.Traces) func(func(i int, e ptrace.Span) bool) { 195 | count := 0 196 | return func(f func(i int, e ptrace.Span) bool) { 197 | var elem ptrace.Span 198 | for i := 0; i < traces.ResourceSpans().Len(); i++ { 199 | rs := traces.ResourceSpans().At(i) 200 | for j := 0; j < rs.ScopeSpans().Len(); j++ { 201 | ss := rs.ScopeSpans().At(j) 202 | for k := 0; k < ss.Spans().Len(); k++ { 203 | elem = ss.Spans().At(k) 204 | if !f(count, elem) { 205 | return 206 | } 207 | count++ 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | func iterResources(traces ptrace.Traces) func(func(i int, e pcommon.Resource) bool) { 215 | return func(f func(i int, e pcommon.Resource) bool) { 216 | var elem pcommon.Resource 217 | for i := 0; i < traces.ResourceSpans().Len(); i++ { 218 | rs := traces.ResourceSpans().At(i) 219 | elem = rs.Resource() 220 | if !f(i, elem) { 221 | return 222 | } 223 | } 224 | } 225 | } 226 | 227 | func requireAttributeCountGreaterOrEqual(t *testing.T, attributes pcommon.Map, compare int, prefixes ...string) { 228 | t.Helper() 229 | count := countAttributes(attributes, prefixes...) 230 | require.GreaterOrEqual(t, count, compare, "expected at least %d attributes, got %d", compare, count) 231 | } 232 | 233 | func requireAttributeCountEqual(t *testing.T, attributes pcommon.Map, expected int, prefixes ...string) { 234 | t.Helper() 235 | count := countAttributes(attributes, prefixes...) 236 | require.GreaterOrEqual(t, expected, count, "expected at least %d attributes, got %d", expected, count) 237 | } 238 | 239 | func requireAttributeEqual(t *testing.T, attributes pcommon.Map, key string, expected any) { 240 | t.Helper() 241 | val, found := attributes.Get(key) 242 | require.True(t, found, "attribute %s not found", key) 243 | require.Equal(t, expected, val.AsRaw(), "value %v expected for attribute %s but was %v", expected, key, val.AsRaw()) 244 | } 245 | 246 | func countAttributes(attributes pcommon.Map, prefixes ...string) int { 247 | var count int 248 | attributes.Range(func(k string, _ pcommon.Value) bool { 249 | if len(prefixes) == 0 { 250 | count++ 251 | return true 252 | } 253 | 254 | for _, prefix := range prefixes { 255 | if strings.HasPrefix(k, prefix) { 256 | count++ 257 | } 258 | } 259 | return true 260 | }) 261 | return count 262 | } 263 | 264 | func ptr[T any](v T) *T { 265 | return &v 266 | } 267 | -------------------------------------------------------------------------------- /pkg/tracegen/tracegen.go: -------------------------------------------------------------------------------- 1 | package tracegen 2 | 3 | import ( 4 | "go.opentelemetry.io/collector/pdata/ptrace" 5 | ) 6 | 7 | // Generator creates traces to be used in k6 tests 8 | type Generator interface { 9 | Traces() ptrace.Traces 10 | } 11 | -------------------------------------------------------------------------------- /pkg/util/maps.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { 4 | var n int 5 | for _, m := range maps { 6 | n += len(m) 7 | } 8 | 9 | merged := make(map[K]V, n) 10 | for _, m := range maps { 11 | for k, v := range m { 12 | merged[k] = v 13 | } 14 | } 15 | 16 | return merged 17 | } 18 | -------------------------------------------------------------------------------- /tracing.go: -------------------------------------------------------------------------------- 1 | package clienttracing 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "fmt" 7 | "os" 8 | "sync" 9 | 10 | "github.com/grafana/sobek" 11 | "go.k6.io/k6/js/common" 12 | "go.k6.io/k6/js/modules" 13 | "go.opentelemetry.io/collector/component" 14 | "go.opentelemetry.io/collector/component/componenttest" 15 | "go.opentelemetry.io/collector/config/configgrpc" 16 | "go.opentelemetry.io/collector/config/confighttp" 17 | "go.opentelemetry.io/collector/config/configopaque" 18 | "go.opentelemetry.io/collector/config/configtls" 19 | "go.opentelemetry.io/collector/exporter" 20 | "go.opentelemetry.io/collector/exporter/otlpexporter" 21 | "go.opentelemetry.io/collector/exporter/otlphttpexporter" 22 | "go.opentelemetry.io/collector/pdata/ptrace" 23 | metricnoop "go.opentelemetry.io/otel/metric/noop" 24 | tracenoop "go.opentelemetry.io/otel/trace/noop" 25 | "go.uber.org/zap" 26 | "go.uber.org/zap/zapcore" 27 | 28 | "github.com/grafana/xk6-client-tracing/pkg/tracegen" 29 | "github.com/grafana/xk6-client-tracing/pkg/util" 30 | ) 31 | 32 | type exporterType string 33 | 34 | const ( 35 | exporterNone exporterType = "" 36 | exporterOTLP exporterType = "otlp" 37 | exporterOTLPHTTP exporterType = "otlphttp" 38 | ) 39 | 40 | var ( 41 | _ modules.Module = &RootModule{} 42 | _ modules.Instance = &TracingModule{} 43 | ) 44 | 45 | func init() { 46 | modules.Register("k6/x/tracing", new(RootModule)) 47 | } 48 | 49 | type RootModule struct { 50 | sync.Mutex 51 | } 52 | 53 | func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance { 54 | return &TracingModule{ 55 | vu: vu, 56 | paramGenerators: make(map[*sobek.Object]*tracegen.ParameterizedGenerator), 57 | templatedGenerators: make(map[*sobek.Object]*tracegen.TemplatedGenerator), 58 | } 59 | } 60 | 61 | type TracingModule struct { 62 | vu modules.VU 63 | client *Client 64 | paramGenerators map[*sobek.Object]*tracegen.ParameterizedGenerator 65 | templatedGenerators map[*sobek.Object]*tracegen.TemplatedGenerator 66 | } 67 | 68 | func (ct *TracingModule) Exports() modules.Exports { 69 | return modules.Exports{ 70 | Named: map[string]interface{}{ 71 | // constants 72 | "SEMANTICS_HTTP": tracegen.SemanticsHTTP, 73 | "SEMANTICS_DB": tracegen.SemanticsDB, 74 | "EXPORTER_OTLP": exporterOTLP, 75 | "EXPORTER_OTLP_HTTP": exporterOTLPHTTP, 76 | // constructors 77 | "Client": ct.newClient, 78 | "ParameterizedGenerator": ct.newParameterizedGenerator, 79 | "TemplatedGenerator": ct.newTemplatedGenerator, 80 | }, 81 | } 82 | } 83 | 84 | func (ct *TracingModule) newClient(g sobek.ConstructorCall, rt *sobek.Runtime) *sobek.Object { 85 | var cfg ClientConfig 86 | err := rt.ExportTo(g.Argument(0), &cfg) 87 | if err != nil { 88 | common.Throw(rt, fmt.Errorf("unable to create client: constructor expects first argument to be ClientConfig: %w", err)) 89 | } 90 | 91 | if ct.client == nil { 92 | ct.client, err = NewClient(&cfg, ct.vu) 93 | if err != nil { 94 | common.Throw(rt, fmt.Errorf("unable to create client: %w", err)) 95 | } 96 | } 97 | 98 | return rt.ToValue(ct.client).ToObject(rt) 99 | } 100 | 101 | func (ct *TracingModule) newParameterizedGenerator(g sobek.ConstructorCall, rt *sobek.Runtime) *sobek.Object { 102 | paramVal := g.Argument(0) 103 | paramObj := paramVal.ToObject(rt) 104 | 105 | generator, found := ct.paramGenerators[paramObj] 106 | if !found { 107 | var param []*tracegen.TraceParams 108 | err := rt.ExportTo(paramVal, ¶m) 109 | if err != nil { 110 | common.Throw(rt, fmt.Errorf("the ParameterizedGenerator constructor expects first argument to be []TraceParams: %w", err)) 111 | } 112 | 113 | generator = tracegen.NewParameterizedGenerator(param) 114 | ct.paramGenerators[paramObj] = generator 115 | } 116 | 117 | return rt.ToValue(generator).ToObject(rt) 118 | } 119 | 120 | func (ct *TracingModule) newTemplatedGenerator(g sobek.ConstructorCall, rt *sobek.Runtime) *sobek.Object { 121 | tmplVal := g.Argument(0) 122 | tmplObj := tmplVal.ToObject(rt) 123 | 124 | generator, found := ct.templatedGenerators[tmplObj] 125 | if !found { 126 | var tmpl tracegen.TraceTemplate 127 | err := rt.ExportTo(tmplVal, &tmpl) 128 | if err != nil { 129 | common.Throw(rt, fmt.Errorf("the TemplatedGenerator constructor expects first argument to be TraceTemplate: %w", err)) 130 | } 131 | 132 | generator, err = tracegen.NewTemplatedGenerator(&tmpl) 133 | if err != nil { 134 | common.Throw(rt, fmt.Errorf("unable to generate TemplatedGenerator: %w", err)) 135 | } 136 | 137 | ct.templatedGenerators[tmplObj] = generator 138 | } 139 | 140 | return rt.ToValue(generator).ToObject(rt) 141 | } 142 | 143 | type TLSClientConfig struct { 144 | Insecure bool `js:"insecure"` 145 | InsecureSkipVerify bool `js:"insecure_skip_verify"` 146 | ServerName string `js:"server_name"` 147 | CAFile string `js:"ca_file"` 148 | CertFile string `js:"cert_file"` 149 | KeyFile string `js:"key_file"` 150 | } 151 | 152 | type ClientConfig struct { 153 | Exporter exporterType `js:"exporter"` 154 | Endpoint string `js:"endpoint"` 155 | TLS TLSClientConfig `js:"tls"` 156 | Authentication struct { 157 | User string `js:"user"` 158 | Password string `js:"password"` 159 | } 160 | Headers map[string]configopaque.String `js:"headers"` 161 | } 162 | 163 | type Client struct { 164 | exporter exporter.Traces 165 | vu modules.VU 166 | } 167 | 168 | func NewClient(cfg *ClientConfig, vu modules.VU) (*Client, error) { 169 | if cfg.Endpoint == "" { 170 | cfg.Endpoint = "0.0.0.0:4317" 171 | } 172 | 173 | var ( 174 | factory exporter.Factory 175 | exporterCfg component.Config 176 | ) 177 | 178 | tlsConfig := configtls.ClientConfig{ 179 | Insecure: cfg.TLS.Insecure, 180 | InsecureSkipVerify: cfg.TLS.InsecureSkipVerify, 181 | ServerName: cfg.TLS.ServerName, 182 | Config: configtls.Config{ 183 | CAFile: cfg.TLS.CAFile, 184 | CertFile: cfg.TLS.CertFile, 185 | KeyFile: cfg.TLS.KeyFile, 186 | }, 187 | } 188 | 189 | switch cfg.Exporter { 190 | case exporterNone, exporterOTLP: 191 | factory = otlpexporter.NewFactory() 192 | exporterCfg = factory.CreateDefaultConfig() 193 | exporterCfg.(*otlpexporter.Config).ClientConfig = configgrpc.ClientConfig{ 194 | Endpoint: cfg.Endpoint, 195 | TLSSetting: tlsConfig, 196 | Headers: util.MergeMaps(map[string]configopaque.String{ 197 | "Authorization": authorizationHeader(cfg.Authentication.User, cfg.Authentication.Password), 198 | }, cfg.Headers), 199 | } 200 | case exporterOTLPHTTP: 201 | factory = otlphttpexporter.NewFactory() 202 | exporterCfg = factory.CreateDefaultConfig() 203 | exporterCfg.(*otlphttpexporter.Config).ClientConfig = confighttp.ClientConfig{ 204 | Endpoint: cfg.Endpoint, 205 | TLSSetting: tlsConfig, 206 | Headers: util.MergeMaps(map[string]configopaque.String{ 207 | "Authorization": authorizationHeader(cfg.Authentication.User, cfg.Authentication.Password), 208 | }, cfg.Headers), 209 | } 210 | default: 211 | return nil, fmt.Errorf("failed to init exporter: unknown exporter type %s", cfg.Exporter) 212 | } 213 | 214 | exporter, err := factory.CreateTraces( 215 | context.Background(), 216 | exporter.Settings{ 217 | TelemetrySettings: component.TelemetrySettings{ 218 | Logger: zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zapcore.EncoderConfig{}), zapcore.AddSync(os.Stdout), zap.InfoLevel)), 219 | TracerProvider: tracenoop.NewTracerProvider(), 220 | MeterProvider: metricnoop.NewMeterProvider(), 221 | }, 222 | BuildInfo: component.NewDefaultBuildInfo(), 223 | }, 224 | exporterCfg, 225 | ) 226 | if err != nil { 227 | return nil, fmt.Errorf("failed create exporter: %w", err) 228 | } 229 | 230 | err = exporter.Start(vu.Context(), componenttest.NewNopHost()) 231 | if err != nil { 232 | return nil, fmt.Errorf("failed to start exporter: %w", err) 233 | } 234 | 235 | return &Client{ 236 | exporter: exporter, 237 | vu: vu, 238 | }, nil 239 | } 240 | 241 | func (c *Client) Push(traces ptrace.Traces) error { 242 | return c.exporter.ConsumeTraces(c.vu.Context(), traces) 243 | } 244 | 245 | func (c *Client) Shutdown() error { 246 | return c.exporter.Shutdown(c.vu.Context()) 247 | } 248 | 249 | func authorizationHeader(user, password string) configopaque.String { 250 | return configopaque.String("Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))) 251 | } 252 | --------------------------------------------------------------------------------