mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
Support custom resource attributes for Hyperdx frontend app's internal telemetry (#2067)
This commit is contained in:
parent
c4a1311e86
commit
6e9a553da2
3 changed files with 111 additions and 1 deletions
|
|
@ -3,6 +3,7 @@ import type { NextPage } from 'next';
|
|||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { NextAdapter } from 'next-query-params';
|
||||
import { env } from 'next-runtime-env';
|
||||
import randomUUID from 'crypto-randomuuid';
|
||||
import { enableMapSet } from 'immer';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
|
|
@ -16,7 +17,7 @@ import {
|
|||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
|
||||
import { DynamicFavicon } from '@/components/DynamicFavicon';
|
||||
import { IS_LOCAL_MODE } from '@/config';
|
||||
import { IS_LOCAL_MODE, parseResourceAttributes } from '@/config';
|
||||
import {
|
||||
DEFAULT_FONT_VAR,
|
||||
FONT_VAR_MAP,
|
||||
|
|
@ -136,12 +137,19 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||
.then(res => res.json())
|
||||
.then((_jsonData?: NextApiConfigResponseData) => {
|
||||
if (_jsonData?.apiKey) {
|
||||
const frontendAttrs = parseResourceAttributes(
|
||||
env('NEXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES') ?? '',
|
||||
);
|
||||
HyperDX.init({
|
||||
apiKey: _jsonData.apiKey,
|
||||
consoleCapture: true,
|
||||
maskAllInputs: true,
|
||||
maskAllText: true,
|
||||
// service.version is applied last so it always reflects the
|
||||
// NEXT_PUBLIC_APP_VERSION and cannot be overridden by
|
||||
// NEXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES.
|
||||
otelResourceAttributes: {
|
||||
...frontendAttrs,
|
||||
'service.version': process.env.NEXT_PUBLIC_APP_VERSION,
|
||||
},
|
||||
service: _jsonData.serviceName,
|
||||
|
|
|
|||
83
packages/app/src/__tests__/config.test.ts
Normal file
83
packages/app/src/__tests__/config.test.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { parseResourceAttributes } from '@/config';
|
||||
|
||||
describe('parseResourceAttributes', () => {
|
||||
it('parses a standard comma-separated string', () => {
|
||||
const raw =
|
||||
'service.namespace=observability,deployment.environment=prod,k8s.cluster.name=us-west-2';
|
||||
expect(parseResourceAttributes(raw)).toEqual({
|
||||
'service.namespace': 'observability',
|
||||
'deployment.environment': 'prod',
|
||||
'k8s.cluster.name': 'us-west-2',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty object for an empty string', () => {
|
||||
expect(parseResourceAttributes('')).toEqual({});
|
||||
});
|
||||
|
||||
it('handles a single key=value pair', () => {
|
||||
expect(parseResourceAttributes('foo=bar')).toEqual({ foo: 'bar' });
|
||||
});
|
||||
|
||||
it('handles values containing equals signs', () => {
|
||||
expect(parseResourceAttributes('url=https://example.com?a=1')).toEqual({
|
||||
url: 'https://example.com?a=1',
|
||||
});
|
||||
});
|
||||
|
||||
it('skips malformed entries without an equals sign', () => {
|
||||
expect(parseResourceAttributes('good=value,badentry,ok=yes')).toEqual({
|
||||
good: 'value',
|
||||
ok: 'yes',
|
||||
});
|
||||
});
|
||||
|
||||
it('skips entries where key is empty (leading equals)', () => {
|
||||
expect(parseResourceAttributes('=nokey,valid=value')).toEqual({
|
||||
valid: 'value',
|
||||
});
|
||||
});
|
||||
|
||||
it('handles trailing commas gracefully', () => {
|
||||
expect(parseResourceAttributes('a=1,b=2,')).toEqual({ a: '1', b: '2' });
|
||||
});
|
||||
|
||||
it('handles leading commas gracefully', () => {
|
||||
expect(parseResourceAttributes(',a=1,b=2')).toEqual({ a: '1', b: '2' });
|
||||
});
|
||||
|
||||
it('allows empty values', () => {
|
||||
expect(parseResourceAttributes('key=')).toEqual({ key: '' });
|
||||
});
|
||||
|
||||
it('last value wins for duplicate keys', () => {
|
||||
expect(parseResourceAttributes('k=first,k=second')).toEqual({
|
||||
k: 'second',
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes percent-encoded commas in values', () => {
|
||||
expect(parseResourceAttributes('tags=a%2Cb%2Cc')).toEqual({
|
||||
tags: 'a,b,c',
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes percent-encoded equals in values', () => {
|
||||
expect(parseResourceAttributes('expr=x%3D1')).toEqual({
|
||||
expr: 'x=1',
|
||||
});
|
||||
});
|
||||
|
||||
it('decodes percent-encoded keys', () => {
|
||||
expect(parseResourceAttributes('my%2Ekey=value')).toEqual({
|
||||
'my.key': 'value',
|
||||
});
|
||||
});
|
||||
|
||||
it('round-trips values with both encoded commas and equals', () => {
|
||||
expect(parseResourceAttributes('q=a%3D1%2Cb%3D2,other=plain')).toEqual({
|
||||
q: 'a=1,b=2',
|
||||
other: 'plain',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -14,6 +14,25 @@ export const HDX_SERVICE_NAME =
|
|||
process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME ?? 'hdx-oss-dev-app';
|
||||
export const HDX_EXPORTER_ENABLED =
|
||||
(process.env.HDX_EXPORTER_ENABLED ?? 'true') === 'true';
|
||||
|
||||
export function parseResourceAttributes(raw: string): Record<string, string> {
|
||||
return raw
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.reduce(
|
||||
(acc, pair) => {
|
||||
const idx = pair.indexOf('=');
|
||||
if (idx > 0) {
|
||||
acc[decodeURIComponent(pair.slice(0, idx))] = decodeURIComponent(
|
||||
pair.slice(idx + 1),
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
}
|
||||
|
||||
export const HDX_COLLECTOR_URL =
|
||||
process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT ??
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
|
||||
|
|
|
|||
Loading…
Reference in a new issue