Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions changelogs/fragments/11387.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Create agent_traces plugin in observability workspace ([#11387](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/11387))
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
"start:docker": "scripts/use_node scripts/opensearch_dashboards --dev --opensearch.hosts=$OPENSEARCH_HOSTS --opensearch.ignoreVersionMismatch=true --server.host=$SERVER_HOST",
"start:security": "scripts/use_node scripts/opensearch_dashboards --dev --security",
"start:enhancements": "scripts/use_node scripts/opensearch_dashboards --dev --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true ",
"start:explore": "scripts/use_node scripts/opensearch_dashboards --dev --explore.enabled=true --explore.discoverTraces.enabled=true --explore.discoverMetrics.enabled=true --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true --data_source.enabled=true --opensearchDashboards.branding.useExpandedHeader=false --opensearch.ignoreVersionMismatch=true --opensearchDashboards.keyboardShortcuts.enabled=true --csp.warnLegacyBrowsers=false --datasetManagement.enabled=true",
"start:explore": "scripts/use_node scripts/opensearch_dashboards --dev --explore.enabled=true --explore.discoverTraces.enabled=true --explore.discoverMetrics.enabled=true --explore.agentTraces.enabled=true --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true --data_source.enabled=true --opensearchDashboards.branding.useExpandedHeader=false --opensearch.ignoreVersionMismatch=true --opensearchDashboards.keyboardShortcuts.enabled=true --csp.warnLegacyBrowsers=false --datasetManagement.enabled=true",
"debug": "scripts/use_node --nolazy --inspect scripts/opensearch_dashboards --dev",
"debug-break": "scripts/use_node --nolazy --inspect-brk scripts/opensearch_dashboards --dev",
"lint": "yarn run lint:es && yarn run lint:style",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
},
"/\\.explore/": {
"explanation": "\"explore\" prefix is preserved for Explore plugin",
"approved": ["src/plugins/explore/", "examples/expressions_example/public/index.scss"]
"approved": ["src/plugins/explore/", "src/plugins/agent_traces/", "examples/expressions_example/public/index.scss"]
}
}
1 change: 1 addition & 0 deletions src/.i18nrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"paths": {
"advancedSettings": "plugins/advanced_settings",
"agentTraces": "plugins/agent_traces",
"apmOss": "plugins/apm_oss",
"banner": "plugins/banner",
"charts": "plugins/charts",
Expand Down
21 changes: 21 additions & 0 deletions src/plugins/agent_traces/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export const PLUGIN_ID = 'agentTraces';
export const PLUGIN_NAME = 'Agent Traces';
export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
export const SAMPLE_SIZE_SETTING = 'discover:sampleSize';
export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder';
export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn';
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
export const DEFAULT_TRACE_COLUMNS_SETTING = 'explore:defaultTraceColumns';
export const DEFAULT_LOGS_COLUMNS_SETTING = 'explore:defaultLogsColumns';
export const AGENT_TRACES_DEFAULT_LANGUAGE = 'PPL';
export const AGENT_TRACES_TRACES_TAB_ID = 'traces';
export const AGENT_TRACES_SPANS_TAB_ID = 'spans';

export enum AgentTracesFlavor {
Traces = 'traces',
}
25 changes: 25 additions & 0 deletions src/plugins/agent_traces/opensearch_dashboards.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"id": "agentTraces",
"version": "1.0.0",
"opensearchDashboardsVersion": "opensearchDashboards",
"server": true,
"ui": true,
"requiredPlugins": [
"charts",
"data",
"embeddable",
"home",
"inspector",
"navigation",
"opensearchDashboardsLegacy",
"savedObjects",
"uiActions",
"urlForwarding",
"usageCollection",
"visualizations",
"expressions"
],
"requiredBundles": ["opensearchDashboardsReact", "opensearchDashboardsUtils", "dataImporter"],
"optionalPlugins": ["home", "share", "contextProvider", "datasetManagement", "dataImporter", "dataSource", "dataSourceManagement"],
"configPath": ["agentTraces"]
}
31 changes: 31 additions & 0 deletions src/plugins/agent_traces/public/__mock__/index.test.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { DataSource } from 'src/plugins/data/public';

