Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.

Commit b9e03a2

Browse files
joshuali925opensearch-changeset-bot[bot]
authored andcommitted
feat: create agent_traces plugin in observability workspace (opensearch-project#11387)
* add agent_traces plugin similar to explore Signed-off-by: Joshua Li <joshuali925@gmail.com> * add explore.agentTraces.enabled feature flag Signed-off-by: Joshua Li <joshuali925@gmail.com> * copy explore plugin as agent_traces plugin Signed-off-by: Joshua Li <joshuali925@gmail.com> * add plugin entry files with dynamic feature flag Signed-off-by: Joshua Li <joshuali925@gmail.com> * add agent_traces page Signed-off-by: Joshua Li <joshuali925@gmail.com> * Changeset file for PR opensearch-project#11387 created/updated * fix lint Signed-off-by: Joshua Li <joshuali925@gmail.com> * update table styles Signed-off-by: Joshua Li <joshuali925@gmail.com> * feat: add copy button to parent span field in detail panel Signed-off-by: Joshua Li <joshuali925@gmail.com> * feat: add hover tooltip to timeline gantt bars Signed-off-by: Joshua Li <joshuali925@gmail.com> * refactor: use EuiBadge for span category badges and expand categorization Signed-off-by: Joshua Li <joshuali925@gmail.com> * address comments Signed-off-by: Joshua Li <joshuali925@gmail.com> * show token acount when input or output tokens is missing Signed-off-by: Joshua Li <joshuali925@gmail.com> --------- Signed-off-by: Joshua Li <joshuali925@gmail.com> Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Signed-off-by: Mark Boyd <mark.boyd@gsa.gov>
1 parent a04512a commit b9e03a2

597 files changed

Lines changed: 73625 additions & 7 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

changelogs/fragments/11387.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
feat:
2+
- Create agent_traces plugin in observability workspace ([#11387](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/11387))

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"start:docker": "scripts/use_node scripts/opensearch_dashboards --dev --opensearch.hosts=$OPENSEARCH_HOSTS --opensearch.ignoreVersionMismatch=true --server.host=$SERVER_HOST",
6969
"start:security": "scripts/use_node scripts/opensearch_dashboards --dev --security",
7070
"start:enhancements": "scripts/use_node scripts/opensearch_dashboards --dev --uiSettings.overrides['query:enhancements:enabled']=true --uiSettings.overrides['home:useNewHomePage']=true ",
71-
"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",
71+
"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",
7272
"debug": "scripts/use_node --nolazy --inspect scripts/opensearch_dashboards --dev",
7373
"debug-break": "scripts/use_node --nolazy --inspect-brk scripts/opensearch_dashboards --dev",
7474
"lint": "yarn run lint:es && yarn run lint:style",

packages/osd-stylelint-config/config/global_selectors.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@
3030
},
3131
"/\\.explore/": {
3232
"explanation": "\"explore\" prefix is preserved for Explore plugin",
33-
"approved": ["src/plugins/explore/", "examples/expressions_example/public/index.scss"]
33+
"approved": ["src/plugins/explore/", "src/plugins/agent_traces/", "examples/expressions_example/public/index.scss"]
3434
}
3535
}

src/.i18nrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"paths": {
33
"advancedSettings": "plugins/advanced_settings",
4+
"agentTraces": "plugins/agent_traces",
45
"apmOss": "plugins/apm_oss",
56
"banner": "plugins/banner",
67
"charts": "plugins/charts",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export const PLUGIN_ID = 'agentTraces';
7+
export const PLUGIN_NAME = 'Agent Traces';
8+
export const DEFAULT_COLUMNS_SETTING = 'defaultColumns';
9+
export const SAMPLE_SIZE_SETTING = 'discover:sampleSize';
10+
export const SORT_DEFAULT_ORDER_SETTING = 'discover:sort:defaultOrder';
11+
export const DOC_HIDE_TIME_COLUMN_SETTING = 'doc_table:hideTimeColumn';
12+
export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch';
13+
export const DEFAULT_TRACE_COLUMNS_SETTING = 'explore:defaultTraceColumns';
14+
export const DEFAULT_LOGS_COLUMNS_SETTING = 'explore:defaultLogsColumns';
15+
export const AGENT_TRACES_DEFAULT_LANGUAGE = 'PPL';
16+
export const AGENT_TRACES_TRACES_TAB_ID = 'traces';
17+
export const AGENT_TRACES_SPANS_TAB_ID = 'spans';
18+
19+
export enum AgentTracesFlavor {
20+
Traces = 'traces',
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"id": "agentTraces",
3+
"version": "1.0.0",
4+
"opensearchDashboardsVersion": "opensearchDashboards",
5+
"server": true,
6+
"ui": true,
7+
"requiredPlugins": [
8+
"charts",
9+
"data",
10+
"embeddable",
11+
"home",
12+
"inspector",
13+
"navigation",
14+
"opensearchDashboardsLegacy",
15+
"savedObjects",
16+
"uiActions",
17+
"urlForwarding",
18+
"usageCollection",
19+
"visualizations",
20+
"expressions"
21+
],
22+
"requiredBundles": ["opensearchDashboardsReact", "opensearchDashboardsUtils", "dataImporter"],
23+
"optionalPlugins": ["home", "share", "contextProvider", "datasetManagement", "dataImporter", "dataSource", "dataSourceManagement"],
24+
"configPath": ["agentTraces"]
25+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { DataSource } from 'src/plugins/data/public';
7+
8+
interface DataSourceConfig {
9+
name: string;
10+
type: string;
11+
metadata: any;
12+
id: string;
13+
}
14+
15+
export class MockS3DataSource extends DataSource {
16+
constructor({ id, name, type, metadata }: DataSourceConfig) {
17+
super({ id, name, type, metadata });
18+
}
19+
20+
async getDataSet(dataSetParams?: any) {
21+
return { dataSets: [this.getName()] };
22+
}
23+
24+
async testConnection(): Promise<boolean> {
25+
return true;
26+
}
27+
28+
async runQuery(queryParams: any) {
29+
return { data: {} };
30+
}
31+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { indexPatterns, IndexPattern, IndexPatternField } from '../../../data/public';
7+
import { IIndexPatternFieldList } from '../../../data/common';
8+
9+
// Initial data of index pattern fields
10+
const fieldsData = [
11+
{
12+
name: '_id',
13+
type: 'string',
14+
scripted: false,
15+
aggregatable: true,
16+
filterable: true,
17+
searchable: true,
18+
sortable: true,
19+
},
20+
{
21+
name: '_index',
22+
type: 'string',
23+
scripted: false,
24+
filterable: true,
25+
aggregatable: true,
26+
searchable: true,
27+
sortable: true,
28+
},
29+
{
30+
name: '_source',
31+
type: '_source',
32+
scripted: false,
33+
aggregatable: false,
34+
filterable: false,
35+
searchable: false,
36+
sortable: false,
37+
},
38+
];
39+
40+
// Create a mock object for index pattern fields with methods: getAll, getByName and getByType
41+
export const indexPatternFieldMock = {
42+
getAll: () => fieldsData,
43+
getByName: (name) => fieldsData.find((field) => field.name === name),
44+
getByType: (type) => fieldsData.filter((field) => field.type === type),
45+
} as IIndexPatternFieldList;
46+
47+
// Create a mock for the initial index pattern
48+
export const indexPatternInitialMock = ({
49+
id: '123',
50+
title: 'test_index',
51+
fields: indexPatternFieldMock,
52+
timeFieldName: 'order_date',
53+
formatHit: (hit: any) => (hit.fields ? hit.fields : hit._source),
54+
flattenHit: undefined,
55+
formatField: undefined,
56+
metaFields: ['_id', '_index', '_source'],
57+
getFieldByName: () => ({}),
58+
} as unknown) as IndexPattern;
59+
60+
// Add a flattenHit method to the initial index pattern mock using flattenHitWrapper
61+
const flatternHitMock = indexPatterns.flattenHitWrapper(
62+
indexPatternInitialMock,
63+
indexPatternInitialMock.metaFields
64+
);
65+
indexPatternInitialMock.flattenHit = flatternHitMock;
66+
67+
// Add a formatField method to the initial index pattern mock
68+
const formatFieldMock = (hit: Record<string, any>, field: string) => {
69+
return field === '_source' ? hit._source : indexPatternInitialMock.flattenHit(hit)[field];
70+
};
71+
indexPatternInitialMock.formatField = formatFieldMock;
72+
73+
// Export the fully set up index pattern mock
74+
export const indexPatternMock = indexPatternInitialMock;
75+
76+
// Export a function that allows customization of index pattern mocks, by adding extra fields to the fieldsData
77+
export const getMockedIndexPatternWithCustomizedFields = (fields: IndexPatternField[]) => {
78+
const customizedFieldsData = [...fieldsData, ...fields];
79+
const customizedFieldsMock = {
80+
getAll: () => customizedFieldsData,
81+
getByName: (name) => customizedFieldsData.find((field) => field.name === name),
82+
getByType: (type) => customizedFieldsData.filter((field) => field.type === type),
83+
} as IIndexPatternFieldList;
84+
85+
return {
86+
...indexPatternMock,
87+
fields: customizedFieldsMock,
88+
};
89+
};
90+
91+
// Export a function that allows customization of index pattern mocks with both extra fields and time field
92+
export const getMockedIndexPatternWithTimeField = (
93+
fields: IndexPatternField[],
94+
timeFiledName: string
95+
) => {
96+
const indexPatternWithTimeFieldMock = getMockedIndexPatternWithCustomizedFields(fields);
97+
98+
return {
99+
...indexPatternWithTimeFieldMock,
100+
timeFieldName: timeFiledName,
101+
};
102+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { createAskAiAction } from './ask_ai_action';
7+
import { ChatServiceStart } from '../../../../../core/public';
8+
9+
// Mock the AskAIActionItem component
10+
jest.mock('../components/ask_ai_action_item', () => ({
11+
AskAIActionItem: jest.fn(() => null),
12+
}));
13+
14+
describe('createAskAiAction', () => {
15+
let mockChatService: jest.Mocked<ChatServiceStart>;
16+
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
20+
// Create a minimal mock of ChatServiceStart
21+
mockChatService = {
22+
isAvailable: jest.fn().mockReturnValue(true),
23+
isWindowOpen: jest.fn().mockReturnValue(false),
24+
sendMessageWithWindow: jest.fn().mockResolvedValue(undefined),
25+
getThreadId$: jest.fn(),
26+
getThreadId: jest.fn(),
27+
openWindow: jest.fn(),
28+
closeWindow: jest.fn(),
29+
sendMessage: jest.fn(),
30+
getWindowState$: jest.fn(),
31+
onWindowOpen: jest.fn(),
32+
onWindowClose: jest.fn(),
33+
suggestedActionsService: undefined,
34+
};
35+
});
36+
37+
describe('action configuration', () => {
38+
it('should create action with correct properties', () => {
39+
const action = createAskAiAction(mockChatService);
40+
41+
expect(action.id).toBe('ask_ai');
42+
expect(action.displayName).toBe('Ask AI');
43+
expect(action.iconType).toBe('generate');
44+
expect(action.order).toBe(1);
45+
expect(typeof action.isCompatible).toBe('function');
46+
expect(typeof action.component).toBe('function');
47+
});
48+
});
49+
50+
describe('isCompatible', () => {
51+
const mockContext = {
52+
document: { message: 'test log' },
53+
query: 'test query',
54+
};
55+
56+
it('should return true when chatService is available', () => {
57+
const action = createAskAiAction(mockChatService);
58+
expect(action.isCompatible(mockContext)).toBe(true);
59+
});
60+
61+
it('should return false when chatService is not available', () => {
62+
const unavailableChatService = {
63+
...mockChatService,
64+
isAvailable: jest.fn().mockReturnValue(false),
65+
};
66+
const action = createAskAiAction(unavailableChatService);
67+
expect(action.isCompatible(mockContext)).toBe(false);
68+
});
69+
});
70+
71+
describe('chatService availability', () => {
72+
it('should create valid action with available chatService', () => {
73+
const action = createAskAiAction(mockChatService);
74+
const mockContext = { document: {} };
75+
76+
expect(action.isCompatible(mockContext)).toBe(true);
77+
expect(action).toMatchObject({
78+
id: 'ask_ai',
79+
displayName: 'Ask AI',
80+
iconType: 'generate',
81+
order: 1,
82+
});
83+
});
84+
85+
it('should create action that is incompatible when chatService is not available', () => {
86+
const unavailableChatService = {
87+
...mockChatService,
88+
isAvailable: jest.fn().mockReturnValue(false),
89+
};
90+
const action = createAskAiAction(unavailableChatService);
91+
const mockContext = { document: {} };
92+
93+
expect(action.isCompatible(mockContext)).toBe(false);
94+
expect(action.id).toBe('ask_ai');
95+
});
96+
});
97+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { LogActionDefinition } from '../types/log_actions';
7+
import { AskAIActionItem } from '../components/ask_ai_action_item';
8+
import { ChatServiceStart } from '../../../../core/public';
9+
10+
/**
11+
* Creates the Ask AI action that uses the AskAIActionItem component
12+
*/
13+
export function createAskAiAction(chatService: ChatServiceStart): LogActionDefinition {
14+
return {
15+
id: 'ask_ai',
16+
displayName: 'Ask AI',
17+
iconType: 'generate',
18+
order: 1,
19+
20+
isCompatible: () => {
21+
return chatService.isAvailable();
22+
},
23+
24+
component: (props) => {
25+
// chatService is always defined since we're using core service
26+
return AskAIActionItem({ ...props, chatService });
27+
},
28+
};
29+
}

0 commit comments

Comments
 (0)