load-tests/otel-traces/test.ts
Rama main · réplica de lectura del repositorio original.
import './bru.ts';
import { randomIntBetween, randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';
import * as immer from 'https://unpkg.com/immer@10.1.3/dist/immer.mjs';
import { check } from 'k6';
import http from 'k6/http';
// Cardinality Variables Start
const countUniqueErrorCodes = 200;
const countUniqueClients = 200;
const appVersionsPerClient = 100;
// Cardinality Variables End
//
export const options = {
scenarios: {
example_scenario: {
executor: 'constant-vus', // Keeps a constant number of VUs
vus: 10, // 10 virtual users
duration: '1m', // Run for 10 minutes
},
},
};
const otelEndpointUrl = __ENV.OTEL_ENDPOINT || 'http://localhost:4318/v1/traces';
console.log(
`Endpoint: ${otelEndpointUrl}. (Overwrite using the OTEL_ENDPOINT environment variable)`,
);
const HIVE_ORGANIZATION_ACCESS_TOKEN = __ENV.HIVE_ORGANIZATION_ACCESS_TOKEN;
if (!HIVE_ORGANIZATION_ACCESS_TOKEN) {
throw new Error('Environment variable HIVE_ORGANIZATION_ACCESS_TOKEN is missing.');
}
const HIVE_TARGET_REF = __ENV.HIVE_TARGET_REF; //'debug/hive/dev';
if (!HIVE_TARGET_REF) {
throw new Error('Environment variable HIVE_TARGET_REF is missing.');
}
// A helper to generate a random 16-byte trace/span ID in hex
function randomId(bytes: number = 32): string {
let traceId = '';
for (let i = 0; i < bytes; i++) {
// generate random nibble (0–15)
const nibble = Math.floor(Math.random() * 16);
traceId += nibble.toString(16);
}
// ensure not all zero (very unlikely)
if (/^0+$/.test(traceId)) {
return randomId(bytes);
}
return traceId;
}
function toTimeUnixNano(date = new Date()) {
const milliseconds = date.getTime(); // ms since epoch
const nanoseconds = BigInt(milliseconds) * 1_000_000n; // ns = ms × 1_000_000
return nanoseconds;
}
function getRandomIndex(length: number) {
return Math.floor(Math.random() * length);
}
function randomArrayItem<T>(arr: Array<T>) {
return arr[getRandomIndex(arr.length)];
}
const clientNames = new Array(countUniqueClients)
.fill(null)
.map(() => randomString(randomIntBetween(5, 30)));
const appVersions = new Map<string, Array<string>>();
for (const name of clientNames) {
const versions = new Array<string>();
for (let i = 0; i <= appVersionsPerClient; i++) {
versions.push(randomString(20));
}
appVersions.set(name, versions);
}
function generateRandomClient() {
const name = randomArrayItem(clientNames);
const version = randomArrayItem(appVersions.get(name)!);
return {
name,
version,
};
}
const errorCodes = new Array(countUniqueErrorCodes)
.fill(null)
.map(() => randomString(randomIntBetween(3, 30)));
function getRandomErrorCodes() {
if (randomIntBetween(0, 10) > 3) {
return [];
}
if (randomIntBetween(0, 10) > 3) {
return new Array(randomIntBetween(1, 10)).fill(null).map(() => randomArrayItem(errorCodes));
}
return [randomArrayItem(errorCodes)];
}
// graphql query document size
// operation name length
//
const references: Array<Reference> = [
open('./../../scripts/seed-traces/sample-introspection.json'),
open('./../../scripts/seed-traces/sample-my-profile.json'),
open('./../../scripts/seed-traces/sample-products-overview.json'),
open('./../../scripts/seed-traces/sample-user-review.json'),
open('./../../scripts/seed-traces/sample-user-review-error-missing-variables.json'),
open('./../../scripts/seed-traces/sample-user-review-not-found.json'),
].map(res => JSON.parse(res));
function mutate(currentTime: Date, reference: Reference) {
const newTraceId = randomId();
const newSpanIds = new Map<string, string>();
function getNewSpanId(spanId: string) {
let newSpanId = newSpanIds.get(spanId);
if (!newSpanId) {
newSpanId = randomId(16);
newSpanIds.set(spanId, newSpanId);
}
return newSpanId;
}
let rootTrace:
| Reference[number]['resourceSpans'][number]['scopeSpans'][number]['spans'][number]
| null = null;
for (const payload of reference) {
for (const resourceSpan of payload.resourceSpans) {
for (const scopeSpan of resourceSpan.scopeSpans) {
for (const span of scopeSpan.spans) {
if (span.parentSpanId === undefined) {
rootTrace = span;
const client = generateRandomClient();
rootTrace.attributes.push(
{
key: 'hive.client.name',
value: { stringValue: client.name },
},
{
key: 'hive.client.version',
value: { stringValue: client.version },
},
// TODO: actually calculate this based on the operation.
{
key: 'hive.graphql.operation.hash',
value: { stringValue: randomString(20) },
},
);
const errors = getRandomErrorCodes();
if (errors.length) {
rootTrace.attributes.push(
{
key: 'hive.graphql.error.codes',
value: {
arrayValue: {
values: errors.map(code => ({ stringValue: code })),
},
},
},
{
key: 'hive.graphql.error.count',
value: { intValue: errors.length },
},
);
}
break;
}
}
}
}
}
if (!rootTrace) {
throw new Error('Parent Span must always be the first span in the file.');
}
const startTime = BigInt(rootTrace.startTimeUnixNano);
const currentTimeB = toTimeUnixNano(currentTime);
for (const payload of reference) {
for (const resourceSpans of payload.resourceSpans) {
for (const scopeSpan of resourceSpans.scopeSpans) {
for (const span of scopeSpan.spans) {
if (span.parentSpanId) {
span.parentSpanId = getNewSpanId(span.parentSpanId);
}
span.spanId = getNewSpanId(span.spanId);
span.traceId = newTraceId;
const spanStartTime = BigInt(span.startTimeUnixNano);
const spanEndTime = BigInt(span.endTimeUnixNano);
const spanDuration = spanEndTime - spanStartTime;
const spanOffset = spanStartTime - startTime;
const newStartTime = currentTimeB + spanOffset;
span.startTimeUnixNano = newStartTime.toString();
span.endTimeUnixNano = (newStartTime + spanDuration).toString();
if (span.events.length) {
for (const event of span.events) {
const spanStartTime = BigInt(event.timeUnixNano);
const spanOffset = spanStartTime - startTime;
const newStartTime = currentTimeB + spanOffset;
event.timeUnixNano = newStartTime.toString();
}
}
}
}
}
}
}
function createTrace(date: Date, reference: Reference) {
return immer.produce(reference, draft => mutate(date, draft));
}
const data = new Array(50).fill(null).flatMap(() => {
const reference = randomArrayItem(references);
const tracePayloads = createTrace(new Date(), reference);
return tracePayloads.flatMap(payload => payload.resourceSpans);
});
export default function () {
const response = http.post(otelEndpointUrl, JSON.stringify({ resourceSpans: data }), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${HIVE_ORGANIZATION_ACCESS_TOKEN}`,
'x-hive-target-ref': HIVE_TARGET_REF,
},
});
check(response, {
'is status 200': r => r.status === 200,
});
}