mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-10-05 06:18:04 +03:00
console: Add OpenTelemetry to Metadata
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7401 GitOrigin-RevId: 13b768e4a3d940bf2acf8fbc1fe38f89a6c10f05
This commit is contained in:
parent
85cda65261
commit
24cba66344
14
console/package-lock.json
generated
14
console/package-lock.json
generated
@ -104,7 +104,7 @@
|
||||
"use-async-effect": "2.2.7",
|
||||
"uuid": "8.3.2",
|
||||
"xstate": "^4.30.1",
|
||||
"zod": "3.17.3"
|
||||
"zod": "3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.13.10",
|
||||
@ -47408,9 +47408,9 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.17.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.17.3.tgz",
|
||||
"integrity": "sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg==",
|
||||
"version": "3.20.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz",
|
||||
"integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@ -84107,9 +84107,9 @@
|
||||
}
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.17.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.17.3.tgz",
|
||||
"integrity": "sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg=="
|
||||
"version": "3.20.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz",
|
||||
"integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ=="
|
||||
},
|
||||
"zwitch": {
|
||||
"version": "1.0.5",
|
||||
|
@ -174,7 +174,7 @@
|
||||
"use-async-effect": "2.2.7",
|
||||
"uuid": "8.3.2",
|
||||
"xstate": "^4.30.1",
|
||||
"zod": "3.17.3"
|
||||
"zod": "3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.13.10",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {
|
||||
MetadataUtils,
|
||||
useInvalidateMetata,
|
||||
useInvalidateMetadata,
|
||||
useMetadata,
|
||||
} from '@/features/hasura-metadata-api';
|
||||
import { MetadataTable } from '@/features/hasura-metadata-types';
|
||||
@ -16,7 +16,7 @@ export const useUpdateTableConfiguration = (
|
||||
|
||||
const { fireNotification } = useFireNotification();
|
||||
|
||||
const invalidateMetadata = useInvalidateMetata();
|
||||
const invalidateMetadata = useInvalidateMetadata();
|
||||
|
||||
const { data } = useMetadata(m => ({
|
||||
source: MetadataUtils.findMetadataSource(dataSourceName, m),
|
||||
|
@ -2,7 +2,7 @@ import { useMetadataMigration } from '@/features/MetadataAPI';
|
||||
import { useFireNotification } from '@/new-components/Notifications';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useInvalidateMetata } from '@/features/hasura-metadata-api';
|
||||
import { useInvalidateMetadata } from '@/features/hasura-metadata-api';
|
||||
import { useIsUnmounted } from '@/components/Services/Data';
|
||||
import { useMetadataSource, tablesQueryKey } from '@/features/Data';
|
||||
import type { TrackableTable } from '../types';
|
||||
@ -18,7 +18,7 @@ export const useTrackTable = (dataSourceName: string) => {
|
||||
const unMounted = useIsUnmounted();
|
||||
|
||||
const { data } = useMetadataSource(dataSourceName);
|
||||
const invalidateMetadata = useInvalidateMetata();
|
||||
const invalidateMetadata = useInvalidateMetadata();
|
||||
|
||||
const metadata = data?.metadata;
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { HasuraMetadataV3 } from '@/metadata/types';
|
||||
import { MetadataQueryType } from '@/metadata/queryUtils';
|
||||
import { IntrospectionQuery } from 'graphql';
|
||||
import { RemoteField } from '../RemoteRelationships/RemoteSchemaRelationships/types';
|
||||
import type { HasuraMetadataV3 } from '@/metadata/types';
|
||||
import type { MetadataQueryType } from '@/metadata/queryUtils';
|
||||
import type { OpenTelemetryQueries } from '@/features/hasura-metadata-types';
|
||||
import type { IntrospectionQuery } from 'graphql';
|
||||
import type { RemoteField } from '../RemoteRelationships/RemoteSchemaRelationships/types';
|
||||
|
||||
import { DataTarget } from '../Datasources';
|
||||
import type { DataTarget } from '../Datasources';
|
||||
|
||||
export interface MetadataResponse {
|
||||
resource_version: number;
|
||||
@ -92,4 +93,5 @@ export type AllMetadataQueries =
|
||||
export type allowedMetadataTypes =
|
||||
| typeof allowedMetadataTypesArr[number]
|
||||
| AllMetadataQueries
|
||||
| MetadataQueryType;
|
||||
| MetadataQueryType
|
||||
| OpenTelemetryQueries;
|
||||
|
@ -16,9 +16,6 @@ import { OpenTelemetryConfig } from './OpenTelemetryConfig';
|
||||
export default {
|
||||
title: 'Features/OpenTelemetryConfig/OpenTelemetryConfig',
|
||||
component: OpenTelemetryConfig,
|
||||
argTypes: {
|
||||
updateOpenTelemetryConfig: { action: true },
|
||||
},
|
||||
} as ComponentMeta<typeof OpenTelemetryConfig>;
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
@ -46,8 +43,8 @@ Disabled.storyName = '💠 Disabled';
|
||||
// a Partial<Props> and then developers cannot know that they break the story by changing the
|
||||
// component props
|
||||
const disabledArgs: ComponentPropsWithoutRef<typeof OpenTelemetryConfig> = {
|
||||
updateOpenTelemetryConfig: (...args) => {
|
||||
action('updateOpenTelemetryConfig')(...args);
|
||||
updateOpenTelemetry: (...args) => {
|
||||
action('updateOpenTelemetry')(...args);
|
||||
|
||||
// Fake the server loading
|
||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
||||
@ -62,7 +59,7 @@ const disabledArgs: ComponentPropsWithoutRef<typeof OpenTelemetryConfig> = {
|
||||
batchSize: 512,
|
||||
attributes: [],
|
||||
dataType: ['traces'],
|
||||
connectionType: 'http',
|
||||
connectionType: 'http/protobuf',
|
||||
},
|
||||
};
|
||||
Disabled.args = disabledArgs;
|
||||
@ -94,8 +91,8 @@ Enabled.storyName = '💠 Enabled';
|
||||
// a Partial<Props> and then developers cannot know that they break the story by changing the
|
||||
// component props
|
||||
const enabledArgs: ComponentPropsWithoutRef<typeof OpenTelemetryConfig> = {
|
||||
updateOpenTelemetryConfig: (...args) => {
|
||||
action('updateOpenTelemetryConfig')(...args);
|
||||
updateOpenTelemetry: (...args) => {
|
||||
action('updateOpenTelemetry')(...args);
|
||||
|
||||
// Fake the server loading
|
||||
return new Promise(resolve => setTimeout(resolve, 1000));
|
||||
@ -110,7 +107,7 @@ const enabledArgs: ComponentPropsWithoutRef<typeof OpenTelemetryConfig> = {
|
||||
batchSize: 512,
|
||||
attributes: [],
|
||||
dataType: ['traces'],
|
||||
connectionType: 'http',
|
||||
connectionType: 'http/protobuf',
|
||||
},
|
||||
};
|
||||
Enabled.args = enabledArgs;
|
||||
@ -142,7 +139,7 @@ Skeleton.storyName = '💠 Skeleton';
|
||||
// a Partial<Props> and then developers cannot know that they break the story by changing the
|
||||
// component props
|
||||
const skeletonArgs: ComponentPropsWithoutRef<typeof OpenTelemetryConfig> = {
|
||||
updateOpenTelemetryConfig: (...args) => {
|
||||
updateOpenTelemetry: (...args) => {
|
||||
action('updateOpenT')(...args);
|
||||
|
||||
// Fake the server loading
|
||||
|
@ -9,14 +9,14 @@ import { Header } from './components/Header/Header';
|
||||
interface OpenTelemetryConfigProps {
|
||||
skeletonMode: boolean;
|
||||
metadataFormValues: FormValues | undefined;
|
||||
updateOpenTelemetryConfig: (formValues: FormValues) => Promise<void>;
|
||||
updateOpenTelemetry: (formValues: FormValues) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the OpenTelemetry page without any external dependency.
|
||||
*/
|
||||
export function OpenTelemetryConfig(props: OpenTelemetryConfigProps) {
|
||||
const { skeletonMode, metadataFormValues, updateOpenTelemetryConfig } = props;
|
||||
const { skeletonMode, metadataFormValues, updateOpenTelemetry } = props;
|
||||
|
||||
const formValues = metadataFormValues || defaultValues;
|
||||
|
||||
@ -33,7 +33,7 @@ export function OpenTelemetryConfig(props: OpenTelemetryConfigProps) {
|
||||
<div>
|
||||
{/* This div avoid space-y-md separating too much the toggle and the form */}
|
||||
<Form
|
||||
onSubmit={updateOpenTelemetryConfig}
|
||||
onSubmit={updateOpenTelemetry}
|
||||
defaultValues={formValues}
|
||||
skeletonMode={skeletonMode}
|
||||
/>
|
||||
|
@ -219,7 +219,7 @@ HappyPath.play = async ({ args, canvasElement }) => {
|
||||
expect(receivedValues).toMatchObject<FormValues>({
|
||||
enabled: true,
|
||||
endpoint: 'http://hasura.io',
|
||||
connectionType: 'http',
|
||||
connectionType: 'http/protobuf',
|
||||
dataType: ['traces'],
|
||||
batchSize: 100,
|
||||
headers: [
|
||||
|
@ -61,7 +61,7 @@ export function Form(props: FormProps) {
|
||||
name="connectionType"
|
||||
label="Connection Type"
|
||||
tooltip="The protocol to be used for the communication with the receiver. At the moment, only HTTP is supported."
|
||||
options={[{ value: 'http', label: 'HTTP' }]}
|
||||
options={[{ value: 'http/protobuf', label: 'HTTP' }]}
|
||||
loading={skeletonMode}
|
||||
// At the beginning, only one Connection Type is available, hence it does not make sense
|
||||
// to enable the users to change it.
|
||||
|
@ -1,36 +1,62 @@
|
||||
import { z } from 'zod';
|
||||
import { requestHeadersSelectorSchema } from '@/new-components/RequestHeadersSelector';
|
||||
|
||||
export const formSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
const endPointSchema = z.string().url({ message: 'Invalid URL' });
|
||||
|
||||
endpoint: z.string().url({ message: 'Invalid URL' }),
|
||||
|
||||
connectionType: z.enum(['http', 'http2']),
|
||||
// --------------------------------------------------
|
||||
// SCHEMA
|
||||
// --------------------------------------------------
|
||||
const normalProperties = {
|
||||
// CONNECTION TYPE
|
||||
connectionType: z.enum(['http/protobuf']),
|
||||
|
||||
dataType: z.enum(['traces']).array(),
|
||||
|
||||
// HEADERS
|
||||
// Names should be validated against /^[a-zA-Z0-9]*$/ but we must be sure the server performs the
|
||||
// same check. Values should be validated against /^[a-zA-Z0-9_ :;.,\\\/"'\?\!\(\)\{\}\[\]@<>=\-+\*#$&`|~^%]*$/
|
||||
// see: the CloudFlare docs as an example https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/#:~:text=The%20value%20of%20the%20HTTP,%24%26%60%7C~%5E%25
|
||||
// More: empty env vars should not be accepted!
|
||||
headers: requestHeadersSelectorSchema,
|
||||
|
||||
// ATM, only string values are accepted out of the wide variety of values OpenTelemetry accepts
|
||||
// see: https://opentelemetry.io/docs/reference/specification/common/#attribute
|
||||
// ATTENTION: a restricted version of requestHeadersSelectorSchema should be used here! Because
|
||||
// the attributes cannot be sent as env vars, even if the same RequestHeadersSelector component is
|
||||
// used. RequestHeadersSelector accepts a typeSelect prop but the schema does not reflect it!
|
||||
attributes: requestHeadersSelectorSchema,
|
||||
|
||||
// TODO: migrate to coerce
|
||||
batchSize: z.preprocess(
|
||||
// see: https://github.com/colinhacks/zod/discussions/330#discussioncomment-4043200
|
||||
Number,
|
||||
z
|
||||
.number()
|
||||
// The message is the same for min and max to avoid a "The value should be greater than 1"
|
||||
// error and then another "The value should be lower than 512" one. By using the same message,
|
||||
// the user will only see one error and understand everything at once.
|
||||
.min(1, { message: 'The value should be between 1 and 512' })
|
||||
.max(512, { message: 'The value should be between 1 and 512' })
|
||||
),
|
||||
});
|
||||
batchSize: z.coerce
|
||||
.number()
|
||||
// The message is the same for min and max to avoid a "The value should be greater than 1"
|
||||
// error and then another "The value should be lower than 512" one. By using the same message,
|
||||
// the user will only see one error and understand everything at once.
|
||||
.min(1, { message: 'The value should be between 1 and 512' })
|
||||
.max(512, { message: 'The value should be between 1 and 512' }),
|
||||
};
|
||||
|
||||
export const formSchema = z.discriminatedUnion('enabled', [
|
||||
z.object({
|
||||
...normalProperties,
|
||||
|
||||
enabled: z.literal(true),
|
||||
endpoint: endPointSchema,
|
||||
}),
|
||||
z.object({
|
||||
...normalProperties,
|
||||
|
||||
enabled: z.literal(false),
|
||||
endpoint: z.union([
|
||||
endPointSchema,
|
||||
// When OpenTelemetry is disabled, the endpoint is not mandatory
|
||||
z.literal(''),
|
||||
]),
|
||||
}),
|
||||
]);
|
||||
|
||||
// --------------------------------------------------
|
||||
// FORM VALUES
|
||||
// --------------------------------------------------
|
||||
export type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export const defaultValues: FormValues = {
|
||||
@ -43,7 +69,7 @@ export const defaultValues: FormValues = {
|
||||
// At the time of writing, the server sets 512 as the default value.
|
||||
batchSize: 512,
|
||||
|
||||
connectionType: 'http',
|
||||
connectionType: 'http/protobuf',
|
||||
|
||||
dataType: ['traces'],
|
||||
|
||||
|
@ -0,0 +1,82 @@
|
||||
import {
|
||||
formAttributesToMetadataAttributes,
|
||||
metadataAttributesToFormAttributes,
|
||||
} from './metadataAttributesToFormAttributes';
|
||||
|
||||
describe('Metadata <--> Form Values', () => {
|
||||
describe.each`
|
||||
metadataAttributes | formValues
|
||||
${[]} | ${[]}
|
||||
${[{ name: 'foo', value: 'bar' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_value' }]}
|
||||
${[{ name: 'foo', value: 'bar' }, { name: 'baz', value: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: 'baz', value: 'qux', type: 'from_value' }]}
|
||||
`(
|
||||
// TODO: fix the test description variables
|
||||
'Metadata $metadataAttributes <--> Form Values $formValues conversion',
|
||||
({ metadataAttributes, formValues }) => {
|
||||
it('Given the metadata, should return the form values', () => {
|
||||
expect(metadataAttributesToFormAttributes(metadataAttributes)).toEqual(
|
||||
formValues
|
||||
);
|
||||
});
|
||||
|
||||
it('Given the form values, should return the metadata', () => {
|
||||
expect(formAttributesToMetadataAttributes(formValues)).toEqual(
|
||||
metadataAttributes
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe.each`
|
||||
metadataAttributes | formValues
|
||||
${[{ name: 'foo', value: 'bar' }, { name: 'foo', value: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: 'foo', value: 'qux', type: 'from_value' }]}
|
||||
`(
|
||||
// TODO: fix the test description variables
|
||||
'When there are duplicated attributes, should handle them. Given $metadataAttributes and $formValues',
|
||||
({ metadataAttributes, formValues }) => {
|
||||
it('Given the metadata, should return the form values', () => {
|
||||
expect(metadataAttributesToFormAttributes(metadataAttributes)).toEqual(
|
||||
formValues
|
||||
);
|
||||
});
|
||||
|
||||
it('Given the form values, should return the metadata', () => {
|
||||
expect(formAttributesToMetadataAttributes(formValues)).toEqual(
|
||||
metadataAttributes
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe('Nameless attributes in form values', () => {
|
||||
test.each`
|
||||
formValues | metadataAttributes
|
||||
${[{ name: '', value: '', type: 'from_value' }]} | ${[]}
|
||||
${[{ name: '', value: '', type: 'from_value' }, { name: '', value: '', type: 'from_value' }]} | ${[]}
|
||||
${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: '', value: '', type: 'from_value' }]} | ${[{ name: 'foo', value: 'bar' }]}
|
||||
${[{ name: '', value: '', type: 'from_value' }, { name: 'foo', value: 'bar', type: 'from_value' }]} | ${[{ name: 'foo', value: 'bar' }]}
|
||||
${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: '', value: '', type: 'from_value' }, { name: 'baz', value: 'qux', type: 'from_value' }]} | ${[{ name: 'foo', value: 'bar' }, { name: 'baz', value: 'qux' }]}
|
||||
`(
|
||||
'when invoked with $formValues, should return $metadataAttributes',
|
||||
({ metadataAttributes, formValues }) => {
|
||||
expect(formAttributesToMetadataAttributes(formValues)).toEqual(
|
||||
metadataAttributes
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('Valueless attributes in form values', () => {
|
||||
test.each`
|
||||
formValues | metadataAttributes
|
||||
${[{ name: 'foo', value: '', type: 'from_value' }]} | ${[{ name: 'foo', value: '' }]}
|
||||
`(
|
||||
'when invoked with $formValues, should return $metadataAttributes',
|
||||
({ metadataAttributes, formValues }) => {
|
||||
expect(formAttributesToMetadataAttributes(formValues)).toEqual(
|
||||
metadataAttributes
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,40 @@
|
||||
import type { OpenTelemetry } from '@/features/hasura-metadata-types';
|
||||
import type { FormValues } from '../../../OpenTelemetryConfig/components/Form/schema';
|
||||
|
||||
type MetadataAttributes = OpenTelemetry['exporter_otlp']['resource_attributes'];
|
||||
type FormAttributes = FormValues['attributes'];
|
||||
|
||||
/**
|
||||
* Convert the OpenTelemetry attributes into the corresponding form values.
|
||||
*/
|
||||
export function metadataAttributesToFormAttributes(
|
||||
metadataAttributes: MetadataAttributes
|
||||
) {
|
||||
return metadataAttributes.reduce<FormAttributes>((acc, metadataAttribute) => {
|
||||
acc.push({
|
||||
name: metadataAttribute.name,
|
||||
value: metadataAttribute.value,
|
||||
type: 'from_value',
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the form attributes into the corresponding metadata values.
|
||||
*/
|
||||
export function formAttributesToMetadataAttributes(
|
||||
formAttributes: FormAttributes
|
||||
) {
|
||||
return formAttributes.reduce<MetadataAttributes>((acc, formAttribute) => {
|
||||
if (formAttribute.name === '') return acc;
|
||||
|
||||
acc.push({
|
||||
name: formAttribute.name,
|
||||
value: formAttribute.value,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import {
|
||||
formHeadersToMetadataHeaders,
|
||||
metadataHeadersToFormHeaders,
|
||||
} from './metadataHeadersToFormHeaders';
|
||||
|
||||
describe('Metadata <--> Form Values', () => {
|
||||
describe.each`
|
||||
metadataHeaders | formValues
|
||||
${[]} | ${[]}
|
||||
${[{ name: 'foo', value: 'bar' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_value' }]}
|
||||
${[{ name: 'foo', value: 'bar' }, { name: 'baz', value: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: 'baz', value: 'qux', type: 'from_value' }]}
|
||||
${[{ name: 'foo', value_from_env: 'bar' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_env' }]}
|
||||
${[{ name: 'foo', value_from_env: 'bar' }, { name: 'baz', value_from_env: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_env' }, { name: 'baz', value: 'qux', type: 'from_env' }]}
|
||||
${[{ name: 'foo', value_from_env: 'bar' }, { name: 'baz', value: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_env' }, { name: 'baz', value: 'qux', type: 'from_value' }]}
|
||||
`(
|
||||
// TODO: fix the test description variables
|
||||
'Metadata $metadataHeaders <--> Form Values $formValues conversion',
|
||||
({ metadataHeaders, formValues }) => {
|
||||
it('Given the metadata, should return the form values', () => {
|
||||
expect(metadataHeadersToFormHeaders(metadataHeaders)).toEqual(
|
||||
formValues
|
||||
);
|
||||
});
|
||||
|
||||
it('Given the form values, should return the metadata', () => {
|
||||
expect(formHeadersToMetadataHeaders(formValues)).toEqual(
|
||||
metadataHeaders
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe.each`
|
||||
metadataHeaders | formValues
|
||||
${[{ name: 'foo', value: 'bar' }, { name: 'foo', value: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: 'foo', value: 'qux', type: 'from_value' }]}
|
||||
${[{ name: 'foo', value_from_env: 'bar' }, { name: 'foo', value_from_env: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_env' }, { name: 'foo', value: 'qux', type: 'from_env' }]}
|
||||
${[{ name: 'foo', value_from_env: 'bar' }, { name: 'foo', value: 'qux' }]} | ${[{ name: 'foo', value: 'bar', type: 'from_env' }, { name: 'foo', value: 'qux', type: 'from_value' }]}
|
||||
`(
|
||||
// TODO: fix the test description variables
|
||||
'When there are duplicated headers, should handle them. Given $metadataHeaders and $formValues',
|
||||
({ metadataHeaders, formValues }) => {
|
||||
it('Given the metadata, should return the form values', () => {
|
||||
expect(metadataHeadersToFormHeaders(metadataHeaders)).toEqual(
|
||||
formValues
|
||||
);
|
||||
});
|
||||
|
||||
it('Given the form values, should return the metadata', () => {
|
||||
expect(formHeadersToMetadataHeaders(formValues)).toEqual(
|
||||
metadataHeaders
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe('Nameless headers in form values', () => {
|
||||
test.each`
|
||||
formValues | metadataHeaders
|
||||
${[{ name: '', value: '', type: 'from_value' }]} | ${[]}
|
||||
${[{ name: '', value: '', type: 'from_value' }, { name: '', value: '', type: 'from_value' }]} | ${[]}
|
||||
${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: '', value: '', type: 'from_value' }]} | ${[{ name: 'foo', value: 'bar' }]}
|
||||
${[{ name: '', value: '', type: 'from_value' }, { name: 'foo', value: 'bar', type: 'from_value' }]} | ${[{ name: 'foo', value: 'bar' }]}
|
||||
${[{ name: 'foo', value: 'bar', type: 'from_value' }, { name: '', value: '', type: 'from_value' }, { name: 'baz', value: 'qux', type: 'from_value' }]} | ${[{ name: 'foo', value: 'bar' }, { name: 'baz', value: 'qux' }]}
|
||||
`(
|
||||
'when invoked with $formValues, should return $metadataHeaders',
|
||||
({ metadataHeaders, formValues }) => {
|
||||
expect(formHeadersToMetadataHeaders(formValues)).toEqual(
|
||||
metadataHeaders
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('Valueless headers in form values', () => {
|
||||
test.each`
|
||||
formValues | metadataHeaders
|
||||
${[{ name: 'foo', value: '', type: 'from_value' }]} | ${[{ name: 'foo', value: '' }]}
|
||||
`(
|
||||
'when invoked with $formValues, should return $metadataHeaders',
|
||||
({ metadataHeaders, formValues }) => {
|
||||
expect(formHeadersToMetadataHeaders(formValues)).toEqual(
|
||||
metadataHeaders
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,56 @@
|
||||
import type { OpenTelemetry } from '@/features/hasura-metadata-types';
|
||||
import type { FormValues } from '../../../OpenTelemetryConfig/components/Form/schema';
|
||||
|
||||
type MetadataHeaders = OpenTelemetry['exporter_otlp']['headers'];
|
||||
type FormHeaders = FormValues['headers'];
|
||||
|
||||
/**
|
||||
* Convert the OpenTelemetry headers into the corresponding form values.
|
||||
*/
|
||||
export function metadataHeadersToFormHeaders(metadataHeaders: MetadataHeaders) {
|
||||
return metadataHeaders.reduce<FormHeaders>((acc, metadataHeader) => {
|
||||
// The name cannot be used as a discriminator, but the presence of 'value_from_env' can
|
||||
if ('value_from_env' in metadataHeader) {
|
||||
acc.push({
|
||||
name: metadataHeader.name,
|
||||
value: metadataHeader.value_from_env,
|
||||
type: 'from_env',
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
name: metadataHeader.name,
|
||||
value: metadataHeader.value,
|
||||
type: 'from_value',
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the form headers into the corresponding metadata values.
|
||||
*/
|
||||
export function formHeadersToMetadataHeaders(formHeaders: FormHeaders) {
|
||||
return formHeaders.reduce<MetadataHeaders>((acc, formHeader) => {
|
||||
if (formHeader.name === '') return acc;
|
||||
|
||||
if (formHeader.type === 'from_env') {
|
||||
acc.push({
|
||||
name: formHeader.name,
|
||||
value_from_env: formHeader.value,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push({
|
||||
name: formHeader.name,
|
||||
value: formHeader.value,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import type { Metadata } from '@/features/hasura-metadata-types';
|
||||
import type { FormValues } from '../../OpenTelemetryConfig/components/Form/schema';
|
||||
|
||||
import {
|
||||
formValuesToOpenTelemetryConfig,
|
||||
openTelemetryConfigToFormValues,
|
||||
} from './openTelemetryConfigToFormValues';
|
||||
|
||||
describe('openTelemetryConfigToFormValues', () => {
|
||||
const openTelemetryConfig: Metadata['metadata']['opentelemetry'] = {
|
||||
status: 'disabled',
|
||||
|
||||
exporter_otlp: {
|
||||
resource_attributes: [],
|
||||
protocol: 'http/protobuf',
|
||||
headers: [{ name: 'baz', value: 'qux' }],
|
||||
otlp_traces_endpoint: 'https://hasura.io',
|
||||
},
|
||||
|
||||
data_types: ['traces'],
|
||||
batch_span_processor: {
|
||||
max_export_batch_size: 100,
|
||||
},
|
||||
};
|
||||
|
||||
const formValues: FormValues = {
|
||||
enabled: false,
|
||||
|
||||
batchSize: 100,
|
||||
attributes: [],
|
||||
endpoint: 'https://hasura.io',
|
||||
headers: [{ name: 'baz', value: 'qux', type: 'from_value' }],
|
||||
|
||||
// At the beginning, only one Data Type is available
|
||||
dataType: ['traces'],
|
||||
// At the beginning, only one Connection Type is available
|
||||
connectionType: 'http/protobuf',
|
||||
};
|
||||
|
||||
it('When passed with a OpenTelemetryConfig, should return the same values for the form', () => {
|
||||
expect(openTelemetryConfigToFormValues(openTelemetryConfig)).toEqual(
|
||||
formValues
|
||||
);
|
||||
});
|
||||
|
||||
it('When passed with some form values, should return the same values for the OpenTelemetryConfig', () => {
|
||||
expect(formValuesToOpenTelemetryConfig(formValues)).toEqual(
|
||||
openTelemetryConfig
|
||||
);
|
||||
});
|
||||
});
|
@ -0,0 +1,119 @@
|
||||
import type { Metadata, OpenTelemetry } from '@/features/hasura-metadata-types';
|
||||
import type { FormValues } from '../../OpenTelemetryConfig/components/Form/schema';
|
||||
|
||||
import { defaultValues } from '../../OpenTelemetryConfig/components/Form/schema';
|
||||
|
||||
import {
|
||||
formHeadersToMetadataHeaders,
|
||||
metadataHeadersToFormHeaders,
|
||||
} from './metadataToFormConverters/metadataHeadersToFormHeaders';
|
||||
import {
|
||||
formAttributesToMetadataAttributes,
|
||||
metadataAttributesToFormAttributes,
|
||||
} from './metadataToFormConverters/metadataAttributesToFormAttributes';
|
||||
|
||||
/**
|
||||
* Convert a metadata's OpenTelemetry configuration into its corresponding form values object.
|
||||
*
|
||||
* ATTENTION: It takes for granted the OpenTelemetry configuration received from the server respects
|
||||
* the type! Misalignments, if any, must be caught before calling openTelemetryConfigToFormValues!
|
||||
*/
|
||||
export function openTelemetryConfigToFormValues(
|
||||
openTelemetryConfig: Metadata['metadata']['opentelemetry']
|
||||
): FormValues {
|
||||
if (!openTelemetryConfig) return defaultValues;
|
||||
|
||||
if (openTelemetryConfig.status === 'disabled') {
|
||||
return {
|
||||
enabled: false,
|
||||
endpoint: openTelemetryConfig.exporter_otlp.otlp_traces_endpoint ?? '',
|
||||
headers: metadataHeadersToFormHeaders(
|
||||
openTelemetryConfig.exporter_otlp.headers
|
||||
),
|
||||
batchSize: openTelemetryConfig.batch_span_processor.max_export_batch_size,
|
||||
|
||||
attributes: metadataAttributesToFormAttributes(
|
||||
openTelemetryConfig.exporter_otlp.resource_attributes
|
||||
),
|
||||
|
||||
// At the beginning, only one Data Type is available
|
||||
dataType: ['traces'],
|
||||
// At the beginning, only one Connection Type is available
|
||||
connectionType: 'http/protobuf',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
endpoint: openTelemetryConfig.exporter_otlp.otlp_traces_endpoint,
|
||||
headers: metadataHeadersToFormHeaders(
|
||||
openTelemetryConfig.exporter_otlp.headers
|
||||
),
|
||||
batchSize: openTelemetryConfig.batch_span_processor.max_export_batch_size,
|
||||
|
||||
attributes: metadataAttributesToFormAttributes(
|
||||
openTelemetryConfig.exporter_otlp.resource_attributes
|
||||
),
|
||||
|
||||
// At the beginning, only one Data Type is available
|
||||
dataType: ['traces'],
|
||||
// At the beginning, only one Connection Type is available
|
||||
connectionType: 'http/protobuf',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the form values their corresponding metadata object.
|
||||
*/
|
||||
export function formValuesToOpenTelemetryConfig(
|
||||
formValues: FormValues
|
||||
): OpenTelemetry {
|
||||
const otlp_traces_endpoint = formValues.endpoint;
|
||||
const max_export_batch_size = formValues.batchSize;
|
||||
|
||||
// At the beginning, only one Connection Type is available
|
||||
const protocol = 'http/protobuf';
|
||||
|
||||
const headers = formHeadersToMetadataHeaders(formValues.headers);
|
||||
const resource_attributes = formAttributesToMetadataAttributes(
|
||||
formValues.attributes
|
||||
);
|
||||
|
||||
if (!formValues.enabled) {
|
||||
return {
|
||||
status: 'disabled',
|
||||
|
||||
// At the beginning, only one Data Type is available
|
||||
data_types: ['traces'],
|
||||
|
||||
exporter_otlp: {
|
||||
headers,
|
||||
protocol,
|
||||
resource_attributes,
|
||||
otlp_traces_endpoint,
|
||||
},
|
||||
|
||||
batch_span_processor: {
|
||||
max_export_batch_size,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'enabled',
|
||||
|
||||
// At the beginning, only one Data Type is available
|
||||
data_types: ['traces'],
|
||||
|
||||
exporter_otlp: {
|
||||
headers,
|
||||
protocol,
|
||||
resource_attributes,
|
||||
otlp_traces_endpoint,
|
||||
},
|
||||
|
||||
batch_span_processor: {
|
||||
max_export_batch_size,
|
||||
},
|
||||
};
|
||||
}
|
1
console/src/features/OpenTelemetryConfig/index.ts
Normal file
1
console/src/features/OpenTelemetryConfig/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './metadata.mock';
|
33
console/src/features/OpenTelemetryConfig/metadata.mock.ts
Normal file
33
console/src/features/OpenTelemetryConfig/metadata.mock.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import produce from 'immer';
|
||||
|
||||
import type { MetadataReducer } from '@/mocks/actions';
|
||||
import type { allowedMetadataTypes } from '@/features/MetadataAPI';
|
||||
import type {
|
||||
Metadata,
|
||||
SetOpenTelemetryConfigQuery,
|
||||
} from '@/features/hasura-metadata-types';
|
||||
import { parseOpenTelemetry } from '@/features/hasura-metadata-types';
|
||||
|
||||
export const openTelemetryConfigInitialData: Partial<Metadata['metadata']> = {
|
||||
opentelemetry: undefined,
|
||||
};
|
||||
|
||||
export const metadataHandlers: Partial<
|
||||
Record<allowedMetadataTypes, MetadataReducer>
|
||||
> = {
|
||||
// ATTENTION: the server errors that the Console prevents are not handled here
|
||||
set_opentelemetry_config: (state, action) => {
|
||||
// TODO: strongly type it
|
||||
const newOpenTelemetryConfig =
|
||||
action.args as SetOpenTelemetryConfigQuery['args'];
|
||||
|
||||
const result = parseOpenTelemetry(newOpenTelemetryConfig);
|
||||
if (!result.success) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return produce(state, draft => {
|
||||
draft.metadata.opentelemetry = newOpenTelemetryConfig;
|
||||
});
|
||||
},
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import * as MetadataSelectors from './selectors';
|
||||
import * as MetadataUtils from './utils';
|
||||
|
||||
export { useMetadata, useInvalidateMetata } from './useMetadata';
|
||||
export { useMetadata, useInvalidateMetadata } from './useMetadata';
|
||||
export { areTablesEqual } from './areTablesEqual';
|
||||
export { MetadataSelectors };
|
||||
export { MetadataUtils };
|
||||
|
@ -6,7 +6,7 @@ import { useQuery, useQueryClient } from 'react-query';
|
||||
|
||||
export const DEFAULT_STALE_TIME = 5 * 60000; // 5 minutes as default stale time
|
||||
|
||||
/*
|
||||
/*
|
||||
See the ./metadata-hooks for examples of how to use this hook
|
||||
Use the selector arg to tell react-query which part(s) of the metadata you want
|
||||
Default stale time is 5 minutes, but can be adjusted using the staleTime arg
|
||||
@ -14,7 +14,7 @@ export const DEFAULT_STALE_TIME = 5 * 60000; // 5 minutes as default stale time
|
||||
|
||||
const METADATA_QUERY_KEY = 'export_metadata';
|
||||
|
||||
export const useInvalidateMetata = () => {
|
||||
export const useInvalidateMetadata = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const invalidate = useCallback(
|
||||
() => queryClient.invalidateQueries([METADATA_QUERY_KEY]),
|
||||
@ -29,7 +29,7 @@ export const useMetadata = <T = Metadata>(
|
||||
staleTime: number = DEFAULT_STALE_TIME
|
||||
) => {
|
||||
const httpClient = useHttpClient();
|
||||
const invalidateMetadata = useInvalidateMetata();
|
||||
const invalidateMetadata = useInvalidateMetadata();
|
||||
|
||||
const queryReturn = useQuery({
|
||||
queryKey: [METADATA_QUERY_KEY],
|
||||
|
@ -12,3 +12,4 @@ export * from './apiLimits';
|
||||
export * from './graphqlSchemaIntrospection';
|
||||
export * from './permissions';
|
||||
export * from './metadata';
|
||||
export * from './openTelemetry';
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { InheritedRole } from './inheritedRoles';
|
||||
import { AllowList } from './allowList';
|
||||
import { QueryCollection } from './queryCollections';
|
||||
import { Source } from './source';
|
||||
import { BackendConfigs } from './backendConfigs';
|
||||
import { RemoteSchema } from './remoteSchemas';
|
||||
import { Action, CustomTypes } from './actions';
|
||||
import { CronTrigger } from './cronTriggers';
|
||||
import { Network } from './network';
|
||||
import { RestEndpoint } from './restEndpoints';
|
||||
import { ApiLimits } from './apiLimits';
|
||||
import { GraphQLSchemaIntrospection } from './graphqlSchemaIntrospection';
|
||||
import type { InheritedRole } from './inheritedRoles';
|
||||
import type { AllowList } from './allowList';
|
||||
import type { QueryCollection } from './queryCollections';
|
||||
import type { Source } from './source';
|
||||
import type { BackendConfigs } from './backendConfigs';
|
||||
import type { RemoteSchema } from './remoteSchemas';
|
||||
import type { Action, CustomTypes } from './actions';
|
||||
import type { CronTrigger } from './cronTriggers';
|
||||
import type { Network } from './network';
|
||||
import type { RestEndpoint } from './restEndpoints';
|
||||
import type { ApiLimits } from './apiLimits';
|
||||
import type { GraphQLSchemaIntrospection } from './graphqlSchemaIntrospection';
|
||||
import type { OpenTelemetry } from './openTelemetry';
|
||||
|
||||
export type Metadata = {
|
||||
resource_version: number;
|
||||
@ -28,5 +29,13 @@ export type Metadata = {
|
||||
rest_endpoints?: RestEndpoint[];
|
||||
api_limits?: ApiLimits;
|
||||
graphql_schema_introspection?: GraphQLSchemaIntrospection;
|
||||
|
||||
/**
|
||||
* The EE Lite OpenTelemetry settings.
|
||||
*
|
||||
* ATTENTION: Both Lux and the EE Lite server allow configuring OpenTelemetry. Anyway, this only
|
||||
* represents the EE Lite one since Lux stores the OpenTelemetry settings by itself.
|
||||
*/
|
||||
opentelemetry?: OpenTelemetry;
|
||||
};
|
||||
};
|
||||
|
@ -0,0 +1,3 @@
|
||||
export * from './types';
|
||||
export * from './queries';
|
||||
export * from './parsers';
|
@ -0,0 +1,16 @@
|
||||
import { openTelemetrySchema } from '.';
|
||||
|
||||
/**
|
||||
* Allow to parse the data the server returns and early catch possible console/server misalignments.
|
||||
*
|
||||
* Please note that this is not a strict check (https://github.com/colinhacks/zod#strict) since
|
||||
* strict on discriminated unions is not supported by Zod. Hence if the server adds more properties
|
||||
* to OpenTelemetry and the Console is not updated, chances are the Console strips out the extra
|
||||
* properties.
|
||||
*
|
||||
* More: this function does not throw in case of incompatible types. It's up to the consumer to
|
||||
* decide what to do in case of mismatch.
|
||||
*/
|
||||
export function parseOpenTelemetry(object: unknown) {
|
||||
return openTelemetrySchema.safeParse(object);
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import type { OpenTelemetry } from './types';
|
||||
|
||||
export type OpenTelemetryQueries = SetOpenTelemetryConfigQuery['type'];
|
||||
|
||||
// --------------------------------------------------
|
||||
// SET OPENTELEMETRY CONFIG
|
||||
// --------------------------------------------------
|
||||
|
||||
/**
|
||||
* Allow to set/update the OpenTelemetry configuration.
|
||||
*/
|
||||
export type SetOpenTelemetryConfigQuery = {
|
||||
type: 'set_opentelemetry_config';
|
||||
|
||||
args: OpenTelemetry;
|
||||
|
||||
successResponse: { message: 'success' };
|
||||
|
||||
errorResponse: SetOpenTelemetryConfigQueryErrors;
|
||||
};
|
||||
|
||||
// ERRORS
|
||||
// Please note that this is a non-comprehensive list of errors. It does not include
|
||||
// - all the server-prevented errors also prevented by the Console: ex. 'Invalid URL' and
|
||||
// 'max_export_batch_size must be a positive integer'
|
||||
// - passing a wrong OpenTelemetry config, prevented by the Console
|
||||
// that would result in something like {"code":"unexpected","error":"cannot continue due to new inconsistent metadata","internal":[{"definition":{"headers":[],"protocol":"http/protobuf","resource_attributes":[]},"name":"open_telemetry exporter_otlp","reason":"Inconsistent object: Missing traces endpoint","type":"open_telemetry"}],"path":"$.args"}%
|
||||
// - all the internal server errors
|
||||
// ATTENTION: the `unknown` type is useful to force the consumer to manage every kind of possible
|
||||
// error object. Unfortunately, we do not have good visibility over the server errors yet, and
|
||||
// treating them as a black box (apart from some particular cases) is the only thing we can do.
|
||||
type SetOpenTelemetryConfigQueryErrors = ErrorsNotManagedByTheConsole | unknown;
|
||||
|
||||
type ErrorsNotManagedByTheConsole = {
|
||||
httpStatus: 400;
|
||||
} & {
|
||||
code: 'parse-failed';
|
||||
error: `Environment variable not found: "${string}"`;
|
||||
path: '$.args';
|
||||
};
|
@ -0,0 +1,119 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
// --------------------------------------------------
|
||||
// UTILS
|
||||
// --------------------------------------------------
|
||||
const validUrlSchema = z.string().url({ message: 'Invalid URL' });
|
||||
|
||||
// --------------------------------------------------
|
||||
// ATTRIBUTES
|
||||
// --------------------------------------------------
|
||||
const attributeSchema = z.object({
|
||||
name: z.string(),
|
||||
|
||||
// ATM, only string values are accepted out of the wide variety of values OpenTelemetry accepts
|
||||
// see: https://opentelemetry.io/docs/reference/specification/common/#attribute
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
// --------------------------------------------------
|
||||
// HEADERS
|
||||
// --------------------------------------------------
|
||||
const userDefinedHeaderSchema = z.object({
|
||||
// Names should be validated against /^[a-zA-Z0-9]*$/ but we must be sure the server performs the
|
||||
// same check.
|
||||
// see: the CloudFlare docs as an example https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/#:~:text=The%20value%20of%20the%20HTTP,%24%26%60%7C~%5E%25
|
||||
name: z.string(),
|
||||
// Values should be validated against /^[a-zA-Z0-9_ :;.,\\\/"'\?\!\(\)\{\}\[\]@<>=\-+\*#$&`|~^%]*$/
|
||||
// but we must be sure the server performs the same check.
|
||||
// see: the CloudFlare docs as an example https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/#:~:text=The%20value%20of%20the%20HTTP,%24%26%60%7C~%5E%25
|
||||
value: z.string(),
|
||||
});
|
||||
const envVarHeaderSchema = z.object({
|
||||
// Names should be validated against /^[a-zA-Z0-9]*$/ but we must be sure the server performs the
|
||||
// same check.
|
||||
// see: the CloudFlare docs as an example https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/#:~:text=The%20value%20of%20the%20HTTP,%24%26%60%7C~%5E%25
|
||||
name: z.string(),
|
||||
// FYI: The env vars do exist in the server! The server prevents setting unexisting env vars
|
||||
// Values should be validated against /^[a-zA-Z0-9_ :;.,\\\/"'\?\!\(\)\{\}\[\]@<>=\-+\*#$&`|~^%]*$/
|
||||
// but we must be sure the server performs the same check.
|
||||
// see: the CloudFlare docs as an example https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/#:~:text=The%20value%20of%20the%20HTTP,%24%26%60%7C~%5E%25
|
||||
value_from_env: z.string(),
|
||||
});
|
||||
|
||||
const headersSchema = z.array(
|
||||
z.union([userDefinedHeaderSchema, envVarHeaderSchema])
|
||||
);
|
||||
|
||||
// --------------------------------------------------
|
||||
// OTHER PROPERTIES
|
||||
// --------------------------------------------------
|
||||
|
||||
// In the future, also 'metrics' | 'logs' will be available
|
||||
const dataTypesSchema = z.literal('traces');
|
||||
|
||||
// In the future, also 'grpc' will be available
|
||||
const protocolSchema = z.literal('http/protobuf');
|
||||
|
||||
// --------------------------------------------------
|
||||
// EXPORTER
|
||||
// --------------------------------------------------
|
||||
|
||||
const exporterSchema = z.object({
|
||||
headers: headersSchema,
|
||||
protocol: protocolSchema,
|
||||
resource_attributes: z.array(attributeSchema),
|
||||
|
||||
/**
|
||||
* The most important part of the configuration. You cannot enable OpenTelemetry without a valid
|
||||
* endpoint.
|
||||
*/
|
||||
otlp_traces_endpoint: validUrlSchema,
|
||||
});
|
||||
|
||||
// --------------------------------------------------
|
||||
// ENABLED/DISABLED DIFFERENCES
|
||||
// --------------------------------------------------
|
||||
|
||||
const enabledOpenTelemetrySchema = {
|
||||
/**
|
||||
* If OpenTelemetry is enabled or not. Allows to enable/disable the feature without losing the
|
||||
* configuration and/or inferring the status from other data (initially, data_types meant that
|
||||
* OpenTelemetry is disabled if it is empty but this is not true anymore because of the bad UX
|
||||
* consequences).
|
||||
*/
|
||||
status: z.literal('enabled'),
|
||||
|
||||
/**
|
||||
* The request headers sent to the OpenTelemetry endpoint.
|
||||
*/
|
||||
data_types: z.array(dataTypesSchema),
|
||||
|
||||
batch_span_processor: z.object({
|
||||
// a value between 1 and 512
|
||||
max_export_batch_size: z.number().min(1).max(512),
|
||||
}),
|
||||
|
||||
exporter_otlp: exporterSchema,
|
||||
};
|
||||
|
||||
const disabledOpenTelemetrySchema = {
|
||||
...enabledOpenTelemetrySchema,
|
||||
|
||||
status: z.literal('disabled'),
|
||||
|
||||
exporter_otlp: exporterSchema.partial({
|
||||
// If OpenTelemetry is disabled, the endpoint is not required
|
||||
otlp_traces_endpoint: true,
|
||||
}),
|
||||
};
|
||||
|
||||
// --------------------------------------------------
|
||||
// OPEN TELEMETRY
|
||||
// --------------------------------------------------
|
||||
export const openTelemetrySchema = z.discriminatedUnion('status', [
|
||||
z.object(disabledOpenTelemetrySchema),
|
||||
z.object(enabledOpenTelemetrySchema),
|
||||
]);
|
||||
|
||||
export type OpenTelemetry = z.infer<typeof openTelemetrySchema>;
|
@ -1,8 +1,11 @@
|
||||
import { Moment } from 'moment';
|
||||
import { KeyValuePair } from '@/components/Common/ConfigureTransformation/stateDefaults';
|
||||
import { Nullable } from './../components/Common/utils/tsUtils';
|
||||
import { Driver } from '../dataSources';
|
||||
import { PermissionsType } from '../components/Services/RemoteSchema/Permissions/types';
|
||||
import type { Moment } from 'moment';
|
||||
|
||||
import type { OpenTelemetry } from '@/features/hasura-metadata-types';
|
||||
import type { KeyValuePair } from '@/components/Common/ConfigureTransformation/stateDefaults';
|
||||
|
||||
import type { Driver } from '../dataSources';
|
||||
import type { Nullable } from './../components/Common/utils/tsUtils';
|
||||
import type { PermissionsType } from '../components/Services/RemoteSchema/Permissions/types';
|
||||
|
||||
export type DataSource = {
|
||||
name: string;
|
||||
@ -1232,6 +1235,14 @@ export interface HasuraMetadataV3 {
|
||||
graphql_schema_introspection?: {
|
||||
disabled_for_roles: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The EE Lite OpenTelemetry settings.
|
||||
*
|
||||
* ATTENTION: Both Lux and the EE Lite server allow configuring OpenTelemetry. Anyway, this only
|
||||
* represents the EE Lite one since Lux stores the OpenTelemetry settings by itself.
|
||||
*/
|
||||
opentelemetry?: OpenTelemetry;
|
||||
}
|
||||
|
||||
// Inconsistent Objects
|
||||
|
@ -1,9 +1,10 @@
|
||||
import type { Metadata } from '@/features/hasura-metadata-types';
|
||||
import { allowedMetadataTypes } from '@/features/MetadataAPI';
|
||||
import { Metadata } from '@/features/hasura-metadata-types';
|
||||
|
||||
import { metadataHandlers as allowListMetadataHandlers } from '@/features/AllowLists';
|
||||
import { metadataHandlers as queryCollectionMetadataHandlers } from '@/features/QueryCollections';
|
||||
import { metadataHandlers as adhocEventMetadataHandlers } from '@/features/AdhocEvents';
|
||||
import { metadataHandlers as queryCollectionMetadataHandlers } from '@/features/QueryCollections';
|
||||
import { metadataHandlers as openTelemetryMetadataHandlers } from '@/features/OpenTelemetryConfig';
|
||||
|
||||
import { TMigration } from '../features/MetadataAPI/hooks/useMetadataMigration';
|
||||
|
||||
@ -30,6 +31,7 @@ const metadataHandlers: Partial<Record<allowedMetadataTypes, MetadataReducer>> =
|
||||
...allowListMetadataHandlers,
|
||||
...queryCollectionMetadataHandlers,
|
||||
...adhocEventMetadataHandlers,
|
||||
...openTelemetryMetadataHandlers,
|
||||
};
|
||||
|
||||
export const metadataReducer: MetadataReducer = (state, action) => {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import type { ServerConfig } from '@/hooks';
|
||||
import type { TMigration } from '@/features/MetadataAPI';
|
||||
import type { Metadata } from '@/features/hasura-metadata-types';
|
||||
|
||||
import { allowListInitialData } from '@/features/AllowLists';
|
||||
import { queryCollectionInitialData } from '@/features/QueryCollections';
|
||||
import { TMigration } from '@/features/MetadataAPI';
|
||||
import { Metadata } from '@/features/hasura-metadata-types';
|
||||
import { openTelemetryConfigInitialData } from '@/features/OpenTelemetryConfig';
|
||||
|
||||
import { ServerConfig } from '@/hooks';
|
||||
import { rest } from 'msw';
|
||||
import { metadataReducer } from './actions';
|
||||
|
||||
@ -15,6 +17,7 @@ export const createDefaultInitialData = (): Metadata => ({
|
||||
inherited_roles: [],
|
||||
...allowListInitialData,
|
||||
...queryCollectionInitialData,
|
||||
...openTelemetryConfigInitialData,
|
||||
},
|
||||
});
|
||||
|
||||
@ -31,9 +34,9 @@ type HandlersOptions = {
|
||||
|
||||
const defaultOptions: HandlersOptions = {
|
||||
delay: 0,
|
||||
initialData: createDefaultInitialData,
|
||||
config: defaultConfig,
|
||||
url: 'http://localhost:8080',
|
||||
initialData: createDefaultInitialData,
|
||||
};
|
||||
|
||||
export const handlers = (options?: HandlersOptions) => {
|
||||
@ -49,8 +52,9 @@ export const handlers = (options?: HandlersOptions) => {
|
||||
rest.get(`${url}/v1alpha1/config`, (req, res, ctx) => {
|
||||
return res(ctx.delay(delay), ctx.status(200), ctx.json(config));
|
||||
}),
|
||||
rest.post(`${url}/v1/metadata`, (req, res, ctx) => {
|
||||
const body = req.body as TMigration['query'];
|
||||
|
||||
rest.post(`${url}/v1/metadata`, async (req, res, ctx) => {
|
||||
const body = (await req.json()) as TMigration['query'];
|
||||
|
||||
const response = metadataReducer(metadata, body);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user