From e4a76117b1e6a783e912522e1b356d6509f4b17e Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 18 Mar 2026 11:20:13 +0000 Subject: [PATCH] feat: Enhance telemetry services with Kubernetes cluster auto-discovery and health check configuration --- .../Components/Telemetry/Documentation.tsx | 159 ++++++++++++++++-- .../templates/configmap-daemonset.yaml | 8 +- .../templates/configmap-deployment.yaml | 15 +- HelmChart/Public/kubernetes-agent/values.yaml | 8 +- Telemetry/Services/OtelIngestBaseService.ts | 54 ++++++ Telemetry/Services/OtelLogsIngestService.ts | 20 ++- .../Services/OtelMetricsIngestService.ts | 20 ++- 7 files changed, 244 insertions(+), 40 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Components/Telemetry/Documentation.tsx b/App/FeatureSet/Dashboard/src/Components/Telemetry/Documentation.tsx index e692ff34b1..67e7bef5ca 100644 --- a/App/FeatureSet/Dashboard/src/Components/Telemetry/Documentation.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Telemetry/Documentation.tsx @@ -36,7 +36,9 @@ type Language = | "ruby" | "elixir" | "cpp" - | "swift"; + | "swift" + | "react" + | "angular"; interface LanguageOption { key: Language; @@ -56,6 +58,8 @@ const languages: Array = [ { key: "elixir", label: "Elixir", shortLabel: "Elixir" }, { key: "cpp", label: "C++", shortLabel: "C++" }, { key: "swift", label: "Swift", shortLabel: "Swift" }, + { key: "react", label: "React (Browser)", shortLabel: "React" }, + { key: "angular", label: "Angular (Browser)", shortLabel: "Angular" }, ]; type IntegrationMethod = "opentelemetry" | "fluentbit" | "fluentd"; @@ -99,7 +103,7 @@ function getOtelInstallSnippet(lang: Language): { return { code: `pip install opentelemetry-api \\ opentelemetry-sdk \\ - opentelemetry-exporter-otlp-proto-grpc \\ + opentelemetry-exporter-otlp-proto-http \\ opentelemetry-instrumentation`, language: "bash", }; @@ -107,9 +111,9 @@ function getOtelInstallSnippet(lang: Language): { return { code: `go get go.opentelemetry.io/otel \\ go.opentelemetry.io/otel/sdk \\ - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \\ - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc \\ - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`, + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \\ + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp \\ + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`, language: "bash", }; case "java": @@ -200,6 +204,30 @@ vcpkg install opentelemetry-cpp[otlp-grpc] .product(name: "OtlpGRPCSpanExporting", package: "opentelemetry-swift"),`, language: "bash", }; + case "react": + return { + code: `npm install @opentelemetry/api \\ + @opentelemetry/sdk-trace-web \\ + @opentelemetry/sdk-trace-base \\ + @opentelemetry/exporter-trace-otlp-http \\ + @opentelemetry/instrumentation-document-load \\ + @opentelemetry/instrumentation-fetch \\ + @opentelemetry/instrumentation-xml-http-request \\ + @opentelemetry/context-zone`, + language: "bash", + }; + case "angular": + return { + code: `npm install @opentelemetry/api \\ + @opentelemetry/sdk-trace-web \\ + @opentelemetry/sdk-trace-base \\ + @opentelemetry/exporter-trace-otlp-http \\ + @opentelemetry/instrumentation-document-load \\ + @opentelemetry/instrumentation-fetch \\ + @opentelemetry/instrumentation-xml-http-request \\ + @opentelemetry/context-zone`, + language: "bash", + }; } } @@ -253,8 +281,8 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter -from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter from opentelemetry.sdk.resources import Resource resource = Resource.create({"service.name": "my-service"}) @@ -288,23 +316,20 @@ metrics.set_meter_provider(MeterProvider(resource=resource, metric_readers=[metr import ( "context" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" ) func initTracer() (*sdktrace.TracerProvider, error) { - ctx := metadata.AppendToOutgoingContext( - context.Background(), - "x-oneuptime-token", "", - ) + ctx := context.Background() - exporter, err := otlptracegrpc.New(ctx, - otlptracegrpc.WithEndpoint(""), - otlptracegrpc.WithTLSCredentials(insecure.NewCredentials()), + exporter, err := otlptracehttp.New(ctx, + otlptracehttp.WithEndpoint(""), + otlptracehttp.WithHeaders(map[string]string{ + "x-oneuptime-token": "", + }), ) if err != nil { return nil, err @@ -551,6 +576,106 @@ func initTracer() { }`, language: "swift", }; + case "react": + return { + code: `// src/tracing.ts - Import this file in your index.tsx before ReactDOM.render() +import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { ZoneContextManager } from '@opentelemetry/context-zone'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; +import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; +import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; +import { Resource } from '@opentelemetry/resources'; + +const provider = new WebTracerProvider({ + resource: new Resource({ + 'service.name': 'my-react-app', + }), +}); + +provider.addSpanProcessor( + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: '/v1/traces', + headers: { 'x-oneuptime-token': '' }, + }) + ) +); + +provider.register({ + contextManager: new ZoneContextManager(), +}); + +registerInstrumentations({ + instrumentations: [ + new DocumentLoadInstrumentation(), + new FetchInstrumentation({ + propagateTraceHeaderCorsUrls: [/.*/], + }), + new XMLHttpRequestInstrumentation({ + propagateTraceHeaderCorsUrls: [/.*/], + }), + ], +}); + +// In index.tsx: +// import './tracing'; // Must be first import +// import React from 'react'; +// ...`, + language: "typescript", + }; + case "angular": + return { + code: `// src/tracing.ts +import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { ZoneContextManager } from '@opentelemetry/context-zone'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load'; +import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'; +import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'; +import { Resource } from '@opentelemetry/resources'; + +const provider = new WebTracerProvider({ + resource: new Resource({ + 'service.name': 'my-angular-app', + }), +}); + +provider.addSpanProcessor( + new BatchSpanProcessor( + new OTLPTraceExporter({ + url: '/v1/traces', + headers: { 'x-oneuptime-token': '' }, + }) + ) +); + +provider.register({ + contextManager: new ZoneContextManager(), +}); + +registerInstrumentations({ + instrumentations: [ + new DocumentLoadInstrumentation(), + new FetchInstrumentation({ + propagateTraceHeaderCorsUrls: [/.*/], + }), + new XMLHttpRequestInstrumentation({ + propagateTraceHeaderCorsUrls: [/.*/], + }), + ], +}); + +// In main.ts, import before bootstrapping: +// import './tracing'; // Must be first import +// import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +// ...`, + language: "typescript", + }; } } diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml index ad5630eb6e..d00626b001 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-daemonset.yaml @@ -8,6 +8,10 @@ metadata: {{- include "kubernetes-agent.labels" . | nindent 4 }} data: otel-collector-config.yaml: | + extensions: + health_check: + endpoint: "0.0.0.0:13133" + receivers: # Collect pod logs from /var/log/pods filelog: @@ -115,11 +119,13 @@ data: exporters: otlphttp: - endpoint: "{{ .Values.oneuptime.url }}" + endpoint: "{{ .Values.oneuptime.url }}/otlp" headers: x-oneuptime-token: "${env:ONEUPTIME_API_KEY}" service: + extensions: + - health_check pipelines: logs: receivers: diff --git a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml index a6811db67c..a28e092cb4 100644 --- a/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml +++ b/HelmChart/Public/kubernetes-agent/templates/configmap-deployment.yaml @@ -7,6 +7,10 @@ metadata: {{- include "kubernetes-agent.labels" . | nindent 4 }} data: otel-collector-config.yaml: | + extensions: + health_check: + endpoint: "0.0.0.0:13133" + receivers: # Collect node, pod, and container resource metrics from kubelet kubeletstats: @@ -133,22 +137,25 @@ data: # Batch telemetry for efficient export batch: - send_batch_size: 1024 + send_batch_size: 200 + send_batch_max_size: 500 timeout: 10s # Limit memory usage memory_limiter: check_interval: 5s - limit_mib: 400 - spike_limit_mib: 100 + limit_mib: 1500 + spike_limit_mib: 300 exporters: otlphttp: - endpoint: "{{ .Values.oneuptime.url }}" + endpoint: "{{ .Values.oneuptime.url }}/otlp" headers: x-oneuptime-token: "${env:ONEUPTIME_API_KEY}" service: + extensions: + - health_check pipelines: metrics: receivers: diff --git a/HelmChart/Public/kubernetes-agent/values.yaml b/HelmChart/Public/kubernetes-agent/values.yaml index aaeed034de..1dff9d71b2 100644 --- a/HelmChart/Public/kubernetes-agent/values.yaml +++ b/HelmChart/Public/kubernetes-agent/values.yaml @@ -29,11 +29,11 @@ deployment: replicas: 1 resources: requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 500m + cpu: 200m memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi # Control plane monitoring (etcd, API server, scheduler, controller manager) # Disabled by default — enable for self-managed clusters. diff --git a/Telemetry/Services/OtelIngestBaseService.ts b/Telemetry/Services/OtelIngestBaseService.ts index d09286545d..617fb824f2 100644 --- a/Telemetry/Services/OtelIngestBaseService.ts +++ b/Telemetry/Services/OtelIngestBaseService.ts @@ -1,6 +1,9 @@ import { ExpressRequest } from "Common/Server/Utils/Express"; import CaptureSpan from "Common/Server/Utils/Telemetry/CaptureSpan"; import { JSONArray, JSONObject } from "Common/Types/JSON"; +import ObjectID from "Common/Types/ObjectID"; +import KubernetesClusterService from "Common/Server/Services/KubernetesClusterService"; +import logger from "Common/Server/Utils/Logger"; export default abstract class OtelIngestBaseService { @CaptureSpan() @@ -33,6 +36,57 @@ export default abstract class OtelIngestBaseService { return "Unknown Service"; } + @CaptureSpan() + protected static getClusterNameFromAttributes( + attributes: JSONArray, + ): string | null { + for (const attribute of attributes) { + if ( + attribute["key"] === "k8s.cluster.name" && + attribute["value"] && + (attribute["value"] as JSONObject)["stringValue"] + ) { + const value = (attribute["value"] as JSONObject)["stringValue"]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + } + return null; + } + + @CaptureSpan() + protected static async autoDiscoverKubernetesCluster(data: { + projectId: ObjectID; + attributes: JSONArray; + }): Promise { + try { + const clusterName: string | null = this.getClusterNameFromAttributes( + data.attributes, + ); + + if (!clusterName) { + return; + } + + const cluster = + await KubernetesClusterService.findOrCreateByClusterIdentifier({ + projectId: data.projectId, + clusterIdentifier: clusterName, + }); + + if (cluster._id) { + await KubernetesClusterService.updateLastSeen( + new ObjectID(cluster._id.toString()), + ); + } + } catch (err) { + logger.error( + "Error auto-discovering Kubernetes cluster: " + (err as Error).message, + ); + } + } + @CaptureSpan() protected static getServiceNameFromHeaders( req: ExpressRequest, diff --git a/Telemetry/Services/OtelLogsIngestService.ts b/Telemetry/Services/OtelLogsIngestService.ts index fc2362826c..8022e7f140 100644 --- a/Telemetry/Services/OtelLogsIngestService.ts +++ b/Telemetry/Services/OtelLogsIngestService.ts @@ -120,13 +120,22 @@ export default class OtelLogsIngestService extends OtelIngestBaseService { await Promise.resolve(); } resourceLogCounter++; - const serviceName: string = this.getServiceNameFromAttributes( - req, + const resourceAttributes_raw: JSONArray = ((resourceLog["resource"] as JSONObject)?.[ "attributes" - ] as JSONArray) || [], + ] as JSONArray) || []; + + const serviceName: string = this.getServiceNameFromAttributes( + req, + resourceAttributes_raw, ); + // Auto-discover Kubernetes cluster from resource attributes + await this.autoDiscoverKubernetesCluster({ + projectId, + attributes: resourceAttributes_raw, + }); + if (!serviceDictionary[serviceName]) { const service: { serviceId: ObjectID; @@ -151,10 +160,7 @@ export default class OtelLogsIngestService extends OtelIngestBaseService { serviceName: serviceName, }), ...TelemetryUtil.getAttributes({ - items: - ((resourceLog["resource"] as JSONObject)?.[ - "attributes" - ] as JSONArray) || [], + items: resourceAttributes_raw, prefixKeysWithString: "resource", }), }; diff --git a/Telemetry/Services/OtelMetricsIngestService.ts b/Telemetry/Services/OtelMetricsIngestService.ts index 91a0717aca..2bb0f9bec3 100644 --- a/Telemetry/Services/OtelMetricsIngestService.ts +++ b/Telemetry/Services/OtelMetricsIngestService.ts @@ -118,13 +118,22 @@ export default class OtelMetricsIngestService extends OtelIngestBaseService { await Promise.resolve(); } resourceMetricCounter++; - const serviceName: string = this.getServiceNameFromAttributes( - req, + const resourceAttributes_raw: JSONArray = ((resourceMetric["resource"] as JSONObject)?.[ "attributes" - ] as JSONArray) || [], + ] as JSONArray) || []; + + const serviceName: string = this.getServiceNameFromAttributes( + req, + resourceAttributes_raw, ); + // Auto-discover Kubernetes cluster from resource attributes + await this.autoDiscoverKubernetesCluster({ + projectId, + attributes: resourceAttributes_raw, + }); + if (!serviceDictionary[serviceName]) { const service: { serviceId: ObjectID; @@ -152,10 +161,7 @@ export default class OtelMetricsIngestService extends OtelIngestBaseService { serviceName: serviceName, }), ...TelemetryUtil.getAttributes({ - items: - ((resourceMetric["resource"] as JSONObject)?.[ - "attributes" - ] as JSONArray) || [], + items: resourceAttributes_raw, prefixKeysWithString: "resource", }), };