feat: Integrate OpenTelemetry for distributed tracing in both frontend and backend applications.

This commit is contained in:
Zuev
2026-03-19 03:55:22 +03:00
parent f519650bbb
commit 8ced8ae669
5 changed files with 67 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ COPY pom.xml .
RUN mvn dependency:go-offline -B RUN mvn dependency:go-offline -B
COPY src ./src COPY src ./src
RUN mvn package -DskipTests -B RUN mvn package -DskipTests -B
RUN curl -L -o opentelemetry-javaagent.jar https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
FROM eclipse-temurin:17-jre-alpine FROM eclipse-temurin:17-jre-alpine
@@ -13,5 +14,6 @@ USER spring:spring
WORKDIR /app WORKDIR /app
COPY --from=build /app/target/app.jar app.jar COPY --from=build /app/target/app.jar app.jar
COPY --from=build /app/opentelemetry-javaagent.jar opentelemetry-javaagent.jar
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]

View File

@@ -56,6 +56,13 @@
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- OpenTelemetry API for custom span attributes -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<version>1.49.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -9,6 +9,8 @@ import org.springframework.web.servlet.HandlerInterceptor;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import org.slf4j.MDC;
import io.opentelemetry.api.trace.Span;
/** /**
* Interceptor: извлекает поддомен из Host header и кладёт в TenantContext. * Interceptor: извлекает поддомен из Host header и кладёт в TenantContext.
@@ -48,6 +50,8 @@ public class TenantInterceptor implements HandlerInterceptor {
// (нужно чтобы админ мог добавить тенант даже если его домен не настроен) // (нужно чтобы админ мог добавить тенант даже если его домен не настроен)
if (path.startsWith("/api/database")) { if (path.startsWith("/api/database")) {
TenantContext.setCurrentTenant(tenant); TenantContext.setCurrentTenant(tenant);
MDC.put("tenant.id", tenant);
Span.current().setAttribute("tenant.id", tenant);
log.debug("Database API request, tenant '{}' (no strict check)", tenant); log.debug("Database API request, tenant '{}' (no strict check)", tenant);
return true; return true;
} }
@@ -66,6 +70,8 @@ public class TenantInterceptor implements HandlerInterceptor {
} }
TenantContext.setCurrentTenant(tenant); TenantContext.setCurrentTenant(tenant);
MDC.put("tenant.id", tenant);
Span.current().setAttribute("tenant.id", tenant);
log.debug("Resolved tenant '{}' from Host '{}'", tenant, host); log.debug("Resolved tenant '{}' from Host '{}'", tenant, host);
return true; return true;
} }
@@ -73,6 +79,7 @@ public class TenantInterceptor implements HandlerInterceptor {
@Override @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear(); TenantContext.clear();
MDC.remove("tenant.id");
} }
private String resolveTenant(String host) { private String resolveTenant(String host) {

View File

@@ -127,8 +127,7 @@ COMMENT ON TABLE equipments IS 'Оборудование';
COMMENT ON TABLE classrooms IS 'Аудитории'; COMMENT ON TABLE classrooms IS 'Аудитории';
COMMENT ON TABLE classroom_equipments IS 'Привязка оборудования к аудиториям'; COMMENT ON TABLE classroom_equipments IS 'Привязка оборудования к аудиториям';
COMMENT ON TABLE teacher_subjects IS 'Привязка преподавателей к дисциплинам'; COMMENT ON TABLE teacher_subjects IS 'Привязка преподавателей к дисциплинам';
COMMENT ON TABLE equipments IS 'Оборудование'; COMMENT ON TABLE teacher_lesson_types IS 'Типы занятий преподавателя';
COMMENT ON TABLE equipments IS 'Оборудование';
COMMENT ON COLUMN users.id IS 'ID пользователя'; COMMENT ON COLUMN users.id IS 'ID пользователя';
@@ -200,3 +199,25 @@ COMMENT ON COLUMN lessons.classroom_id IS 'ID аудитории, в котор
COMMENT ON COLUMN lessons.day IS 'День недели, в который проходит урок'; COMMENT ON COLUMN lessons.day IS 'День недели, в который проходит урок';
COMMENT ON COLUMN lessons.week IS 'Номер недели, в которой проходит урок'; COMMENT ON COLUMN lessons.week IS 'Номер недели, в которой проходит урок';
COMMENT ON COLUMN lessons.time IS 'Время урока'; COMMENT ON COLUMN lessons.time IS 'Время урока';
COMMENT ON COLUMN departments.id IS 'ID кафедры';
COMMENT ON COLUMN departments.name IS 'Название кафедры';
COMMENT ON COLUMN departments.code IS 'Код кафедры';
COMMENT ON COLUMN specialties.id IS 'ID специальности';
COMMENT ON COLUMN specialties.name IS 'Название специальности';
COMMENT ON COLUMN specialties.specialty_code IS 'Код специальности';
COMMENT ON COLUMN teacher_lesson_types.user_id IS 'ID преподавателя';
COMMENT ON COLUMN teacher_lesson_types.subject_id IS 'ID предмета';
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';
COMMENT ON COLUMN schedule_data.id IS 'ID записи данных расписания';
COMMENT ON COLUMN subjects.department_id IS 'ID кафедры';
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
COMMENT ON COLUMN users.full_name IS 'ФИО пользователя';
COMMENT ON COLUMN users.job_title IS 'Должность пользователя';
COMMENT ON COLUMN users.department_id IS 'ID кафедры';

View File

@@ -1,6 +1,32 @@
(() => { (() => {
'use strict'; 'use strict';
// --- OpenTelemetry Frontend Instrumentation ---
// Загружаем OTel Web SDK динамически через esm.sh, чтобы не ломать старый Vanilla JS (без type="module")
import('https://esm.sh/@opentelemetry/sdk-trace-web').then(async ({ WebTracerProvider, BatchSpanProcessor }) => {
const { OTLPTraceExporter } = await import('https://esm.sh/@opentelemetry/exporter-trace-otlp-http');
const { getWebAutoInstrumentations } = await import('https://esm.sh/@opentelemetry/auto-instrumentations-web');
const { registerInstrumentations } = await import('https://esm.sh/@opentelemetry/instrumentation');
const { Resource } = await import('https://esm.sh/@opentelemetry/resources');
const exporter = new OTLPTraceExporter({
url: window.location.origin + '/otel/v1/traces' // Трафик пойдет через ваш Caddy Proxy
});
const provider = new WebTracerProvider({
resource: new Resource({ 'service.name': 'magistr-frontend' }),
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
registerInstrumentations({
instrumentations: [getWebAutoInstrumentations()]
});
console.log("SigNoz (OpenTelemetry) инициализирован во фронтенде.");
}).catch(e => console.error("Ошибка загрузки OTel:", e));
// ----------------------------------------------
const form = document.getElementById('login-form'); const form = document.getElementById('login-form');
const usernameInput = document.getElementById('username'); const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password'); const passwordInput = document.getElementById('password');