interface DataSourceConfig {
name: string;
type: string;
metadata: any;
id: string;
}

export class MockS3DataSource extends DataSource {
constructor({ id, name, type, metadata }: DataSourceConfig) {
super({ id, name, type, metadata });
}

async getDataSet(dataSetParams?: any) {
return { dataSets: [this.getName()] };
}

async testConnection(): Promise<boolean> {
return true;
}

async runQuery(queryParams: any) {
return { data: {} };
}
}
102 changes: 102 additions & 0 deletions src/plugins/agent_traces/public/__mock__/index_pattern_mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { indexPatterns, IndexPattern, IndexPatternField } from '../../../data/public';
import { IIndexPatternFieldList } from '../../../data/common';

// Initial data of index pattern fields
const fieldsData = [
{
name: '_id',
type: 'string',
scripted: false,
aggregatable: true,
filterable: true,
searchable: true,
sortable: true,
},
{
name: '_index',
type: 'string',
scripted: false,
filterable: true,
aggregatable: true,
searchable: true,
sortable: true,
},
{
name: '_source',
type: '_source',
scripted: false,
aggregatable: false,
filterable: false,
searchable: false,
sortable: false,
},
];

// Create a mock object for index pattern fields with methods: getAll, getByName and getByType
export const indexPatternFieldMock = {
getAll: () => fieldsData,
getByName: (name) => fieldsData.find((field) => field.name === name),
getByType: (type) => fieldsData.filter((field) => field.type === type),
} as IIndexPatternFieldList;

// Create a mock for the initial index pattern
export const indexPatternInitialMock = ({
id: '123',
title: 'test_index',
fields: indexPatternFieldMock,
timeFieldName: 'order_date',
formatHit: (hit: any) => (hit.fields ? hit.fields : hit._source),
flattenHit: undefined,
formatField: undefined,
metaFields: ['_id', '_index', '_source'],
getFieldByName: () => ({}),
} as unknown) as IndexPattern;

// Add a flattenHit method to the initial index pattern mock using flattenHitWrapper
const flatternHitMock = indexPatterns.flattenHitWrapper(
indexPatternInitialMock,
indexPatternInitialMock.metaFields
);
indexPatternInitialMock.flattenHit = flatternHitMock;

// Add a formatField method to the initial index pattern mock
const formatFieldMock = (hit: Record<string, any>, field: string) => {
return field === '_source' ? hit._source : indexPatternInitialMock.flattenHit(hit)[field];
};
indexPatternInitialMock.formatField = formatFieldMock;

// Export the fully set up index pattern mock
export const indexPatternMock = indexPatternInitialMock;

// Export a function that allows customization of index pattern mocks, by adding extra fields to the fieldsData
export const getMockedIndexPatternWithCustomizedFields = (fields: IndexPatternField[]) => {
const customizedFieldsData = [...fieldsData, ...fields];
const customizedFieldsMock = {
getAll: () => customizedFieldsData,
getByName: (name) => customizedFieldsData.find((field) => field.name === name),
getByType: (type) => customizedFieldsData.filter((field) => field.type === type),
} as IIndexPatternFieldList;

return {
...indexPatternMock,
fields: customizedFieldsMock,
};
};

// Export a function that allows customization of index pattern mocks with both extra fields and time field
export const getMockedIndexPatternWithTimeField = (
fields: IndexPatternField[],
timeFiledName: string
) => {
const indexPatternWithTimeFieldMock = getMockedIndexPatternWithCustomizedFields(fields);

return {
...indexPatternWithTimeFieldMock,
timeFieldName: timeFiledName,
};
};
97 changes: 97 additions & 0 deletions src/plugins/agent_traces/public/actions/ask_ai_action.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { createAskAiAction } from './ask_ai_action';
import { ChatServiceStart } from '../../../../../core/public';

// Mock the AskAIActionItem component
jest.mock('../components/ask_ai_action_item', () => ({
AskAIActionItem: jest.fn(() => null),
}));

