mirror of
https://github.com/inventree/InvenTree.git
synced 2026-02-12 17:27:02 +00:00
feat(frontend): Add better frontend tracing (#11244)
* add better tracing through the frontend * extend allowed headers * add endpoint to end the trace * end traces correctly in backend * end trace * early cop-out if not tracing is enabled * Update API version link for v447
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
header Allow GET,HEAD,OPTIONS
|
header Allow GET,HEAD,OPTIONS
|
||||||
header Access-Control-Allow-Origin *
|
header Access-Control-Allow-Origin *
|
||||||
header Access-Control-Allow-Methods GET,HEAD,OPTIONS
|
header Access-Control-Allow-Methods GET,HEAD,OPTIONS
|
||||||
header Access-Control-Allow-Headers Authorization,Content-Type,User-Agent
|
header Access-Control-Allow-Headers Authorization,Content-Type,User-Agent,traceparent
|
||||||
|
|
||||||
@cors_preflight{args[0]} method OPTIONS
|
@cors_preflight{args[0]} method OPTIONS
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 452
|
INVENTREE_API_VERSION = 453
|
||||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
v453 -> 2026-02-11 : https://github.com/inventree/InvenTree/pull/11244
|
||||||
|
- Adds (internal) endpoint to end a observability tooling session
|
||||||
|
|
||||||
v452 -> 2026-02-10 : https://github.com/inventree/InvenTree/pull/11276
|
v452 -> 2026-02-10 : https://github.com/inventree/InvenTree/pull/11276
|
||||||
- Adds "install_into_detail" field to the BuildItem API endpoint
|
- Adds "install_into_detail" field to the BuildItem API endpoint
|
||||||
|
|
||||||
|
|||||||
@@ -1289,10 +1289,12 @@ CORS_ALLOWED_ORIGIN_REGEXES = get_setting(
|
|||||||
typecast=list,
|
typecast=list,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_allowed_headers = (*default_cors_headers, 'traceparent')
|
||||||
# Allow extra CORS headers in DEBUG mode
|
# Allow extra CORS headers in DEBUG mode
|
||||||
# Required for serving /static/ and /media/ files
|
# Required for serving /static/ and /media/ files
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
CORS_ALLOW_HEADERS = (*default_cors_headers, 'cache-control', 'pragma', 'expires')
|
_allowed_headers = (*_allowed_headers, 'cache-control', 'pragma', 'expires')
|
||||||
|
CORS_ALLOW_HEADERS = _allowed_headers
|
||||||
|
|
||||||
# In debug mode allow CORS requests from localhost
|
# In debug mode allow CORS requests from localhost
|
||||||
# This allows connection from the frontend development server
|
# This allows connection from the frontend development server
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from django_q.tasks import async_task
|
|||||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from error_report.models import Error
|
from error_report.models import Error
|
||||||
|
from opentelemetry import trace
|
||||||
from pint._typing import UnitLike
|
from pint._typing import UnitLike
|
||||||
from rest_framework import generics, serializers
|
from rest_framework import generics, serializers
|
||||||
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
|
from rest_framework.exceptions import NotAcceptable, NotFound, PermissionDenied
|
||||||
@@ -1115,6 +1116,50 @@ class HealthCheckView(APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObservabilityEndSerializer(serializers.Serializer):
|
||||||
|
"""Serializer for observability end endpoint."""
|
||||||
|
|
||||||
|
traceid = serializers.CharField(
|
||||||
|
help_text='Trace ID to end', max_length=128, required=True
|
||||||
|
)
|
||||||
|
service = serializers.CharField(
|
||||||
|
help_text='Service name', max_length=128, required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObservabilityEnd(CreateAPI):
|
||||||
|
"""Endpoint for observability tools."""
|
||||||
|
|
||||||
|
permission_classes = [AllowAnyOrReadScope]
|
||||||
|
serializer_class = ObservabilityEndSerializer
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
"""End a trace in the observability system."""
|
||||||
|
if not settings.TRACING_ENABLED:
|
||||||
|
return Response({'status': 'ok'})
|
||||||
|
|
||||||
|
data = self.get_serializer(data=request.data)
|
||||||
|
data.is_valid(raise_exception=True)
|
||||||
|
|
||||||
|
traceid = data.validated_data['traceid']
|
||||||
|
# service = data.validated_data['service'] # This will become interesting with frontend observability
|
||||||
|
|
||||||
|
# End the foreign trend via the low level otel API
|
||||||
|
tracer = trace.get_tracer(__name__)
|
||||||
|
span_context = trace.SpanContext(
|
||||||
|
trace_id=int(traceid, 16),
|
||||||
|
span_id=0,
|
||||||
|
is_remote=True,
|
||||||
|
trace_flags=trace.TraceFlags(0x01),
|
||||||
|
trace_state=trace.TraceState(),
|
||||||
|
)
|
||||||
|
with tracer.start_span('Ending session') as span:
|
||||||
|
span.add_event('Ending external trace')
|
||||||
|
span.add_link(span_context)
|
||||||
|
|
||||||
|
return Response({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
selection_urls = [
|
selection_urls = [
|
||||||
path(
|
path(
|
||||||
'<int:pk>/',
|
'<int:pk>/',
|
||||||
@@ -1393,7 +1438,22 @@ common_api_urls = [
|
|||||||
# System APIs (related to basic system functions)
|
# System APIs (related to basic system functions)
|
||||||
path(
|
path(
|
||||||
'system/',
|
'system/',
|
||||||
include([path('health/', HealthCheckView.as_view(), name='api-system-health')]),
|
include([
|
||||||
|
# Health check
|
||||||
|
path('health/', HealthCheckView.as_view(), name='api-system-health')
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
# Internal System APIs - DO NOT USE
|
||||||
|
path(
|
||||||
|
'system-internal/',
|
||||||
|
include([
|
||||||
|
# Observability
|
||||||
|
path(
|
||||||
|
'observability/end',
|
||||||
|
ObservabilityEnd.as_view(),
|
||||||
|
name='api-system-observability',
|
||||||
|
)
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -244,5 +244,8 @@ export enum ApiEndpoints {
|
|||||||
email_test = 'admin/email/test/',
|
email_test = 'admin/email/test/',
|
||||||
config_list = 'admin/config/',
|
config_list = 'admin/config/',
|
||||||
parameter_list = 'parameter/',
|
parameter_list = 'parameter/',
|
||||||
parameter_template_list = 'parameter/template/'
|
parameter_template_list = 'parameter/template/',
|
||||||
|
|
||||||
|
// Internal system things
|
||||||
|
system_internal_trace_end = 'system-internal/observability/end'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { ApiEndpoints, apiUrl } from '@lib/index';
|
||||||
|
import { frontendID, serviceName } from './defaults/defaults';
|
||||||
import { useLocalState } from './states/LocalState';
|
import { useLocalState } from './states/LocalState';
|
||||||
|
|
||||||
// Global API instance
|
// Global API instance
|
||||||
@@ -33,3 +35,22 @@ export const queryClient = new QueryClient({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
export function setTraceId() {
|
||||||
|
const runID = crypto.randomUUID().replace(/-/g, '');
|
||||||
|
const traceid = `00-${runID}-${frontendID}-01`;
|
||||||
|
api.defaults.headers['traceparent'] = traceid;
|
||||||
|
|
||||||
|
return runID;
|
||||||
|
}
|
||||||
|
export function removeTraceId(traceid: string) {
|
||||||
|
delete api.defaults.headers['traceparent'];
|
||||||
|
|
||||||
|
api
|
||||||
|
.post(apiUrl(ApiEndpoints.system_internal_trace_end), {
|
||||||
|
traceid: traceid,
|
||||||
|
service: serviceName
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error removing trace ID:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,3 +36,6 @@ export const SizeMarks: SiteMarkProps[] = [
|
|||||||
{ value: 75, label: 'lg' },
|
{ value: 75, label: 'lg' },
|
||||||
{ value: 100, label: 'xl' }
|
{ value: 100, label: 'xl' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const frontendID = '706f6f7062757474';
|
||||||
|
export const serviceName = 'FRONTEND';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { setApiDefaults } from '../../App';
|
import { removeTraceId, setApiDefaults, setTraceId } from '../../App';
|
||||||
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
|
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
|
||||||
import { AuthenticationForm } from '../../components/forms/AuthenticationForm';
|
import { AuthenticationForm } from '../../components/forms/AuthenticationForm';
|
||||||
import { InstanceOptions } from '../../components/forms/InstanceOptions';
|
import { InstanceOptions } from '../../components/forms/InstanceOptions';
|
||||||
@@ -63,7 +63,9 @@ export default function Login() {
|
|||||||
if (newHost === null) return;
|
if (newHost === null) return;
|
||||||
setHost(hostList[newHost]?.host, newHost);
|
setHost(hostList[newHost]?.host, newHost);
|
||||||
setApiDefaults();
|
setApiDefaults();
|
||||||
|
const traceid = setTraceId();
|
||||||
fetchServerApiState();
|
fetchServerApiState();
|
||||||
|
removeTraceId(traceid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default host to localhost if no host is selected
|
// Set default host to localhost if no host is selected
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PluginProps } from '@lib/types/Plugins';
|
import type { PluginProps } from '@lib/types/Plugins';
|
||||||
import { setApiDefaults } from '../App';
|
import { removeTraceId, setApiDefaults, setTraceId } from '../App';
|
||||||
import { useGlobalStatusState } from './GlobalStatusState';
|
import { useGlobalStatusState } from './GlobalStatusState';
|
||||||
import { useIconState } from './IconState';
|
import { useIconState } from './IconState';
|
||||||
import { useServerApiState } from './ServerApiState';
|
import { useServerApiState } from './ServerApiState';
|
||||||
@@ -53,6 +53,7 @@ export async function fetchGlobalStates() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setApiDefaults();
|
setApiDefaults();
|
||||||
|
const traceId = setTraceId();
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
useServerApiState.getState().fetchServerApiState(),
|
useServerApiState.getState().fetchServerApiState(),
|
||||||
useUserSettingsState.getState().fetchSettings(),
|
useUserSettingsState.getState().fetchSettings(),
|
||||||
@@ -60,4 +61,5 @@ export async function fetchGlobalStates() {
|
|||||||
useGlobalStatusState.getState().fetchStatus(),
|
useGlobalStatusState.getState().fetchStatus(),
|
||||||
useIconState.getState().fetchIcons()
|
useIconState.getState().fetchIcons()
|
||||||
]);
|
]);
|
||||||
|
removeTraceId(traceId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user