├── .github └── workflows │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── Taskfile.yml ├── assets └── xk6-opentelemetry-logo.png ├── attributes.go ├── collector_logs.go ├── collector_metrics.go ├── collector_trace.go ├── examples ├── test-logs.js ├── test-metrics-histogram.js ├── test-metrics.js └── test-traces.js ├── generator.go ├── go.mod ├── go.sum ├── logs.go ├── metric_gauge.go ├── metric_histogram.go ├── metric_sum.go ├── metrics.go ├── module.go ├── resource.go ├── testing ├── docker-compose.yml └── otel-collector.yaml ├── trace.go └── trace_id_gen.go /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | permissions: 10 | contents: read 11 | pull-requests: read 12 | jobs: 13 | golangci: 14 | name: lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/setup-go@v3 18 | with: 19 | go-version: '1.22' 20 | - uses: actions/checkout@v3 21 | - name: golangci-lint 22 | uses: golangci/golangci-lint-action@v3 23 | with: 24 | version: v1.56.2 25 | only-new-issues: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | k6 -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: true 3 | disable: 4 | - exhaustruct 5 | - nosnakecase 6 | - maligned # deprecated 7 | - golint # deprecated 8 | - ifshort # deprecated 9 | - structcheck # deprecated 10 | - varcheck # deprecated 11 | - exhaustivestruct # deprecated 12 | - interfacer # deprecated 13 | - scopelint # deprecated 14 | - deadcode # deprecated 15 | - rowserrcheck # generics 16 | - structcheck # generics 17 | - wastedassign # generics 18 | - gofumpt # generics 19 | 20 | issues: 21 | exclude-rules: 22 | - path: module\.go 23 | linters: 24 | - gochecknoinits 25 | - ireturn 26 | - text: "G404:" 27 | linters: 28 | - gosec 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 AS build 2 | WORKDIR /go/src/github.com/thmshmm/xk6-opentelemetry 3 | COPY . . 4 | RUN go install go.k6.io/xk6/cmd/xk6@latest 5 | RUN xk6 build --with xk6-opentelemetry=. --with github.com/mostafa/xk6-kafka 6 | 7 | FROM alpine:latest as base 8 | RUN apk add --no-cache ca-certificates && \ 9 | addgroup -S app && adduser -S -G app app 10 | 11 | FROM scratch 12 | COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 13 | COPY --from=base /etc/passwd /etc/passwd 14 | COPY --from=build /go/src/github.com/thmshmm/xk6-opentelemetry/k6 /k6 15 | USER app 16 | ENTRYPOINT ["/k6"] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Thomas Hamm 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xk6-opentelemetry logo xk6-opentelemetry 2 | 3 | The xk6-opentelemetry project is a [k6 extension](https://k6.io/docs/extensions/guides/what-are-k6-extensions/) that enables k6 users to generate random [OpenTelemetry signals](https://opentelemetry.io/docs/reference/specification/glossary/#signals) (metrics, logs, traces) for testing purposes. 4 | 5 | Check the [examples](./examples/) directory which contains some scripts to get started. 6 | 7 | ## Features 8 | 9 | - Generate 10 | - ExportLogsServiceRequest 11 | - ExportMetricsServiceRequest (Types: gauge, sum, histogram) 12 | - ExportTraceServiceRequest 13 | - Set resource attributes 14 | 15 | ## Usage 16 | 17 | ### Docker 18 | 19 | The easiest way to get started is to build the provided Docker image and run k6 scripts inside the container. 20 | 21 | Build the image: 22 | ``` 23 | docker build -t xk6-opentelemetry . 24 | ``` 25 | 26 | Run a local k6 script: 27 | ``` 28 | docker run --rm -i xk6-opentelemetry run - .js 72 | ``` 73 | 74 | Run all examples: 75 | ``` 76 | task run-examples 77 | ``` 78 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | build-k6: 5 | cmds: 6 | - xk6 build --with xk6-opentelemetry=. --with github.com/mostafa/xk6-kafka 7 | 8 | build-docker: 9 | cmds: 10 | - docker build -t xk6-opentelemetry . 11 | 12 | lint: 13 | cmds: 14 | - golangci-lint run 15 | 16 | testing-up: 17 | dir: ./testing 18 | cmds: 19 | - docker compose -p xk6-opentelemetry up -d 20 | 21 | testing-down: 22 | dir: ./testing 23 | cmds: 24 | - docker compose -p xk6-opentelemetry down 25 | 26 | run-examples: 27 | cmds: 28 | - ./k6 run examples/test-logs.js 29 | - ./k6 run examples/test-metrics.js 30 | - ./k6 run examples/test-metrics-histogram.js 31 | - ./k6 run examples/test-traces.js 32 | -------------------------------------------------------------------------------- /assets/xk6-opentelemetry-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thmshmm/xk6-opentelemetry/5694b6443907d093da38094977a47dd4eb9bd25f/assets/xk6-opentelemetry-logo.png -------------------------------------------------------------------------------- /attributes.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 5 | ) 6 | 7 | const unsupportedTypeValue = "unsupported_type" 8 | 9 | func ToAttributes(attrs map[string]interface{}) []*commonpb.KeyValue { 10 | attributes := make([]*commonpb.KeyValue, 0) 11 | 12 | for k, v := range attrs { 13 | attributes = append(attributes, &commonpb.KeyValue{ 14 | Key: k, 15 | Value: ToAnyValue(v), 16 | }) 17 | } 18 | 19 | return attributes 20 | } 21 | 22 | func ToAnyValue(attr interface{}) *commonpb.AnyValue { 23 | var value *commonpb.AnyValue 24 | 25 | switch attrValue := attr.(type) { 26 | case string: 27 | value = &commonpb.AnyValue{ 28 | Value: &commonpb.AnyValue_StringValue{ 29 | StringValue: attrValue, 30 | }, 31 | } 32 | case int64: 33 | value = &commonpb.AnyValue{ 34 | Value: &commonpb.AnyValue_IntValue{ 35 | IntValue: attrValue, 36 | }, 37 | } 38 | case int: 39 | value = &commonpb.AnyValue{ 40 | Value: &commonpb.AnyValue_IntValue{ 41 | IntValue: int64(attrValue), 42 | }, 43 | } 44 | case float64: 45 | value = &commonpb.AnyValue{ 46 | Value: &commonpb.AnyValue_DoubleValue{ 47 | DoubleValue: attrValue, 48 | }, 49 | } 50 | case bool: 51 | value = &commonpb.AnyValue{ 52 | Value: &commonpb.AnyValue_BoolValue{ 53 | BoolValue: attrValue, 54 | }, 55 | } 56 | default: 57 | value = &commonpb.AnyValue{ 58 | Value: &commonpb.AnyValue_StringValue{ 59 | StringValue: unsupportedTypeValue, 60 | }, 61 | } 62 | } 63 | 64 | return value 65 | } 66 | -------------------------------------------------------------------------------- /collector_logs.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | collogspb "go.opentelemetry.io/proto/otlp/collector/logs/v1" 5 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 6 | logspb "go.opentelemetry.io/proto/otlp/logs/v1" 7 | ) 8 | 9 | func ExportLogsServiceRequest( 10 | resourceAttrs []*commonpb.KeyValue, 11 | config LogConfig, 12 | ) *collogspb.ExportLogsServiceRequest { 13 | return &collogspb.ExportLogsServiceRequest{ 14 | ResourceLogs: []*logspb.ResourceLogs{ 15 | ResourceLogs(resourceAttrs, config), 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /collector_metrics.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | colmetricspb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" 5 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 6 | metricspb "go.opentelemetry.io/proto/otlp/metrics/v1" 7 | ) 8 | 9 | func ExportMetricsServiceRequest( 10 | resourceAttrs []*commonpb.KeyValue, 11 | config MetricConfig, 12 | ) *colmetricspb.ExportMetricsServiceRequest { 13 | return &colmetricspb.ExportMetricsServiceRequest{ 14 | ResourceMetrics: []*metricspb.ResourceMetrics{ 15 | ResourceMetrics(resourceAttrs, config), 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /collector_trace.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | coltracepb "go.opentelemetry.io/proto/otlp/collector/trace/v1" 5 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 6 | tracepb "go.opentelemetry.io/proto/otlp/trace/v1" 7 | ) 8 | 9 | func ExportTraceServiceRequest( 10 | resourceAttrs []*commonpb.KeyValue, 11 | config TraceConfig, 12 | ) *coltracepb.ExportTraceServiceRequest { 13 | return &coltracepb.ExportTraceServiceRequest{ 14 | ResourceSpans: []*tracepb.ResourceSpans{ 15 | ResourceSpans(resourceAttrs, config), 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/test-logs.js: -------------------------------------------------------------------------------- 1 | import generator from 'k6/x/opentelemetry'; 2 | import { 3 | Writer, 4 | SchemaRegistry, 5 | SCHEMA_TYPE_BYTES, 6 | } from 'k6/x/kafka'; 7 | import { 8 | randomItem, 9 | randomIntBetween, 10 | } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; 11 | 12 | const brokers = ["localhost:9092"] 13 | const topic = "otel_logs" 14 | const schemaRegistry = new SchemaRegistry(); 15 | const producer = new Writer({ 16 | brokers: brokers, 17 | topic: topic, 18 | }); 19 | 20 | const logBodies = [ 21 | "this is a log msg", 22 | "this is another log msg" 23 | ] 24 | 25 | export default function () { 26 | var messages = [] 27 | 28 | const resourceAttributes = new Map() 29 | resourceAttributes.set("pod", "test-abc") 30 | resourceAttributes.set("instance", 1) 31 | resourceAttributes.set("something", 1.23) 32 | resourceAttributes.set("isProd", true) 33 | 34 | // optionally set static attributes for the resource 35 | generator.setStaticResourceAttributes(resourceAttributes) 36 | 37 | for (let idx = 0; idx < 10; idx++) { 38 | messages[idx] = { 39 | value: schemaRegistry.serialize({ 40 | data: Array.from(generator.exportLogsServiceRequest({ 41 | // "attributes": logAttributes, // optionally create log specific attribute map 42 | "data": { 43 | "body": randomItem(logBodies), 44 | "severity": randomIntBetween(0, 24) // severity numbers from 0 to 24 45 | }, 46 | })), 47 | schemaType: SCHEMA_TYPE_BYTES, 48 | }) 49 | } 50 | } 51 | 52 | producer.produce({ messages: messages }) 53 | } 54 | -------------------------------------------------------------------------------- /examples/test-metrics-histogram.js: -------------------------------------------------------------------------------- 1 | import generator from 'k6/x/opentelemetry'; 2 | import { 3 | Writer, 4 | SchemaRegistry, 5 | SCHEMA_TYPE_BYTES, 6 | } from 'k6/x/kafka'; 7 | import { 8 | randomIntBetween, 9 | } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; 10 | 11 | const brokers = ["localhost:9092"] 12 | const topic = "otel_metrics" 13 | const schemaRegistry = new SchemaRegistry(); 14 | const producer = new Writer({ 15 | brokers: brokers, 16 | topic: topic, 17 | }); 18 | 19 | // Example of having one cumulative histogram with 10 values after one execution. 20 | export default function () { 21 | var messages = [] 22 | var count = 0 23 | var sum = 0.0 24 | var min = 100.0 25 | var max = 0.0 26 | var bucketCounts = [0, 0, 0] // (-inf,10], (10,50], (50,+inf] 27 | 28 | for (let idx = 0; idx < 10; idx++) { 29 | var recordedValue = randomIntBetween(0, 100) 30 | 31 | var bucketIdx = getBucketIdx(recordedValue) 32 | bucketCounts[bucketIdx] = bucketCounts[bucketIdx] + 1 33 | 34 | sum += recordedValue 35 | min = Math.min(min, recordedValue) 36 | max = Math.max(max, recordedValue) 37 | 38 | messages[idx] = { 39 | value: schemaRegistry.serialize({ 40 | data: Array.from(generator.exportMetricsServiceRequest({ 41 | "type": "histogram", 42 | "name": "hist_metric1", 43 | "unit": "ms", 44 | "data": { 45 | "aggregationTemporality": "cumulative", 46 | "count": ++count, 47 | "sum": sum, 48 | "min": min, 49 | "max": max, 50 | "explicitBounds": [10, 50], 51 | "bucketCounts": bucketCounts, 52 | }, 53 | })), 54 | schemaType: SCHEMA_TYPE_BYTES, 55 | }) 56 | } 57 | } 58 | 59 | producer.produce({ messages: messages }) 60 | } 61 | 62 | function getBucketIdx(value) { 63 | if (value <= 10) { 64 | return 0 65 | } else if (value > 10 && value <= 50) { 66 | return 1 67 | } 68 | 69 | return 2 // value > 50 70 | } 71 | -------------------------------------------------------------------------------- /examples/test-metrics.js: -------------------------------------------------------------------------------- 1 | import generator from 'k6/x/opentelemetry'; 2 | import { 3 | Writer, 4 | SchemaRegistry, 5 | SCHEMA_TYPE_BYTES, 6 | } from 'k6/x/kafka'; 7 | import { 8 | randomItem, 9 | randomIntBetween, 10 | } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; 11 | 12 | const brokers = ["localhost:9092"] 13 | const topic = "otel_metrics" 14 | const schemaRegistry = new SchemaRegistry(); 15 | const producer = new Writer({ 16 | brokers: brokers, 17 | topic: topic, 18 | }); 19 | 20 | const metricNames = [ 21 | "metric1", 22 | "metric2" 23 | ] 24 | 25 | const metricAttributes = new Map() 26 | metricAttributes.set("operation", "create") 27 | 28 | export default function () { 29 | var messages = [] 30 | 31 | const resourceAttributes = new Map() 32 | resourceAttributes.set("pod", "test-abc") 33 | 34 | // optionally set static attributes for the resource 35 | generator.setStaticResourceAttributes(resourceAttributes) 36 | 37 | for (let idx = 0; idx < 5; idx++) { 38 | messages[idx] = { 39 | value: schemaRegistry.serialize({ 40 | data: Array.from(generator.exportMetricsServiceRequest({ 41 | "type": "gauge", 42 | "name": "gauge_" + randomItem(metricNames), 43 | "attributes": metricAttributes, 44 | "data": { 45 | "value": randomIntBetween(10, 100) 46 | }, 47 | })), 48 | schemaType: SCHEMA_TYPE_BYTES, 49 | }) 50 | } 51 | } 52 | 53 | for (let idx = 0; idx < 5; idx++) { 54 | messages[idx] = { 55 | value: schemaRegistry.serialize({ 56 | data: Array.from(generator.exportMetricsServiceRequest({ 57 | "type": "sum", 58 | "name": "sum_" + randomItem(metricNames), 59 | "attributes": metricAttributes, 60 | "data": { 61 | "value": randomIntBetween(10, 100), 62 | "isMonotonic": false, // true or false 63 | "aggregationTemporality": "delta" // delta or cumulative 64 | }, 65 | })), 66 | schemaType: SCHEMA_TYPE_BYTES, 67 | }) 68 | } 69 | } 70 | 71 | producer.produce({ messages: messages }) 72 | } 73 | -------------------------------------------------------------------------------- /examples/test-traces.js: -------------------------------------------------------------------------------- 1 | import generator from 'k6/x/opentelemetry'; 2 | import { 3 | Writer, 4 | SchemaRegistry, 5 | SCHEMA_TYPE_BYTES, 6 | } from 'k6/x/kafka'; 7 | 8 | const brokers = ["localhost:9092"] 9 | const topic = "otel_traces" 10 | const schemaRegistry = new SchemaRegistry(); 11 | const producer = new Writer({ 12 | brokers: brokers, 13 | topic: topic, 14 | }); 15 | 16 | export default function () { 17 | var messages = [] 18 | 19 | const resourceAttributes = new Map() 20 | resourceAttributes.set("pod", "test-abc") 21 | 22 | // make sure to set a service name for correct visualization in Grafana Tempo 23 | resourceAttributes.set("service.name", "service-1") 24 | 25 | // optionally set static attributes for the resource 26 | generator.setStaticResourceAttributes(resourceAttributes) 27 | 28 | for (let idx = 0; idx < 10; idx++) { 29 | var timeNow = generator.timeNowUnixNano() 30 | 31 | var traceId = generator.newTraceID() 32 | 33 | var span1Id = generator.newSpanID() 34 | var span1 = { 35 | "traceId": traceId, 36 | "spanId": span1Id, 37 | // root span has no parendSpanId 38 | "name": "say-hello", 39 | // "attributes": spanAttributes, // optionally create span specific attribute map 40 | "startTimeUnixNano": timeNow - 1e8, // 100ms earlier 41 | "endTimeUnixNano": timeNow, 42 | } 43 | 44 | var span2Id = generator.newSpanID() 45 | var span2 = { 46 | "traceId": traceId, 47 | "spanId": span2Id, 48 | "parentSpanId": span1Id, 49 | "name": "random-name", 50 | "kind": "client", 51 | "startTimeUnixNano": timeNow - 9e7, 52 | "endTimeUnixNano": timeNow - 4e7, 53 | } 54 | 55 | var span3Id = generator.newSpanID() 56 | var span3 = { 57 | "traceId": traceId, 58 | "spanId": span3Id, 59 | "parentSpanId": span1Id, 60 | "name": "enrich", 61 | "startTimeUnixNano": timeNow - 3e7, 62 | "endTimeUnixNano": timeNow - 1e7, 63 | } 64 | 65 | messages[idx] = { 66 | value: schemaRegistry.serialize({ 67 | data: Array.from(generator.exportTraceServiceRequest({ 68 | "data": [ 69 | span1, 70 | span2, 71 | span3, 72 | ], 73 | })), 74 | schemaType: SCHEMA_TYPE_BYTES, 75 | }) 76 | } 77 | } 78 | 79 | producer.produce({ messages: messages }) 80 | } 81 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "log/slog" 5 | "math/rand" 6 | "time" 7 | 8 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 9 | "google.golang.org/protobuf/proto" 10 | ) 11 | 12 | type Generator struct { 13 | *IDGenerator 14 | staticResourceAttributes []*commonpb.KeyValue 15 | } 16 | 17 | // NewGenerator creates a Generator instance which can create supported OpenTelemetry signals. 18 | func NewGenerator() *Generator { 19 | return &Generator{ 20 | IDGenerator: &IDGenerator{ 21 | randSource: rand.New(rand.NewSource(time.Now().UnixNano())), 22 | }, 23 | } 24 | } 25 | 26 | func (g *Generator) SetStaticResourceAttributes(attrs map[string]interface{}) { 27 | g.staticResourceAttributes = ToAttributes(attrs) 28 | } 29 | 30 | func (g *Generator) ExportLogsServiceRequest(config LogConfig) []byte { 31 | request := ExportLogsServiceRequest(g.staticResourceAttributes, config) 32 | 33 | data, err := proto.Marshal(request) 34 | if err != nil { 35 | slog.Error("Failed to marshal ExportLogsServiceRequest", "error", err) 36 | 37 | return nil 38 | } 39 | 40 | return data 41 | } 42 | 43 | func (g *Generator) ExportMetricsServiceRequest(config MetricConfig) []byte { 44 | request := ExportMetricsServiceRequest(g.staticResourceAttributes, config) 45 | 46 | data, err := proto.Marshal(request) 47 | if err != nil { 48 | slog.Error("Failed to marshal ExportMetricsServiceRequest", "error", err) 49 | 50 | return nil 51 | } 52 | 53 | return data 54 | } 55 | 56 | func (g *Generator) ExportTraceServiceRequest(config TraceConfig) []byte { 57 | request := ExportTraceServiceRequest(g.staticResourceAttributes, config) 58 | 59 | data, err := proto.Marshal(request) 60 | if err != nil { 61 | slog.Error("Failed to marshal ExportTraceServiceRequest", "error", err) 62 | 63 | return nil 64 | } 65 | 66 | return data 67 | } 68 | 69 | func (g *Generator) TimeNowUnixNano() int64 { 70 | return time.Now().UnixNano() 71 | } 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thmshmm/xk6-opentelemetry 2 | 3 | go 1.20 4 | 5 | require ( 6 | go.k6.io/k6 v0.51.1-0.20240610082146-1f01a9bc2365 7 | go.opentelemetry.io/proto/otlp v1.1.0 8 | google.golang.org/protobuf v1.33.0 9 | ) 10 | 11 | require ( 12 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 13 | github.com/dlclark/regexp2 v1.9.0 // indirect 14 | github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2 // indirect 15 | github.com/evanw/esbuild v0.21.2 // indirect 16 | github.com/fatih/color v1.16.0 // indirect 17 | github.com/go-logr/logr v1.4.1 // indirect 18 | github.com/go-logr/stdr v1.2.2 // indirect 19 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect 20 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect 21 | github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 // indirect 22 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect 23 | github.com/josharian/intern v1.0.0 // indirect 24 | github.com/mailru/easyjson v0.7.7 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd // indirect 28 | github.com/onsi/ginkgo v1.16.5 // indirect 29 | github.com/onsi/gomega v1.27.2 // indirect 30 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect 31 | github.com/sirupsen/logrus v1.9.3 // indirect 32 | github.com/spf13/afero v1.1.2 // indirect 33 | go.opentelemetry.io/otel v1.24.0 // indirect 34 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect 35 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect 36 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect 37 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 38 | go.opentelemetry.io/otel/sdk v1.24.0 // indirect 39 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 40 | golang.org/x/net v0.26.0 // indirect 41 | golang.org/x/sys v0.21.0 // indirect 42 | golang.org/x/text v0.16.0 // indirect 43 | golang.org/x/time v0.5.0 // indirect 44 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect 45 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect 46 | google.golang.org/grpc v1.63.2 // indirect 47 | gopkg.in/guregu/null.v3 v3.3.0 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 2 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 3 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI= 8 | github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 9 | github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2 h1:OFTHt+yJDo/uaIKMGjEKzc3DGhrpQZoqvMUIloZv6ZY= 10 | github.com/dop251/goja v0.0.0-20240516125602-ccbae20bcec2/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw= 11 | github.com/evanw/esbuild v0.21.2 h1:CLplcGi794CfHLVmUbvVfTMKkykm+nyIHU8SU60KUTA= 12 | github.com/evanw/esbuild v0.21.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 13 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 14 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 15 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 16 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 17 | github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= 18 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 19 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 20 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 21 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 22 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 23 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= 24 | github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= 25 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 28 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 29 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 30 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 31 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 32 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 33 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 34 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 35 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 36 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 37 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 h1:ZgoomqkdjGbQ3+qQXCkvYMCDvGDNg2k5JJDjjdTB6jY= 38 | github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= 39 | github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 40 | github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78 h1:rVCZdB+13G+aQoGm3CBVaDGl0uxZxfjvQgEJy4IeHTA= 41 | github.com/grafana/sobek v0.0.0-20240607083612-4f0cd64f4e78/go.mod h1:6ZH0b0iOxyigeTh+/IlGoL0Hd3lVXA94xoXf0ldNgCM= 42 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= 43 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= 44 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 45 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 46 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 47 | github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= 48 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 49 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 50 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 51 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 52 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 53 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 54 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 55 | github.com/mccutchen/go-httpbin v1.1.2-0.20190116014521-c5cb2f4802fa h1:lx8ZnNPwjkXSzOROz0cg69RlErRXs+L3eDkggASWKLo= 56 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd h1:AC3N94irbx2kWGA8f/2Ks7EQl2LxKIRQYuT9IJDwgiI= 57 | github.com/mstoykov/atlas v0.0.0-20220811071828-388f114305dd/go.mod h1:9vRHVuLCjoFfE3GT06X0spdOAO+Zzo4AMjdIwUHBvAk= 58 | github.com/mstoykov/envconfig v1.5.0 h1:E2FgWf73BQt0ddgn7aoITkQHmgwAcHup1s//MsS5/f8= 59 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 60 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 61 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 62 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 63 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 64 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 65 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 66 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 67 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 68 | github.com/onsi/gomega v1.27.2 h1:SKU0CXeKE/WVgIV1T61kSa3+IRE8Ekrv9rdXDwwTqnY= 69 | github.com/onsi/gomega v1.27.2/go.mod h1:5mR3phAHpkAVIDkHEUBY6HGVsU+cpcEscrGPB4oPlZI= 70 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 71 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 72 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e h1:zWKUYT07mGmVBH+9UgnHXd/ekCK99C8EbDSAt5qsjXE= 73 | github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= 74 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 75 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 76 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 77 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 78 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 79 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 80 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 82 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 83 | go.k6.io/k6 v0.51.1-0.20240610082146-1f01a9bc2365 h1:ZXlJs5hXt1hbY4k3jHVJS8xrgypgTZAwbMBVH1EMCgY= 84 | go.k6.io/k6 v0.51.1-0.20240610082146-1f01a9bc2365/go.mod h1:LJKmFwUODAYoxitsJ3Xk+wsyVJDpyQiLyJAVn+oGyVQ= 85 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 86 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 87 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= 88 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= 89 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= 90 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= 91 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= 92 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= 93 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 94 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 95 | go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= 96 | go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= 97 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 98 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 99 | go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= 100 | go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= 101 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 102 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 103 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 104 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 105 | golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= 106 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 107 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 108 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 109 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 110 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 111 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 112 | golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= 113 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 114 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 120 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 130 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 131 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 132 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 133 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 134 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 135 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 136 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 137 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 138 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 139 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 140 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 141 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 143 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 144 | google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= 145 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= 146 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= 147 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= 148 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= 149 | google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= 150 | google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= 151 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 152 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 153 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 154 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 155 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 156 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 157 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 158 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 159 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 160 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 161 | gopkg.in/guregu/null.v3 v3.3.0 h1:8j3ggqq+NgKt/O7mbFVUFKUMWN+l1AmT5jQmJ6nPh2c= 162 | gopkg.in/guregu/null.v3 v3.3.0/go.mod h1:E4tX2Qe3h7QdL+uZ3a0vqvYwKQsRSQKM5V4YltdgH9Y= 163 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 164 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 165 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 166 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 167 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 168 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 169 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 170 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 171 | -------------------------------------------------------------------------------- /logs.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "time" 5 | 6 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 7 | logspb "go.opentelemetry.io/proto/otlp/logs/v1" 8 | ) 9 | 10 | type LogConfig struct { 11 | Attributes map[string]interface{} `js:"attributes"` 12 | Data LogData `js:"data"` 13 | } 14 | 15 | type LogData struct { 16 | Body string `js:"body"` 17 | Severity int32 `js:"severity"` 18 | } 19 | 20 | func ResourceLogs(resourceAttrs []*commonpb.KeyValue, config LogConfig) *logspb.ResourceLogs { 21 | return &logspb.ResourceLogs{ 22 | Resource: resource(resourceAttrs), 23 | ScopeLogs: []*logspb.ScopeLogs{ 24 | { 25 | LogRecords: []*logspb.LogRecord{ 26 | logRecord(ToAttributes(config.Attributes), config.Data), 27 | }, 28 | }, 29 | }, 30 | } 31 | } 32 | 33 | func logRecord(attrs []*commonpb.KeyValue, data LogData) *logspb.LogRecord { 34 | return &logspb.LogRecord{ 35 | Attributes: attrs, 36 | TimeUnixNano: uint64(time.Now().UnixNano()), 37 | SeverityNumber: logspb.SeverityNumber(data.Severity), 38 | Body: &commonpb.AnyValue{ 39 | Value: &commonpb.AnyValue_StringValue{ 40 | StringValue: data.Body, 41 | }, 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /metric_gauge.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 9 | metricspb "go.opentelemetry.io/proto/otlp/metrics/v1" 10 | ) 11 | 12 | type GaugeData struct { 13 | Value int64 `json:"value"` 14 | } 15 | 16 | func parseGaugeData(rawData map[string]interface{}) (*GaugeData, error) { 17 | dataBytes, err := json.Marshal(rawData) 18 | if err != nil { 19 | return nil, fmt.Errorf("failed to process provided gauge data: %w", err) 20 | } 21 | 22 | var data GaugeData 23 | err = json.Unmarshal(dataBytes, &data) 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to parse JSON gauge data: %w", err) 26 | } 27 | 28 | return &data, nil 29 | } 30 | 31 | func gauge(attrs []*commonpb.KeyValue, data *GaugeData) *metricspb.Metric_Gauge { 32 | return &metricspb.Metric_Gauge{ 33 | Gauge: &metricspb.Gauge{ 34 | DataPoints: []*metricspb.NumberDataPoint{ 35 | { 36 | Attributes: attrs, 37 | TimeUnixNano: uint64(time.Now().UnixNano()), 38 | Value: &metricspb.NumberDataPoint_AsInt{ 39 | AsInt: data.Value, 40 | }, 41 | }, 42 | }, 43 | }, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /metric_histogram.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 9 | metricspb "go.opentelemetry.io/proto/otlp/metrics/v1" 10 | ) 11 | 12 | type HistogramData struct { 13 | AggregationTemporality string `json:"aggregationTemporality"` 14 | Count uint64 `json:"count"` 15 | Sum float64 `json:"sum"` 16 | Min float64 `json:"min"` 17 | Max float64 `json:"max"` 18 | ExplicitBounds []float64 `json:"explicitBounds"` 19 | BucketCounts []uint64 `json:"bucketCounts"` 20 | } 21 | 22 | func parseHistogramData(rawData map[string]interface{}) (*HistogramData, error) { 23 | dataBytes, err := json.Marshal(rawData) 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to process provided histogram data: %w", err) 26 | } 27 | 28 | var data HistogramData 29 | err = json.Unmarshal(dataBytes, &data) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to parse JSON histogram data: %w", err) 32 | } 33 | 34 | return &data, nil 35 | } 36 | 37 | func histogram(attrs []*commonpb.KeyValue, data *HistogramData) *metricspb.Metric_Histogram { 38 | return &metricspb.Metric_Histogram{ 39 | Histogram: &metricspb.Histogram{ 40 | AggregationTemporality: getAggregationTemporality(data.AggregationTemporality), 41 | DataPoints: []*metricspb.HistogramDataPoint{ 42 | { 43 | Attributes: attrs, 44 | TimeUnixNano: uint64(time.Now().UnixNano()), 45 | Count: data.Count, 46 | Sum: &data.Sum, 47 | Min: &data.Min, 48 | Max: &data.Max, 49 | ExplicitBounds: data.ExplicitBounds, 50 | BucketCounts: data.BucketCounts, 51 | }, 52 | }, 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /metric_sum.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 9 | metricspb "go.opentelemetry.io/proto/otlp/metrics/v1" 10 | ) 11 | 12 | type SumData struct { 13 | Value int64 `json:"value"` 14 | IsMonotonic bool `json:"isMonotonic"` 15 | AggregationTemporality string `json:"aggregationTemporality"` 16 | } 17 | 18 | func parseSumData(rawData map[string]interface{}) (*SumData, error) { 19 | dataBytes, err := json.Marshal(rawData) 20 | if err != nil { 21 | return nil, fmt.Errorf("failed to process provided sum data: %w", err) 22 | } 23 | 24 | var data SumData 25 | err = json.Unmarshal(dataBytes, &data) 26 | if err != nil { 27 | return nil, fmt.Errorf("failed to parse JSON sum data: %w", err) 28 | } 29 | 30 | return &data, nil 31 | } 32 | 33 | func sum(attrs []*commonpb.KeyValue, data *SumData) *metricspb.Metric_Sum { 34 | return &metricspb.Metric_Sum{ 35 | Sum: &metricspb.Sum{ 36 | IsMonotonic: data.IsMonotonic, 37 | AggregationTemporality: getAggregationTemporality(data.AggregationTemporality), 38 | DataPoints: []*metricspb.NumberDataPoint{ 39 | { 40 | Attributes: attrs, 41 | TimeUnixNano: uint64(time.Now().UnixNano()), 42 | Value: &metricspb.NumberDataPoint_AsInt{ 43 | AsInt: data.Value, 44 | }, 45 | }, 46 | }, 47 | }, 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /metrics.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "log/slog" 5 | "strings" 6 | 7 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 8 | metricspb "go.opentelemetry.io/proto/otlp/metrics/v1" 9 | ) 10 | 11 | type MetricConfig struct { 12 | Type string `js:"type"` 13 | Name string `js:"name"` 14 | Unit string `js:"unit"` 15 | Attributes map[string]interface{} `js:"attributes"` 16 | Data map[string]interface{} `js:"data"` 17 | } 18 | 19 | func ResourceMetrics(resourceAttrs []*commonpb.KeyValue, config MetricConfig) *metricspb.ResourceMetrics { 20 | metric := &metricspb.Metric{ 21 | Name: config.Name, 22 | Unit: config.Unit, 23 | } 24 | 25 | attrs := ToAttributes(config.Attributes) 26 | 27 | switch config.Type { 28 | case "gauge": 29 | data, err := parseGaugeData(config.Data) 30 | if err != nil { 31 | slog.Error("Failed to parse gauge data", "error", err) 32 | 33 | return nil 34 | } 35 | 36 | metric.Data = gauge(attrs, data) 37 | case "sum": 38 | data, err := parseSumData(config.Data) 39 | if err != nil { 40 | slog.Error("Failed to parse sum data", "error", err) 41 | 42 | return nil 43 | } 44 | 45 | metric.Data = sum(attrs, data) 46 | case "histogram": 47 | data, err := parseHistogramData(config.Data) 48 | if err != nil { 49 | slog.Error("Failed to parse histogram data", "error", err) 50 | 51 | return nil 52 | } 53 | 54 | metric.Data = histogram(attrs, data) 55 | default: 56 | slog.Error("Unimplemented metric type %q, use one of [gauge, sum]", "type", config.Type) 57 | 58 | return nil 59 | } 60 | 61 | return &metricspb.ResourceMetrics{ 62 | Resource: resource(resourceAttrs), 63 | ScopeMetrics: []*metricspb.ScopeMetrics{ 64 | { 65 | Metrics: []*metricspb.Metric{ 66 | metric, 67 | }, 68 | }, 69 | }, 70 | } 71 | } 72 | 73 | func getAggregationTemporality(temporality string) metricspb.AggregationTemporality { 74 | switch strings.ToLower(temporality) { 75 | case "delta": 76 | return metricspb.AggregationTemporality_AGGREGATION_TEMPORALITY_DELTA 77 | case "cumulative": 78 | return metricspb.AggregationTemporality_AGGREGATION_TEMPORALITY_CUMULATIVE 79 | } 80 | 81 | return metricspb.AggregationTemporality_AGGREGATION_TEMPORALITY_UNSPECIFIED 82 | } 83 | -------------------------------------------------------------------------------- /module.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "go.k6.io/k6/js/modules" 5 | ) 6 | 7 | func init() { 8 | modules.Register("k6/x/opentelemetry", New()) 9 | } 10 | 11 | type ( 12 | RootModule struct{} 13 | ModuleInstance struct { 14 | generator *Generator 15 | } 16 | ) 17 | 18 | var ( 19 | _ modules.Instance = &ModuleInstance{} 20 | _ modules.Module = &RootModule{} 21 | ) 22 | 23 | // New returns a pointer to a new RootModule instance. 24 | func New() *RootModule { 25 | return &RootModule{} 26 | } 27 | 28 | // NewModuleInstance implements the modules.Module interface returning a new instance for each VU. 29 | func (*RootModule) NewModuleInstance(_ modules.VU) modules.Instance { 30 | return &ModuleInstance{ 31 | generator: NewGenerator(), 32 | } 33 | } 34 | 35 | // Exports implements the modules.Instance interface and returns the exported types for the JS module. 36 | func (mi *ModuleInstance) Exports() modules.Exports { 37 | return modules.Exports{ 38 | Default: mi.generator, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 5 | resourcepb "go.opentelemetry.io/proto/otlp/resource/v1" 6 | ) 7 | 8 | func resource(attrs []*commonpb.KeyValue) *resourcepb.Resource { 9 | return &resourcepb.Resource{ 10 | Attributes: attrs, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /testing/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | redpanda: 5 | image: vectorized/redpanda:v23.3.7 6 | command: redpanda start --overprovisioned --smp 1 --memory 1G --reserve-memory 0M --node-id 0 --check=false --kafka-addr INSIDE://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 --advertise-kafka-addr INSIDE://broker:29092,OUTSIDE://localhost:9092 7 | container_name: broker 8 | ports: 9 | - "9092:9092" 10 | collector: 11 | image: otel/opentelemetry-collector-contrib:0.96.0 12 | command: 13 | - --config 14 | - /etc/otel/config.yaml 15 | volumes: 16 | - ./otel-collector.yaml/:/etc/otel/config.yaml 17 | restart: always 18 | -------------------------------------------------------------------------------- /testing/otel-collector.yaml: -------------------------------------------------------------------------------- 1 | receivers: 2 | kafka/logs: 3 | brokers: 4 | - broker:29092 5 | group_id: collector-logs 6 | topic: otel_logs 7 | kafka/metrics: 8 | brokers: 9 | - broker:29092 10 | group_id: collector-metrics 11 | topic: otel_metrics 12 | kafka/traces: 13 | brokers: 14 | - broker:29092 15 | group_id: collector-traces 16 | topic: otel_traces 17 | 18 | exporters: 19 | logging: 20 | verbosity: detailed 21 | 22 | processors: 23 | batch: 24 | 25 | service: 26 | pipelines: 27 | logs: 28 | receivers: 29 | - kafka/logs 30 | processors: 31 | - batch 32 | exporters: 33 | - logging 34 | metrics: 35 | receivers: 36 | - kafka/metrics 37 | processors: 38 | - batch 39 | exporters: 40 | - logging 41 | traces: 42 | receivers: 43 | - kafka/traces 44 | processors: 45 | - batch 46 | exporters: 47 | - logging 48 | 49 | telemetry: 50 | logs: 51 | level: "info" 52 | -------------------------------------------------------------------------------- /trace.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "encoding/hex" 5 | "log/slog" 6 | "strings" 7 | 8 | commonpb "go.opentelemetry.io/proto/otlp/common/v1" 9 | tracepb "go.opentelemetry.io/proto/otlp/trace/v1" 10 | ) 11 | 12 | type TraceConfig struct { 13 | Data []SpanData `js:"data"` 14 | } 15 | 16 | type SpanData struct { 17 | TraceID string `js:"traceId"` 18 | SpanID string `js:"spanId"` 19 | ParentSpanID string `js:"parentSpanId"` 20 | Name string `js:"name"` 21 | Attributes map[string]interface{} `js:"attributes"` 22 | Kind string `js:"kind"` 23 | StartTimeUnixNano uint64 `js:"startTimeUnixNano"` 24 | EndTimeUnixNano uint64 `js:"endTimeUnixNano"` 25 | } 26 | 27 | func ResourceSpans(resourceAttrs []*commonpb.KeyValue, config TraceConfig) *tracepb.ResourceSpans { 28 | return &tracepb.ResourceSpans{ 29 | Resource: resource(resourceAttrs), 30 | ScopeSpans: []*tracepb.ScopeSpans{ 31 | { 32 | Spans: spans(config.Data), 33 | }, 34 | }, 35 | } 36 | } 37 | 38 | func spans(data []SpanData) []*tracepb.Span { 39 | spans := []*tracepb.Span{} 40 | 41 | for _, spanData := range data { 42 | spans = append(spans, span(spanData)) 43 | } 44 | 45 | return spans 46 | } 47 | 48 | func span(data SpanData) *tracepb.Span { 49 | traceID, err := hex.DecodeString(data.TraceID) 50 | if err != nil { 51 | slog.Error("invalid trace id") 52 | } 53 | 54 | spanID, err := hex.DecodeString(data.SpanID) 55 | if err != nil { 56 | slog.Error("invalid span id") 57 | } 58 | 59 | parentSpanID, err := hex.DecodeString(data.ParentSpanID) 60 | if err != nil { 61 | slog.Error("invalid parent span id") 62 | } 63 | 64 | return &tracepb.Span{ 65 | TraceId: traceID, 66 | SpanId: spanID, 67 | ParentSpanId: parentSpanID, 68 | Name: data.Name, 69 | Attributes: ToAttributes(data.Attributes), 70 | Kind: getSpanKind(data.Kind), 71 | StartTimeUnixNano: data.StartTimeUnixNano, 72 | EndTimeUnixNano: data.EndTimeUnixNano, 73 | } 74 | } 75 | 76 | func getSpanKind(kind string) tracepb.Span_SpanKind { 77 | switch strings.ToLower(kind) { 78 | case "client": 79 | return tracepb.Span_SPAN_KIND_CLIENT 80 | case "server": 81 | return tracepb.Span_SPAN_KIND_SERVER 82 | case "consumer": 83 | return tracepb.Span_SPAN_KIND_CONSUMER 84 | case "producer": 85 | return tracepb.Span_SPAN_KIND_PRODUCER 86 | } 87 | 88 | return tracepb.Span_SPAN_KIND_INTERNAL 89 | } 90 | -------------------------------------------------------------------------------- /trace_id_gen.go: -------------------------------------------------------------------------------- 1 | package generator 2 | 3 | import ( 4 | "encoding/hex" 5 | "math/rand" 6 | "sync" 7 | ) 8 | 9 | type IDGenerator struct { 10 | sync.Mutex 11 | randSource *rand.Rand 12 | } 13 | 14 | func (idGen *IDGenerator) NewTraceID() string { 15 | idGen.Lock() 16 | defer idGen.Unlock() 17 | 18 | var id [16]byte 19 | 20 | _, _ = idGen.randSource.Read(id[:]) 21 | 22 | return hex.EncodeToString(id[:]) 23 | } 24 | 25 | func (idGen *IDGenerator) NewSpanID() string { 26 | idGen.Lock() 27 | defer idGen.Unlock() 28 | 29 | var id [8]byte 30 | 31 | _, _ = idGen.randSource.Read(id[:]) 32 | 33 | return hex.EncodeToString(id[:]) 34 | } 35 | --------------------------------------------------------------------------------