-
Notifications
You must be signed in to change notification settings - Fork 64
@W-20481777 Setup Generic Telemetry interface #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
5770f43
telemetry wip
jarhun88 e672b12
adding missing types.ts
jarhun88 31d23ae
init telemetry tests
jarhun88 0e95fb0
fix lint
jarhun88 3b981b8
remove unnecessary files and made load synchronous
jarhun88 936736d
loading tracing.js to package
jarhun88 e4dcea9
remove unused fields
jarhun88 7bec3f3
bump version
jarhun88 68b8ca2
make telemetryConfig requirements clear and enforced in runtime
jarhun88 1bd7295
separate runtime enforcement for non custom provider
jarhun88 33655a8
spacing
jarhun88 0091132
introduce custom metrics as counter
jarhun88 fbc43af
type safety for meter and counter
jarhun88 482a6c1
type safety for meter and counter
jarhun88 63b3207
remove dead code and comments
jarhun88 e8e3c48
remove more comments
jarhun88 1e3c3d9
remove more comments
jarhun88 9ce2dd7
add type guard for telemetryProvider
jarhun88 ff24b7d
removing default in telemetryProvider
jarhun88 094adbd
check class implements interface
jarhun88 261e1f2
address nit string calling
jarhun88 bc372e1
using zod for providerTelemetryConfig
jarhun88 3fb6c24
updating docs for new env vars
jarhun88 8207daf
simplify customMetric recording of tools
jarhun88 bcf048f
removing telemetry_enabled
jarhun88 26c97b4
Merge branch 'main' into telemetry
jarhun88 71ee910
refactor init.tests.ts
jarhun88 1b103e8
lint
jarhun88 a07e524
added minify and sourcemap and debug logs to build.ts
jarhun88 cd906e9
remove type assertions and simplified validateTelemetryProvider
jarhun88 e2f90fd
reduce usage of type assertion
jarhun88 84141f1
lint
jarhun88 70bb32c
remove moncloud option
jarhun88 83ce30a
zoddified all telemetry interfaces
jarhun88 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; | ||
jarhun88 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Mock the config module | ||
| vi.mock('../config.js', () => ({ | ||
| getConfig: vi.fn(), | ||
| })); | ||
|
|
||
| // Mock the provider modules | ||
| vi.mock('./moncloud.js', () => ({ | ||
| MonCloudTelemetryProvider: vi.fn().mockImplementation(() => ({ | ||
| initialize: vi.fn(), | ||
| addAttributes: vi.fn(), | ||
| })), | ||
| })); | ||
|
|
||
| vi.mock('./noop.js', () => ({ | ||
| NoOpTelemetryProvider: vi.fn().mockImplementation(() => ({ | ||
| initialize: vi.fn(), | ||
| addAttributes: vi.fn(), | ||
| })), | ||
| })); | ||
|
|
||
| import { getConfig } from '../config.js'; | ||
| import { initializeTelemetry } from './init.js'; | ||
| import { MonCloudTelemetryProvider } from './moncloud.js'; | ||
| import { NoOpTelemetryProvider } from './noop.js'; | ||
| import { TelemetryConfig } from './types.js'; | ||
|
|
||
| describe('initializeTelemetry', () => { | ||
| const mockGetConfig = getConfig as Mock; | ||
|
|
||
| const defaultTelemetryConfig: TelemetryConfig = { | ||
| enabled: true, | ||
| provider: 'noop', | ||
| }; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| // MonCloud tests | ||
| it('returns MonCloudTelemetryProvider when provider is "moncloud"', () => { | ||
| mockGetConfig.mockReturnValue({ | ||
| telemetry: { ...defaultTelemetryConfig, provider: 'moncloud' }, | ||
| }); | ||
|
|
||
| initializeTelemetry(); | ||
|
|
||
| expect(MonCloudTelemetryProvider).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // NoOp tests | ||
| it('returns NoOpTelemetryProvider when telemetry is disabled', () => { | ||
| mockGetConfig.mockReturnValue({ | ||
| telemetry: { ...defaultTelemetryConfig, enabled: false }, | ||
| }); | ||
|
|
||
| const provider = initializeTelemetry(); | ||
|
|
||
| expect(NoOpTelemetryProvider).toHaveBeenCalled(); | ||
| expect(provider.initialize).toBeDefined(); | ||
| expect(provider.addAttributes).toBeDefined(); | ||
| }); | ||
|
|
||
| it('returns NoOpTelemetryProvider when provider is "noop"', () => { | ||
| mockGetConfig.mockReturnValue({ | ||
| telemetry: { ...defaultTelemetryConfig, provider: 'noop' }, | ||
| }); | ||
|
|
||
| initializeTelemetry(); | ||
|
|
||
| expect(NoOpTelemetryProvider).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('returns NoOpTelemetryProvider for unknown provider with warning', () => { | ||
| mockGetConfig.mockReturnValue({ | ||
| telemetry: { ...defaultTelemetryConfig, provider: 'unknown-provider' }, | ||
| }); | ||
|
|
||
| initializeTelemetry(); | ||
|
|
||
| expect(NoOpTelemetryProvider).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('falls back to NoOpTelemetryProvider on initialization error', () => { | ||
| const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); | ||
| const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); | ||
|
|
||
| // Make MonCloudTelemetryProvider throw during initialization | ||
| (MonCloudTelemetryProvider as Mock).mockImplementationOnce(() => ({ | ||
| initialize: vi.fn().mockImplementation(() => { | ||
| throw new Error('Init failed'); | ||
| }), | ||
| addAttributes: vi.fn(), | ||
| })); | ||
|
|
||
| mockGetConfig.mockReturnValue({ | ||
| telemetry: { ...defaultTelemetryConfig, provider: 'moncloud' }, | ||
| }); | ||
|
|
||
| const provider = initializeTelemetry(); | ||
|
|
||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| 'Failed to initialize telemetry provider:', | ||
| expect.any(Error), | ||
| ); | ||
| expect(consoleWarnSpy).toHaveBeenCalledWith('Falling back to NoOp telemetry provider'); | ||
| expect(provider).toBeDefined(); | ||
|
|
||
| consoleErrorSpy.mockRestore(); | ||
| consoleWarnSpy.mockRestore(); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| /** | ||
| * Telemetry initialization and provider factory | ||
| */ | ||
|
|
||
| import { resolve } from 'path'; | ||
|
|
||
| import { getConfig } from '../config.js'; | ||
| import { MonCloudTelemetryProvider } from './moncloud.js'; | ||
| import { NoOpTelemetryProvider } from './noop.js'; | ||
| import { TelemetryProvider } from './types.js'; | ||
|
|
||
| /** | ||
| * Initialize the telemetry provider based on configuration. | ||
| * | ||
| * This function should be called early in application startup, before any | ||
| * HTTP requests or other instrumented operations occur. | ||
| * | ||
| * @returns A configured telemetry provider | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * function main() { | ||
| * // Initialize telemetry first | ||
| * const telemetry = initializeTelemetry(); | ||
| * | ||
| * // Add global attributes | ||
| * telemetry.addAttributes({ | ||
| * 'tableau.server': config.server, | ||
| * 'mcp.version': '1.0.0', | ||
| * }); | ||
| * | ||
| * // Start application... | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function initializeTelemetry(): TelemetryProvider { | ||
| const config = getConfig(); | ||
anyoung-tableau marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // If telemetry is disabled, use NoOp provider | ||
| if (!config.telemetry.enabled) { | ||
| const provider = new NoOpTelemetryProvider(); | ||
| provider.initialize(); | ||
| return provider; | ||
| } | ||
|
|
||
| let provider: TelemetryProvider; | ||
|
|
||
| try { | ||
| // Select provider based on configuration | ||
| switch (config.telemetry.provider) { | ||
| case 'moncloud': | ||
jarhun88 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| provider = new MonCloudTelemetryProvider(); | ||
| break; | ||
|
|
||
| case 'custom': | ||
| // Load custom provider from user's filesystem | ||
| provider = loadCustomProvider(config.telemetry.providerConfig); | ||
| break; | ||
|
|
||
| case 'noop': | ||
| default: | ||
| if (config.telemetry.provider !== 'noop') { | ||
anyoung-tableau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| console.warn( | ||
| `Unknown telemetry provider: ${config.telemetry.provider}. Using NoOp provider.`, | ||
| ); | ||
| } | ||
| provider = new NoOpTelemetryProvider(); | ||
| } | ||
|
|
||
| // Initialize the provider | ||
| provider.initialize(); | ||
| return provider; | ||
| } catch (error) { | ||
| console.error('Failed to initialize telemetry provider:', error); | ||
| console.warn('Falling back to NoOp telemetry provider'); | ||
|
|
||
| // Fallback to NoOp on error - telemetry failures shouldn't break the application | ||
| const fallbackProvider = new NoOpTelemetryProvider(); | ||
| fallbackProvider.initialize(); | ||
| return fallbackProvider; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Load a custom telemetry provider from user's filesystem or npm package. | ||
| * | ||
| * The custom provider module should export a default class that implements TelemetryProvider. | ||
| * | ||
| * @param config - Provider configuration containing the module path | ||
| * @returns A configured custom telemetry provider | ||
| * | ||
| * @example Custom provider from file | ||
| * ```bash | ||
| * TELEMETRY_PROVIDER=custom | ||
| * TELEMETRY_PROVIDER_CONFIG='{"module":"./my-telemetry.js"}' | ||
| * ``` | ||
| * | ||
| * @example Custom provider from npm package | ||
| * ```bash | ||
| * TELEMETRY_PROVIDER=custom | ||
| * TELEMETRY_PROVIDER_CONFIG='{"module":"my-company-telemetry"}' | ||
| * ``` | ||
| */ | ||
| function loadCustomProvider(config?: Record<string, unknown>): TelemetryProvider { | ||
| if (!config?.module) { | ||
| throw new Error( | ||
| 'Custom telemetry provider requires "module" in providerConfig. ' + | ||
| 'Example: TELEMETRY_PROVIDER_CONFIG=\'{"module":"./my-telemetry.js"}\'', | ||
| ); | ||
| } | ||
|
|
||
| const modulePath = config.module as string; | ||
jarhun88 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Determine if it's a file path or npm package name | ||
| let resolvedPath: string; | ||
|
|
||
| if (modulePath.startsWith('.') || modulePath.startsWith('/')) { | ||
| // File path - resolve relative to process working directory (user's project root) | ||
| resolvedPath = resolve(process.cwd(), modulePath); | ||
| } else { | ||
| // npm package name - require as-is | ||
| resolvedPath = modulePath; | ||
| } | ||
|
|
||
| try { | ||
| // eslint-disable-next-line @typescript-eslint/no-require-imports -- Sync load for preload script | ||
| const module = require(resolvedPath); | ||
|
|
||
| // Look for default export or named export "TelemetryProvider" | ||
| const ProviderClass = module.default || module.TelemetryProvider; | ||
|
|
||
| if (!ProviderClass) { | ||
| throw new Error( | ||
| `Module ${modulePath} must export a default class or named export "TelemetryProvider" ` + | ||
| 'that implements the TelemetryProvider interface', | ||
| ); | ||
| } | ||
|
|
||
| // Instantiate the provider with the full config | ||
| return new ProviderClass(config); | ||
anyoung-tableau marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } catch (error) { | ||
| // Provide helpful error message with common issues | ||
| let errorMessage = `Failed to load custom telemetry provider from "${modulePath}". `; | ||
|
|
||
| if ((error as any).code === 'MODULE_NOT_FOUND') { | ||
jarhun88 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| errorMessage += | ||
| 'Module not found. ' + | ||
| 'If using a file path, ensure the file exists and the path is correct. ' + | ||
| 'If using an npm package, ensure it is installed.'; | ||
| } else { | ||
| errorMessage += `Error: ${error}`; | ||
| } | ||
|
|
||
| throw new Error(errorMessage); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.