Skip to content

Add Chart to Metrics Details UI

Daniele Rossetti requested to merge rossetd/metrics-details-charts into master

What does this MR do and why?

As part of the new Observability Metrics feature, we are adding a visualisation chart to the metrics details UI.

  • Created a MetricsHeader component to show some essential metrics details ( title, description, type )
  • Created a MetricsChart component,
    • Wraps GlLineChart
    • Override the default tooltip
  • Added both to the MetricsDetails component

Closes gitlab-org/opstrace/opstrace#2540 (closed)

Screenshots or screen recordings

image image

How to set up and validate locally

  • Enable observability_metrics feature flag

Apply patch to load mocks ( pbpaste | git apply --allow-empty )

diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 523410df9bfc..7dfd1a0afd62 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,21 +1,109 @@
+/* eslint-disable @gitlab/require-i18n-strings */
 import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import axios from '~/lib/utils/axios_utils';
 import { logError } from '~/lib/logger';
 import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from './constants';
 
+const MOCK_METRICS = {
+  metrics: [
+    {
+      name: 'app.ads.ad_requests',
+      description: 'Counts ad requests by request and response type',
+      type: 'Sum',
+    },
+    {
+      name: 'app.frontend.requests',
+      description: '',
+      type: 'Sum',
+    },
+    {
+      name: 'app.payment.transactions',
+      description: '',
+      type: 'Sum',
+    },
+    {
+      name: 'app_currency_counter',
+      description: '',
+      type: 'Sum',
+    },
+    {
+      name: 'app_recommendations_counter',
+      description: 'Counts the total number of given recommendations',
+      type: 'Sum',
+    },
+    {
+      name: 'http.client.duration',
+      description: 'measures the duration of the outbound HTTP request',
+      type: 'Histogram',
+    },
+    {
+      name: 'http.server.duration',
+      description: 'Measures the duration of inbound HTTP requests.',
+      type: 'Histogram',
+    },
+    {
+      name: 'kafka.consumer.assigned_partitions',
+      description: 'The number of partitions currently assigned to this consumer',
+      type: 'Gauge',
+    },
+    {
+      name: 'kafka.consumer.bytes_consumed_rate',
+      description: 'The average number of bytes consumed per second',
+      type: 'Gauge',
+    },
+    {
+      name: 'kafka.consumer.bytes_consumed_total',
+      description: 'The total number of bytes consumed',
+      type: 'Sum',
+    },
+    {
+      name: 'kafka.consumer.commit_latency_avg',
+      description: 'The average time taken for a commit request',
+      type: 'Gauge',
+    },
+    {
+      name: 'kafka.consumer.commit_latency_max',
+      description: 'The max time taken for a commit request',
+      type: 'Gauge',
+    },
+    {
+      name: 'kafka.consumer.commit_rate',
+      description: 'The number of commit calls per second',
+      type: 'Gauge',
+    },
+    {
+      name: 'kafka.consumer.commit_sync_time_ns_total',
+      description: 'The total time the consumer has spent in commitSync in nanoseconds',
+      type: 'Sum',
+    },
+  ],
+};
+
+const MOCK_TRACES = {
+  project_id: 51792562,
+  traces: [],
+  total_traces: 500,
+  next_page_token:
+    'eyJsYXN0X3NlZW5fdGltZXN0YW1wIjoiMjAyMy0xMS0xMCAxNjoyNzo0Ny4xMDQ0NzcwOTIiLCJzZWVuX3RyYWNlX2lkcyI6WyI2ZTI0NjFjZS04MWVmLTRkYWItN2ZmYS1hMTRlMDRiODhmNWUiXX0=',
+};
+
 function reportErrorAndThrow(e) {
   logError(e);
   Sentry.captureException(e);
   throw e;
 }
