├── .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 |
--------------------------------------------------------------------------------