diff --git a/packages/openchoreo-client-node/openapi/openchoreo-observability-api.yaml b/packages/openchoreo-client-node/openapi/openchoreo-observability-api.yaml index 4a35b2fb..cb333ee8 100644 --- a/packages/openchoreo-client-node/openapi/openchoreo-observability-api.yaml +++ b/packages/openchoreo-client-node/openapi/openchoreo-observability-api.yaml @@ -244,6 +244,39 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' + /api/metrics/component/http: + post: + tags: + - Metrics + summary: Get component HTTP metrics + description: Retrieve HTTP request metrics (request counts, latency percentiles) for a component as time series data + operationId: getComponentHTTPMetrics + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MetricsRequest' + responses: + '200': + description: Successfully retrieved HTTP metrics + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPMetricsTimeSeries' + '400': + description: Bad request - invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/metrics/component/usage: post: tags: @@ -476,6 +509,7 @@ components: MetricsRequest: type: object required: + - componentId - environmentId - projectId properties: @@ -623,6 +657,81 @@ components: - time: '2025-01-10T12:05:00Z' value: 2147483648 + HTTPMetricsTimeSeries: + type: object + properties: + requestCount: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: Total HTTP request count time series (requests per second) + successfulRequestCount: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: Successful HTTP request count time series (status 200, requests per second) + unsuccessfulRequestCount: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: Unsuccessful HTTP request count time series (status != 200, requests per second) + meanLatency: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: Mean HTTP request latency time series (in seconds) + latencyPercentile50th: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: 50th percentile (median) HTTP request latency time series (in seconds) + latencyPercentile90th: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: 90th percentile HTTP request latency time series (in seconds) + latencyPercentile99th: + type: array + items: + $ref: '#/components/schemas/TimeValuePoint' + description: 99th percentile HTTP request latency time series (in seconds) + example: + requestCount: + - time: '2025-01-10T12:00:00Z' + value: 125.5 + - time: '2025-01-10T12:05:00Z' + value: 143.2 + successfulRequestCount: + - time: '2025-01-10T12:00:00Z' + value: 120.0 + - time: '2025-01-10T12:05:00Z' + value: 138.5 + unsuccessfulRequestCount: + - time: '2025-01-10T12:00:00Z' + value: 5.5 + - time: '2025-01-10T12:05:00Z' + value: 4.7 + meanLatency: + - time: '2025-01-10T12:00:00Z' + value: 0.125 + - time: '2025-01-10T12:05:00Z' + value: 0.132 + latencyPercentile50th: + - time: '2025-01-10T12:00:00Z' + value: 0.095 + - time: '2025-01-10T12:05:00Z' + value: 0.102 + latencyPercentile90th: + - time: '2025-01-10T12:00:00Z' + value: 0.250 + - time: '2025-01-10T12:05:00Z' + value: 0.265 + latencyPercentile99th: + - time: '2025-01-10T12:00:00Z' + value: 0.500 + - time: '2025-01-10T12:05:00Z' + value: 0.520 + ErrorResponse: type: object required: diff --git a/packages/openchoreo-client-node/src/generated/observability/types.ts b/packages/openchoreo-client-node/src/generated/observability/types.ts index b6d3a130..d062f78a 100644 --- a/packages/openchoreo-client-node/src/generated/observability/types.ts +++ b/packages/openchoreo-client-node/src/generated/observability/types.ts @@ -124,6 +124,26 @@ export interface paths { patch?: never; trace?: never; }; + '/api/metrics/component/http': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Get component HTTP metrics + * @description Retrieve HTTP request metrics (request counts, latency percentiles) for a component as time series data + */ + post: operations['getComponentHTTPMetrics']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; '/api/metrics/component/usage': { parameters: { query?: never; @@ -350,7 +370,7 @@ export interface components { * @description Component identifier * @example comp-123 */ - componentId?: string; + componentId: string; /** * @description Environment identifier * @example env-dev @@ -507,6 +527,96 @@ export interface components { /** @description Memory limits time series (in bytes) */ memoryLimits?: components['schemas']['TimeValuePoint'][]; }; + /** + * @example { + * "requestCount": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 125.5 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 143.2 + * } + * ], + * "successfulRequestCount": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 120 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 138.5 + * } + * ], + * "unsuccessfulRequestCount": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 5.5 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 4.7 + * } + * ], + * "meanLatency": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 0.125 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 0.132 + * } + * ], + * "latencyPercentile50th": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 0.095 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 0.102 + * } + * ], + * "latencyPercentile90th": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 0.25 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 0.265 + * } + * ], + * "latencyPercentile99th": [ + * { + * "time": "2025-01-10T12:00:00Z", + * "value": 0.5 + * }, + * { + * "time": "2025-01-10T12:05:00Z", + * "value": 0.52 + * } + * ] + * } + */ + HTTPMetricsTimeSeries: { + /** @description Total HTTP request count time series (requests per second) */ + requestCount?: components['schemas']['TimeValuePoint'][]; + /** @description Successful HTTP request count time series (status 200, requests per second) */ + successfulRequestCount?: components['schemas']['TimeValuePoint'][]; + /** @description Unsuccessful HTTP request count time series (status != 200, requests per second) */ + unsuccessfulRequestCount?: components['schemas']['TimeValuePoint'][]; + /** @description Mean HTTP request latency time series (in seconds) */ + meanLatency?: components['schemas']['TimeValuePoint'][]; + /** @description 50th percentile (median) HTTP request latency time series (in seconds) */ + latencyPercentile50th?: components['schemas']['TimeValuePoint'][]; + /** @description 90th percentile HTTP request latency time series (in seconds) */ + latencyPercentile90th?: components['schemas']['TimeValuePoint'][]; + /** @description 99th percentile HTTP request latency time series (in seconds) */ + latencyPercentile99th?: components['schemas']['TimeValuePoint'][]; + }; ErrorResponse: { /** * @description Error type @@ -790,6 +900,48 @@ export interface operations { }; }; }; + getComponentHTTPMetrics: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['MetricsRequest']; + }; + }; + responses: { + /** @description Successfully retrieved HTTP metrics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['HTTPMetricsTimeSeries']; + }; + }; + /** @description Bad request - invalid parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; getComponentResourceMetrics: { parameters: { query?: never; diff --git a/plugins/openchoreo-observability-backend/src/services/ObservabilityService.ts b/plugins/openchoreo-observability-backend/src/services/ObservabilityService.ts index ab10be7e..9ed57f5f 100644 --- a/plugins/openchoreo-observability-backend/src/services/ObservabilityService.ts +++ b/plugins/openchoreo-observability-backend/src/services/ObservabilityService.ts @@ -9,7 +9,7 @@ import { createOpenChoreoApiClient, createObservabilityClientWithUrl, } from '@openchoreo/openchoreo-client-node'; -import { Environment, ResourceMetricsTimeSeries } from '../types'; +import { ComponentMetricsTimeSeries, Environment } from '../types'; /** * Error thrown when observability is not configured for a component @@ -132,7 +132,7 @@ export class ObservabilityService { startTime?: string; endTime?: string; }, - ): Promise { + ): Promise { const startTime = Date.now(); try { this.logger.debug( @@ -203,6 +203,22 @@ export class ObservabilityService { }, ); + const { + data: httpData, + error: httpError, + response: httpResponse, + } = await obsClient.POST('/api/metrics/component/http', { + body: { + componentId, + environmentId, + projectId, + limit: options?.limit || 100, + offset: options?.offset || 0, + startTime: options?.startTime, + endTime: options?.endTime, + }, + }); + if (error || !response.ok) { const errorText = await response.text(); this.logger.error( @@ -214,6 +230,17 @@ export class ObservabilityService { ); } + if (httpError || !httpResponse.ok) { + const errorText = await httpResponse.text(); + this.logger.error( + `Failed to fetch HTTP metrics for component ${componentId}: ${httpResponse.status} ${httpResponse.statusText}`, + { error: errorText }, + ); + throw new Error( + `Failed to fetch HTTP metrics: ${httpResponse.status} ${httpResponse.statusText}`, + ); + } + this.logger.debug( `Successfully fetched metrics for component ${componentId}: ${JSON.stringify( data, @@ -234,6 +261,13 @@ export class ObservabilityService { memory: data.memory ?? [], memoryRequests: data.memoryRequests ?? [], memoryLimits: data.memoryLimits ?? [], + requestCount: httpData.requestCount ?? [], + successfulRequestCount: httpData.successfulRequestCount ?? [], + unsuccessfulRequestCount: httpData.unsuccessfulRequestCount ?? [], + meanLatency: httpData.meanLatency ?? [], + latencyPercentile50th: httpData.latencyPercentile50th ?? [], + latencyPercentile90th: httpData.latencyPercentile90th ?? [], + latencyPercentile99th: httpData.latencyPercentile99th ?? [], }; } catch (error: unknown) { if (error instanceof ObservabilityNotConfiguredError) { diff --git a/plugins/openchoreo-observability-backend/src/types.ts b/plugins/openchoreo-observability-backend/src/types.ts index 8bfc08d2..8d0170c4 100644 --- a/plugins/openchoreo-observability-backend/src/types.ts +++ b/plugins/openchoreo-observability-backend/src/types.ts @@ -8,3 +8,7 @@ export type Environment = OpenChoreoComponents['schemas']['EnvironmentResponse']; export type ResourceMetricsTimeSeries = ObservabilityComponents['schemas']['ResourceMetricsTimeSeries']; + +export type ComponentMetricsTimeSeries = + ObservabilityComponents['schemas']['ResourceMetricsTimeSeries'] & + ObservabilityComponents['schemas']['HTTPMetricsTimeSeries']; diff --git a/plugins/openchoreo-observability/src/api/ObservabilityApi.ts b/plugins/openchoreo-observability/src/api/ObservabilityApi.ts index 93c56217..63939569 100644 --- a/plugins/openchoreo-observability/src/api/ObservabilityApi.ts +++ b/plugins/openchoreo-observability/src/api/ObservabilityApi.ts @@ -3,7 +3,7 @@ import { DiscoveryApi, FetchApi, } from '@backstage/core-plugin-api'; -import { UsageMetrics } from '../types'; +import { Metrics } from '../types'; export interface ObservabilityApi { getMetrics( @@ -20,7 +20,7 @@ export interface ObservabilityApi { startTime?: string; endTime?: string; }, - ): Promise; + ): Promise; } export const observabilityApiRef = createApiRef({ @@ -50,7 +50,7 @@ export class ObservabilityClient implements ObservabilityApi { startTime?: string; endTime?: string; }, - ): Promise { + ): Promise { const baseUrl = await this.discoveryApi.getBaseUrl( 'openchoreo-observability-backend', ); @@ -93,6 +93,17 @@ export class ObservabilityClient implements ObservabilityApi { memoryRequests: data.memoryRequests, memoryLimits: data.memoryLimits, }, + networkThroughput: { + requestCount: data.requestCount, + successfulRequestCount: data.successfulRequestCount, + unsuccessfulRequestCount: data.unsuccessfulRequestCount, + }, + networkLatency: { + meanLatency: data.meanLatency, + latencyPercentile50th: data.latencyPercentile50th, + latencyPercentile90th: data.latencyPercentile90th, + latencyPercentile99th: data.latencyPercentile99th, + }, }; } } diff --git a/plugins/openchoreo-observability/src/components/Metrics/MetricGraphByComponent.tsx b/plugins/openchoreo-observability/src/components/Metrics/MetricGraphByComponent.tsx index fbab3055..26cfcdf6 100644 --- a/plugins/openchoreo-observability/src/components/Metrics/MetricGraphByComponent.tsx +++ b/plugins/openchoreo-observability/src/components/Metrics/MetricGraphByComponent.tsx @@ -10,7 +10,12 @@ import { LegendPayload, } from 'recharts'; import { DataKey } from 'recharts/types/util/types'; -import { CpuUsageMetrics, MemoryUsageMetrics } from '../../types'; +import { + CpuUsageMetrics, + MemoryUsageMetrics, + NetworkLatencyMetrics, + NetworkThroughputMetrics, +} from '../../types'; import { formatAxisTime, formatTooltipTime, @@ -28,8 +33,12 @@ export const MetricGraphByComponent = ({ usageType, timeRange, }: { - usageData: CpuUsageMetrics | MemoryUsageMetrics; - usageType: 'cpu' | 'memory'; + usageData: + | CpuUsageMetrics + | MemoryUsageMetrics + | NetworkThroughputMetrics + | NetworkLatencyMetrics; + usageType: 'cpu' | 'memory' | 'networkThroughput' | 'networkLatency'; timeRange?: string; }) => { const classes = useMetricGraphStyles(); @@ -54,6 +63,20 @@ export const MetricGraphByComponent = ({ const handleMouseLeave = () => setHoveringDataKey(undefined); + if (transformedData.length === 0) { + return ( +
+
No data available
+ {(usageType === 'networkThroughput' || + usageType === 'networkLatency') && ( +

+ Network metrics are available in OpenChoreo Cilium Edition only. +

+ )} +
+ ); + } + return (
- Environment + Environment + Time Range + (selected as string[]).join(', ')} > {LOG_LEVELS.map(level => ( @@ -86,11 +88,13 @@ export const LogsFilter: FC = ({ - Selected Fields + Selected Fields {environments.map(env => ( @@ -132,8 +138,13 @@ export const LogsFilter: FC = ({ - Time Range - {TIME_RANGE_OPTIONS.map(option => ( {option.label}