+
+function mockReturnDataWithDelay(data) {
+  return new Promise((resolve) => {
+    setTimeout(() => resolve(data), 500);
+  });
+}
+
 // Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L59
 async function enableObservability(provisioningUrl) {
   try {
-    // Note: axios.put(url, undefined, {withCredentials: true}) does not send cookies properly, so need to use the API below for the correct behaviour
-    return await axios(provisioningUrl, {
-      method: 'put',
-      withCredentials: true,
-    });
+    console.log('[DEBUG] Enabling Observability');
+    return mockReturnDataWithDelay();
   } catch (e) {
     return reportErrorAndThrow(e);
   }
@@ -24,11 +112,12 @@ async function enableObservability(provisioningUrl) {
 // Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L37
 async function isObservabilityEnabled(provisioningUrl) {
   try {
-    const { data } = await axios.get(provisioningUrl, { withCredentials: true });
+    console.log('[DEBUG] Checking Observability Enabled');
+    const data = { status: 'ready' };
     if (data && data.status) {
       // we currently ignore the 'status' payload and just check if the request was successful
       // We might improve this as part of https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2315
-      return true;
+      return mockReturnDataWithDelay(true);
     }
   } catch (e) {
     if (e.response.status === 404) {
@@ -40,19 +129,119 @@ async function isObservabilityEnabled(provisioningUrl) {
 }
 
 async function fetchTrace(tracingUrl, traceId) {
-  try {
-    if (!traceId) {
-      throw new Error('traceId is required.');
-    }
-
-    const { data } = await axios.get(`${tracingUrl}/${traceId}`, {
-      withCredentials: true,
-    });
-
-    return data;
-  } catch (e) {
-    return reportErrorAndThrow(e);
-  }
+  console.log(`[DEBUG] Fetch trace ${traceId} from ${tracingUrl}`);
+  return mockReturnDataWithDelay({
+    timestamp: '2023-11-06T14:58:38.892999936Z',
+    trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+    service_name: 'frontend',
+    operation: 'HTTP POST',
+    status_code: 'STATUS_CODE_UNSET',
+    duration_nano: 6870528,
+    spans: [
+      {
+        timestamp: '2023-11-06T14:58:38.892999936Z',
+        span_id: '86C2CAF54D03A839',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'frontend',
+        operation: 'HTTP POST',
+        duration_nano: 6870528,
+        parent_span_id: '',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.792999900Z',
+        span_id: '5E95BA1D4DCA629C',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'frontend',
+        operation: 'grpc.oteldemo.CartService/AddItem',
+        duration_nano: 4674123,
+        parent_span_id: '86C2CAF54D03A839',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.897313Z',
+        span_id: '79A1A33CCC36DC44',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'cartservice',
+        operation: 'oteldemo.CartService/AddItem',
+        duration_nano: 1138200,
+        parent_span_id: '5E95BA1D4DCA629C',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.8974467Z',
+        span_id: 'B43E6CFFD9AF4A68',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'cartservice',
+        operation: 'HGET',
+        duration_nano: 360700,
+        parent_span_id: '79A1A33CCC36DC44',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.8978547Z',
+        span_id: '80169B2C61AF41EF',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'cartservice',
+        operation: 'HMSET',
+        duration_nano: 249500,
+        parent_span_id: '79A1A33CCC36DC44',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.897999872Z',
+        span_id: '6C4E28FE982F2F73',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'frontend',
+        operation: 'grpc.oteldemo.CartService/GetCart',
+        duration_nano: 1346816,
+        parent_span_id: '86C2CAF54D03A839',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.8981128Z',
+        span_id: '427F06B0B498A482',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'cartservice',
+        operation: 'EXPIRE',
+        duration_nano: 252200,
+        parent_span_id: '79A1A33CCC36DC44',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.8995004Z',
+        span_id: 'FF45FE0F8C45FD68',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'cartservice',
+        operation: 'oteldemo.CartService/GetCart',
+        duration_nano: 512400,
+        parent_span_id: '6C4E28FE982F2F73',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+      {
+        timestamp: '2023-11-06T14:58:38.8996313Z',
+        span_id: 'F6D0D268E8A84A38',
+        trace_id: 'cfa0e008-002f-5505-0d05-31855d493ea0',
+        service_name: 'cartservice',
+        operation: 'HGET',
+        duration_nano: 290700,
+        parent_span_id: 'FF45FE0F8C45FD68',
+        status_code: 'STATUS_CODE_UNSET',
+        statusCode: 'STATUS_CODE_UNSET',
+      },
+    ],
+    total_spans: 9,
+    totalSpans: 9,
+    statusCode: 'STATUS_CODE_UNSET',
+  });
 }
 
 /**
@@ -198,15 +387,15 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize, sort
     : DEFAULT_SORTING_OPTION;
   params.append('sort', sortOrder);
 
+  console.log(`[DEBUG] Fetching traces with params: ${params.toString()}`);
+
   try {
-    const { data } = await axios.get(tracingUrl, {
-      withCredentials: true,
-      params,
-    });
+    const data = MOCK_TRACES;
+
     if (!Array.isArray(data.traces)) {
       throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
     }
-    return data;
+    return mockReturnDataWithDelay(data);
   } catch (e) {
     return reportErrorAndThrow(e);
   }
@@ -214,15 +403,17 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize, sort
 
 async function fetchServices(servicesUrl) {
   try {
-    const { data } = await axios.get(servicesUrl, {
-      withCredentials: true,
-    });
+    console.log(`[DEBUG] Fetching services from ${servicesUrl}`);
+    const uniqueServices = new Set(
+      MOCK_TRACES.traces.map((t) => t.spans.map((s) => s.service_name)).flat(),
+    );
+    const data = { services: Array.from(uniqueServices).map((s) => ({ name: s })) };
 
     if (!Array.isArray(data.services)) {
       throw new Error('failed to fetch services. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
     }
 
-    return data.services;
+    return mockReturnDataWithDelay(data.services);
   } catch (e) {
     return reportErrorAndThrow(e);
   }
@@ -237,15 +428,21 @@ async function fetchOperations(operationsUrl, serviceName) {
       throw new Error('fetchOperations() - operationsUrl must contain $SERVICE_NAME$');
     }
     const url = operationsUrl.replace('$SERVICE_NAME$', serviceName);
-    const { data } = await axios.get(url, {
-      withCredentials: true,
-    });
+
+    console.log('[DEBUG] fetching operations suggestions from', url); // eslint-disable-line @gitlab/require-i18n-strings
+    const uniqOps = new Set(
+      MOCK_TRACES.traces
+        .map((t) => t.spans.filter((s) => s.service_name === serviceName))
+        .flat()
+        .map((s) => s.operation),
+    );
+    const data = { operations: Array.from(uniqOps).map((s) => ({ name: s })) };
 
     if (!Array.isArray(data.operations)) {
       throw new Error('failed to fetch operations. invalid response'); // eslint-disable-line @gitlab/require-i18n-strings
     }
 
-    return data.operations;
+    return mockReturnDataWithDelay(data.operations);
   } catch (e) {
     return reportErrorAndThrow(e);
   }
@@ -253,13 +450,11 @@ async function fetchOperations(operationsUrl, serviceName) {
 
 async function fetchMetrics(metricsUrl) {
   try {
-    const { data } = await axios.get(metricsUrl, {
-      withCredentials: true,
-    });
+    const data = MOCK_METRICS;
     if (!Array.isArray(data.metrics)) {
       throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
     }
-    return data;
+    return mockReturnDataWithDelay(data);
   } catch (e) {
     return reportErrorAndThrow(e);
   }
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index b89c2624f81c..f6cbf7ee771f 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -27,12 +27,12 @@ export default {
 
     // TODO: Improve local GDK dev experience with tracing https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2308
     // Uncomment the lines below to to test this locally
-    // setTimeout(() => {
-    //   this.messageHandler({
-    //     data: { type: 'AUTH_COMPLETION', status: 'success' },
-    //     origin: new URL(this.oauthUrl).origin,
-    //   });
-    // }, 2000);
+    setTimeout(() => {
+      this.messageHandler({
+        data: { type: 'AUTH_COMPLETION', status: 'success' },
+        origin: new URL(this.apiConfig.oauthUrl).origin,
+      });
+    }, 2000);
   },
   destroyed() {
     window.removeEventListener('message', this.messageHandler);

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Daniele Rossetti

Merge request reports

Loading