Commit ed84530a authored by Olena Horal-Koretska's avatar Olena Horal-Koretska 2️⃣
Browse files

feat: add OTel tracing to Language Server startup monitor

parent b85f963b
Loading
Loading
Loading
Loading
+14 −3
Original line number Diff line number Diff line
@@ -35,11 +35,13 @@ import {
} from '../webview';
import { QUICK_CHAT_OPEN_TRIGGER } from '../quick_chat/constants';
import { TerminalManager } from '../duo_workflow/terminal_manager';
import { NoopObservabilityService } from '../observability';
import { LanguageClientWrapper, LanguageClientWrapperImpl } from './language_client_wrapper';
import { LanguageServerFeatureStateProvider } from './language_server_feature_state_provider';
import { FileSnapshotProvider } from './file_snapshot_provider';
import { GET_DIAGNOSTICS_REQUEST_METHOD } from './document_quality_handler';
import { RepositoryClient } from './repository_client';
import { LanguageServerStartupMonitor } from './language_server_startup_monitor';

jest.mock('../code_suggestions/gitlab_platform_manager_for_code_suggestions');
jest.mock('../log'); // disable logging in tests
@@ -118,6 +120,15 @@ describe('LanguageClientWrapper', () => {
    );
  };

  const createWrapperWithMonitor = (options: Parameters<typeof createWrapper>[0] = {}) => {
    const wrapperWithMonitor = createWrapper(options);
    wrapperWithMonitor.startupMonitor = new LanguageServerStartupMonitor(
      options.mockTelemetryEnvironment ?? gitLabTelemetryEnvironment,
      new NoopObservabilityService(),
    );
    return wrapperWithMonitor;
  };

  beforeEach(() => {
    const gitLabPlatform: GitLabPlatformForAccount = gitlabPlatformForAccount;
    getGitLabPlatformMock.mockResolvedValue(gitLabPlatform);
@@ -174,7 +185,7 @@ describe('LanguageClientWrapper', () => {
    describe('startup monitor', () => {
      it('resolves when the language server reaches Running state', async () => {
        // default mock fires State.Running immediately - just verify it resolves
        wrapper = createWrapper();
        wrapper = createWrapperWithMonitor();

        await expect(wrapper.initAndStart()).resolves.toBeUndefined();
      });
@@ -185,7 +196,7 @@ describe('LanguageClientWrapper', () => {
          ...client,
          onDidChangeState: jest.fn(() => ({ dispose: jest.fn() })), // never fires
        });
        wrapper = createWrapper();
        wrapper = createWrapperWithMonitor();

        const promise = wrapper.initAndStart();
        jest.advanceTimersByTime(10000);
@@ -204,7 +215,7 @@ describe('LanguageClientWrapper', () => {
            return { dispose: jest.fn() };
          }),
        });
        wrapper = createWrapper();
        wrapper = createWrapperWithMonitor();

        const promise = wrapper.initAndStart();
        jest.advanceTimersByTime(30000);
+3 −5
Original line number Diff line number Diff line
@@ -76,7 +76,7 @@ const createRequestFn =
    client.sendRequest(method, param);

export interface LanguageClientWrapper {
  startupMonitor: LanguageServerStartupMonitor;
  startupMonitor: LanguageServerStartupMonitor | undefined;
  setCustomPlatformConfig(clientConfig: Partial<ClientConfig>): void;
  initAndStart(): Promise<void>;
  syncConfig(): Promise<void>;
@@ -128,11 +128,10 @@ export class LanguageClientWrapperImpl implements LanguageClientWrapper {

  #platformClientConfig?: Partial<ClientConfig>;

  #startupMonitor: LanguageServerStartupMonitor;
  #startupMonitor: LanguageServerStartupMonitor | undefined;

  set startupMonitor(monitor: LanguageServerStartupMonitor) {
    this.#startupMonitor = monitor;
    this.#subscriptions.push(monitor);
  }

  constructor(
@@ -157,7 +156,6 @@ export class LanguageClientWrapperImpl implements LanguageClientWrapper {
    this.#documentQualityHandler = new DocumentQualityHandler();
    this.#repositoryClient = repositoryClient;
    this.#subscriptions.push(this.#documentQualityHandler);
    this.#startupMonitor = new LanguageServerStartupMonitor(telemetryEnvironment);
  }

  setCustomPlatformConfig(clientConfig: Partial<ClientConfig>): void {
@@ -257,7 +255,7 @@ export class LanguageClientWrapperImpl implements LanguageClientWrapper {
      this.#documentQualityHandler.getDiagnostics,
    );

    const startupComplete = this.#startupMonitor.observe(this.#client);
    const startupComplete = this.#startupMonitor?.observe(this.#client);
    this.#client.start().catch(() => {
      // Rejection here is expected during crash/restart cycles - the monitor tracks the outcome
    });
+15 −8
Original line number Diff line number Diff line
@@ -30,11 +30,13 @@ import { LanguageClientWrapperImpl } from './language_client_wrapper';
import { LanguageServerManager } from './language_server_manager';
import { LanguageClientFactory } from './client_factory';
import { LanguageServerFeatureStateProvider } from './language_server_feature_state_provider';
import { LanguageServerStartupMonitor } from './language_server_startup_monitor';

jest.mock('../code_suggestions/code_suggestions_gutter_icon');
jest.mock('../code_suggestions/code_suggestions_status_bar_item');
jest.mock('../code_suggestions/code_suggestions_state_manager');
jest.mock('./language_client_wrapper');
jest.mock('./language_server_startup_monitor');
jest.mock('../code_suggestions/gitlab_platform_manager_for_code_suggestions');
jest.mock('../webview/setup_webviews');
jest.mock('../diagnostics/log_collector_output_channel');
@@ -64,9 +66,6 @@ describe('LanguageServerManager', () => {
      sendQuickChatMessageEvent: jest.fn(),
      syncConfig: jest.fn(),
      dispose: jest.fn(),
      startupMonitor: createFakePartial({
        notifyHandshakeStarted: jest.fn(),
      }),
    });
    clientContext = {
      ide: {
@@ -95,6 +94,15 @@ describe('LanguageServerManager', () => {
    });
    jest.mocked(LanguageClientWrapperImpl).mockReturnValue(clientWrapper);
    jest.mocked(GitLabPlatformManagerForCodeSuggestionsImpl).mockReturnValue(platformManager);
    jest.mocked(LanguageServerStartupMonitor).mockImplementation(() => {
      const monitor = createFakePartial<LanguageServerStartupMonitor>({
        onDidChangePhase: jest.fn().mockReturnValue({ dispose: jest.fn() }),
        observe: jest.fn().mockResolvedValue(undefined),
        notifyHandshakeStarted: jest.fn(),
        dispose: jest.fn(),
      });
      return monitor as unknown as LanguageServerStartupMonitor;
    });

    client = createFakePartial<BaseLanguageClient>({
      stop: jest.fn(),
@@ -233,14 +241,13 @@ describe('LanguageServerManager', () => {
  it('calls notifyHandshakeStarted on the startupMonitor when initializationOptions is invoked', () => {
    const { initializationOptions } = jest.mocked(languageClientFactory.createLanguageClient).mock
      .calls[0][1];
    const notifyHandshakeStarted = jest.spyOn(
      clientWrapper.startupMonitor,
      'notifyHandshakeStarted',
    );
    const mockResults = jest.mocked(LanguageServerStartupMonitor).mock.results;
    const monitorInstance = mockResults[mockResults.length - 1]
      .value as LanguageServerStartupMonitor;

    (initializationOptions as () => unknown)();

    expect(notifyHandshakeStarted).toHaveBeenCalled();
    expect(monitorInstance.notifyHandshakeStarted).toHaveBeenCalled();
  });

  it('initializes state manager', async () => {
+7 −2
Original line number Diff line number Diff line
@@ -103,7 +103,8 @@ export class LanguageServerManager implements WebviewManager, VersionProvider {
      return;
    }

    const { gitLabPlatformManager, gitLabTelemetryEnvironment } = this.#dependencyContainer;
    const { gitLabPlatformManager, gitLabTelemetryEnvironment, observabilityService } =
      this.#dependencyContainer;
    const stateManager = new CodeSuggestionsStateManager(
      gitLabPlatformManager,
      this.#context,
@@ -112,7 +113,10 @@ export class LanguageServerManager implements WebviewManager, VersionProvider {
    const statusBarItem = new CodeSuggestionsStatusBarItem(stateManager);
    const gutterIcon = new CodeSuggestionsGutterIcon(this.#context, stateManager);
    const middleware = new LanguageClientMiddleware(stateManager);
    const startupMonitor = new LanguageServerStartupMonitor(gitLabTelemetryEnvironment);
    const startupMonitor = new LanguageServerStartupMonitor(
      gitLabTelemetryEnvironment,
      observabilityService,
    );
    const startupStatusBarItem = new LanguageServerStartupStatusBarItem(startupMonitor);
    const baseAssetsUrl = vscode.Uri.joinPath(
      this.#context.extensionUri,
@@ -162,6 +166,7 @@ export class LanguageServerManager implements WebviewManager, VersionProvider {
    await this.#wrapper.initAndStart();
    const subscriptions = [
      this.#wrapper,
      startupMonitor,
      startupStatusBarItem,
      outputChannel,
      vscode.commands.registerCommand(
+5 −1
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ import { createFakePartial } from '../test_utils/create_fake_partial';
import { log } from '../log';
import { Snowplow } from '../snowplow/snowplow';
import { GitLabTelemetryEnvironment } from '../platform/gitlab_telemetry_environment';
import { NoopObservabilityService } from '../observability';
import { LanguageServerStartupMonitor } from './language_server_startup_monitor';

jest.mock('../log');
@@ -26,7 +27,10 @@ describe('LanguageServerStartupMonitor', () => {
    const telemetryEnvironment = createFakePartial<GitLabTelemetryEnvironment>({
      getOs: jest.fn().mockReturnValue('linux'),
    });
    monitor = new LanguageServerStartupMonitor(telemetryEnvironment);
    monitor = new LanguageServerStartupMonitor(
      telemetryEnvironment,
      new NoopObservabilityService(),
    );
    client = createFakePartial<BaseLanguageClient>({
      onDidChangeState: jest.fn(handler => {
        onDidChangeStateHandler = handler;
Loading