describe('createAskAiAction', () => {
let mockChatService: jest.Mocked<ChatServiceStart>;

beforeEach(() => {
jest.clearAllMocks();

// Create a minimal mock of ChatServiceStart
mockChatService = {
isAvailable: jest.fn().mockReturnValue(true),
isWindowOpen: jest.fn().mockReturnValue(false),
sendMessageWithWindow: jest.fn().mockResolvedValue(undefined),
getThreadId$: jest.fn(),
getThreadId: jest.fn(),
openWindow: jest.fn(),
closeWindow: jest.fn(),
sendMessage: jest.fn(),
getWindowState$: jest.fn(),
onWindowOpen: jest.fn(),
onWindowClose: jest.fn(),
suggestedActionsService: undefined,
};
});

describe('action configuration', () => {
it('should create action with correct properties', () => {
const action = createAskAiAction(mockChatService);

expect(action.id).toBe('ask_ai');
expect(action.displayName).toBe('Ask AI');
expect(action.iconType).toBe('generate');
expect(action.order).toBe(1);
expect(typeof action.isCompatible).toBe('function');
expect(typeof action.component).toBe('function');
});
});

describe('isCompatible', () => {
const mockContext = {
document: { message: 'test log' },
query: 'test query',
};

it('should return true when chatService is available', () => {
const action = createAskAiAction(mockChatService);
expect(action.isCompatible(mockContext)).toBe(true);
});

it('should return false when chatService is not available', () => {
const unavailableChatService = {
...mockChatService,
isAvailable: jest.fn().mockReturnValue(false),
};
const action = createAskAiAction(unavailableChatService);
expect(action.isCompatible(mockContext)).toBe(false);
});
});

describe('chatService availability', () => {
it('should create valid action with available chatService', () => {
const action = createAskAiAction(mockChatService);
const mockContext = { document: {} };

expect(action.isCompatible(mockContext)).toBe(true);
expect(action).toMatchObject({
id: 'ask_ai',
displayName: 'Ask AI',
iconType: 'generate',
order: 1,
});
});

it('should create action that is incompatible when chatService is not available', () => {
const unavailableChatService = {
...mockChatService,
isAvailable: jest.fn().mockReturnValue(false),
};
const action = createAskAiAction(unavailableChatService);
const mockContext = { document: {} };

expect(action.isCompatible(mockContext)).toBe(false);
expect(action.id).toBe('ask_ai');
});
});
});
29 changes: 29 additions & 0 deletions src/plugins/agent_traces/public/actions/ask_ai_action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { LogActionDefinition } from '../types/log_actions';
import { AskAIActionItem } from '../components/ask_ai_action_item';
import { ChatServiceStart } from '../../../../core/public';

/**
* Creates the Ask AI action that uses the AskAIActionItem component
*/
export function createAskAiAction(chatService: ChatServiceStart): LogActionDefinition {
return {
id: 'ask_ai',
displayName: 'Ask AI',
iconType: 'generate',
order: 1,

isCompatible: () => {
return chatService.isAvailable();
},

component: (props) => {
// chatService is always defined since we're using core service
return AskAIActionItem({ ...props, chatService });
},
};
}
33 changes: 33 additions & 0 deletions src/plugins/agent_traces/public/actions/import_data_action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { i18n } from '@osd/i18n';
import { FlyoutActionConfig, FlyoutComponentProps } from '../services/query_panel_actions_registry';
import { ImportDataModal } from '../components/import_data_modal';

const label = i18n.translate('agentTraces.queryPanel.importDataLabel', {
defaultMessage: 'Import data',
});

/**
* Component wrapper that adapts the shared ImportDataModal for use with the flyout action system
*/
const ImportDataActionComponent: React.FC<FlyoutComponentProps> = ({ closeFlyout, services }) => {
return <ImportDataModal services={services} isVisible={true} onClose={closeFlyout} />;
};

/**
* Import data action configuration for the query panel actions registry
*/
export const importDataActionConfig: FlyoutActionConfig = {
id: 'import-data',
actionType: 'flyout',
order: 100,
getLabel: () => label,
getIcon: () => 'importAction',
getIsEnabled: () => true,
component: ImportDataActionComponent,
};
Loading
Loading