mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
252 lines
7.5 KiB
TypeScript
252 lines
7.5 KiB
TypeScript
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,
|
||
});
|
||
}
|