diff --git a/.github/workflows/opensearch-observability-test-and-build-workflow.yml b/.github/workflows/opensearch-observability-test-and-build-workflow.yml index 1ba57de04..307df7332 100644 --- a/.github/workflows/opensearch-observability-test-and-build-workflow.yml +++ b/.github/workflows/opensearch-observability-test-and-build-workflow.yml @@ -3,7 +3,7 @@ name: Test and Build OpenSearch Observability Backend Plugin on: [pull_request, push] env: - OPENSEARCH_VERSION: '1.2.0-SNAPSHOT' + OPENSEARCH_VERSION: '1.2.1-SNAPSHOT' OPENSEARCH_BRANCH: '1.2' COMMON_UTILS_BRANCH: 'main' diff --git a/dashboards-observability/common/constants/application_analytics.ts b/dashboards-observability/common/constants/application_analytics.ts new file mode 100644 index 000000000..59ee7704e --- /dev/null +++ b/dashboards-observability/common/constants/application_analytics.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const TAB_OVERVIEW_ID_TXT_PFX = 'app-analytics-overview-'; +export const TAB_SERVICE_ID_TXT_PFX = 'app-analytics-service-'; +export const TAB_TRACE_ID_TXT_PFX = 'app-analytics-trace-'; +export const TAB_LOG_ID_TXT_PFX = 'app-analytics-log-'; +export const TAB_CONFIG_ID_TXT_PFX = 'app-analytics-config-'; +export const TAB_OVERVIEW_TITLE = 'Overview'; +export const TAB_SERVICE_TITLE = 'Services'; +export const TAB_TRACE_TITLE = 'Traces & Spans'; +export const TAB_LOG_TITLE = 'Log Events'; +export const TAB_CONFIG_TITLE = 'Configuration'; + +export interface optionType { + label: string; +} diff --git a/dashboards-observability/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts index 4838a54ab..26cf80bbf 100644 --- a/dashboards-observability/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import CSS from 'csstype'; + // Client route export const PPL_BASE = '/api/ppl'; export const PPL_SEARCH = '/search'; @@ -58,3 +60,9 @@ export const PLOTLY_COLOR = [ ]; export const LONG_CHART_COLOR = PLOTLY_COLOR[1]; + +export const pageStyles: CSS.Properties = { + float: 'left', + width: '100%', + maxWidth: '1130px', +}; diff --git a/dashboards-observability/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts index af86b24e8..da4018488 100644 --- a/dashboards-observability/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -14,7 +14,7 @@ import { SELECTED_TIMESTAMP, SELECTED_DATE_RANGE } from '../constants/explorer'; - import { HttpStart, NotificationsStart } from '../../../../src/core/public'; + import { CoreStart, HttpStart, NotificationsStart } from '../../../../src/core/public'; import SavedObjects from '../../public/services/saved_objects/event_analytics/saved_objects'; import TimestampUtils from '../../public/services/timestamp/timestamp'; import PPLService from '../../public/services/requests/ppl'; @@ -98,4 +98,6 @@ export interface IExplorerProps { text?: React.ReactChild | undefined, side?: string | undefined ) => void; -} \ No newline at end of file + http: CoreStart['http']; + tabCreatedTypes?: any; +} diff --git a/dashboards-observability/public/components/app.tsx b/dashboards-observability/public/components/app.tsx index 4c7e57642..38560e08a 100644 --- a/dashboards-observability/public/components/app.tsx +++ b/dashboards-observability/public/components/app.tsx @@ -11,6 +11,7 @@ import { CoreStart } from '../../../../src/core/public'; import { observabilityID, observabilityTitle } from '../../common/constants/shared'; import store from '../framework/redux/store'; import { AppPluginStartDependencies } from '../types'; +import { Home as ApplicationAnalyticsHome } from './application_analytics/home'; import { Home as CustomPanelsHome } from './custom_panels/home'; import { EventAnalytics } from './explorer/event_analytics'; import { Main as NotebooksHome } from './notebooks/components/main'; @@ -50,6 +51,24 @@ export const App = ({ <> + { + return ( + + ) + }} + /> ( @@ -108,7 +127,7 @@ export const App = ({ /> ); }} - /> + /> diff --git a/dashboards-observability/public/components/application_analytics/components/app_table.tsx b/dashboards-observability/public/components/application_analytics/components/app_table.tsx new file mode 100644 index 000000000..254f183ac --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/app_table.tsx @@ -0,0 +1,221 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiLink, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiPopover, + EuiSpacer, + EuiTableFieldDataColumnType, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import _ from 'lodash'; +import React, { ReactElement, useEffect, useState } from 'react'; +import { AppAnalyticsComponentDeps, ApplicationType } from '../home'; +import { pageStyles } from '../../../../common/constants/shared'; + +interface AppTableProps extends AppAnalyticsComponentDeps { + loading: boolean; + applications: Array; + }; + +export function AppTable(props: AppTableProps) { + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const { applications, parentBreadcrumb } = props; + + useEffect(() => { + props.chrome.setBreadcrumbs( + [ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics', + } + ]); + }) + + const popoverButton = ( + setIsActionsPopoverOpen(!isActionsPopoverOpen)} + > + Actions + + ); + + const popoverItems: ReactElement[] = [ + + Rename + , + + Duplicate + , + + Delete + , + + Add sample application + , + ]; + + const tableColumns = [ + { + field: 'name', + name: 'Name', + sortable: true, + truncateText: true, + render: (value, record) => ( + {_.truncate(value, { length: 100 })} + ), + }, + { + field: 'composition', + name: 'Composition', + sortable: true, + truncateText: true, + }, + { + field: 'currentAvailability', + name: 'Current Availability', + sortable: true, + truncateText: true, + }, + { + field: 'availabilityMetrics', + name: 'Availability Metrics', + sortable: true, + truncateText: true, + }, + ] as Array< + EuiTableFieldDataColumnType<{ + name: string; + id: string; + composition: string; + currentAvailability: string; + availabilityMetrics: string; + }> + >; + + return ( +
+ + + + + +

Overview

+
+
+
+ + + + +

+ Applications ({applications.length}) +

+
+
+ + + + setIsActionsPopoverOpen(false)} + > + + + + + + Create application + + + + +
+ + {applications.length > 0 ? ( + + ) : ( + <> + + +

No applications

+
+ + + + + Create application + + + + + Add sample applications + + + + + + )} +
+
+
+
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/application.tsx b/dashboards-observability/public/components/application_analytics/components/application.tsx new file mode 100644 index 000000000..3e1816570 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/application.tsx @@ -0,0 +1,244 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, + EuiText, + EuiTitle, + } from '@elastic/eui'; +import { LogExplorer } from '../../explorer/log_explorer'; +import { Dashboard } from '../../trace_analytics/components/dashboard'; +import { Services } from '../../trace_analytics/components/services'; +import { Traces } from '../../trace_analytics/components/traces'; +import { SpanDetailPanel } from '../../trace_analytics/components/traces/span_detail_panel'; +import { Configuration } from './configuration'; +import DSLService from 'public/services/requests/dsl'; +import PPLService from 'public/services/requests/ppl'; +import SavedObjects from 'public/services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; +import React, { ReactChild, useMemo, useState } from 'react'; +import { isEmpty, uniqueId } from 'lodash'; +import { + TAB_CONFIG_ID_TXT_PFX, + TAB_CONFIG_TITLE, + TAB_LOG_ID_TXT_PFX, + TAB_LOG_TITLE, + TAB_OVERVIEW_ID_TXT_PFX, + TAB_OVERVIEW_TITLE, + TAB_SERVICE_ID_TXT_PFX, + TAB_SERVICE_TITLE, + TAB_TRACE_ID_TXT_PFX, + TAB_TRACE_TITLE +} from '../../../../common/constants/application_analytics'; +import { EmptyTabParams, IQueryTab } from '../../../../common/types/explorer'; +import { useHistory } from 'react-router-dom'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { RAW_QUERY } from '../../../../common/constants/explorer'; +import { NotificationsStart } from '../../../../../../src/core/public'; +import { AppAnalyticsComponentDeps } from '../home'; + + +const TAB_OVERVIEW_ID = uniqueId(TAB_OVERVIEW_ID_TXT_PFX); +const TAB_SERVICE_ID = uniqueId(TAB_SERVICE_ID_TXT_PFX); +const TAB_TRACE_ID = uniqueId(TAB_TRACE_ID_TXT_PFX); +const TAB_LOG_ID = uniqueId(TAB_LOG_ID_TXT_PFX); +const TAB_CONFIG_ID = uniqueId(TAB_CONFIG_ID_TXT_PFX); + +export interface DetailTab { + id: string; + label: string; + description: string; + onClick: () => void; + testId: string; +} + +interface AppDetailProps extends AppAnalyticsComponentDeps { + disabled?: boolean; + appId: string; + pplService: PPLService; + dslService: DSLService; + savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + notifications: NotificationsStart; +} + + +export function Application(props: AppDetailProps) { + const { pplService, dslService, timestampUtils, savedObjects, http, notifications } = props; + const [selectedTabId, setSelectedTab] = useState(TAB_OVERVIEW_ID); + const handleContentTabClick = (selectedTab: IQueryTab) => setSelectedTab(selectedTab.id); + const history = useHistory(); + const [toasts, setToasts] = useState>([]); + + const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { + if (!text) text = ''; + setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); + }; + + const getExistingEmptyTab = ({tabIds, queries, explorerData}: EmptyTabParams) => { + let emptyTabId = ''; + for (let i = 0; i < tabIds!.length; i++) { + const tid = tabIds![i]; + if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { + emptyTabId = tid; + break; + } + } + return emptyTabId; + }; + + + const getOverview = () => { + return ( + + ); + }; + + const getService = () => { + return ( + + ); + }; + + const getTrace = () => { + return ( + <> + + + + + ); + }; + + const getLog = () => { + return ( + + ); + }; + + const getConfig = () => { + return ( + + ); + }; + + function getAppAnalyticsTab ({ + tabId, + tabTitle, + getContent + }: { + tabId: string, + tabTitle: string, + getContent: () => JSX.Element + }) { + return { + id: tabId, + name: (<> + + { tabTitle } + + ), + content: ( + <> + { getContent() } + ) + }; + }; + + const getAppAnalyticsTabs = () => { + return [ + getAppAnalyticsTab( + { + tabId: TAB_OVERVIEW_ID, + tabTitle: TAB_OVERVIEW_TITLE, + getContent: () => getOverview() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_SERVICE_ID, + tabTitle: TAB_SERVICE_TITLE, + getContent: () => getService() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_TRACE_ID, + tabTitle: TAB_TRACE_TITLE, + getContent: () => getTrace() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_LOG_ID, + tabTitle: TAB_LOG_TITLE, + getContent: () => getLog() + } + ), + getAppAnalyticsTab( + { + tabId: TAB_CONFIG_ID, + tabTitle: TAB_CONFIG_TITLE, + getContent: () => getConfig() + } + ) + ]; + }; + + + const memorizedAppAnalyticsTabs = useMemo(() => { + return getAppAnalyticsTabs(); + }, + []); + + return ( +
+ + + + + +

my-app1

+
+
+
+ { tab.id === selectedTabId }) } + onTabClick={ (selectedTab: EuiTabbedContentTab) => handleContentTabClick(selectedTab) } + tabs={ memorizedAppAnalyticsTabs } + /> +
+
+
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx new file mode 100644 index 000000000..297e409c4 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/log_config.tsx @@ -0,0 +1,108 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiText, EuiSpacer, EuiButton, EuiFormRow, EuiFlexItem, EuiBadge, EuiOverlayMask } from "@elastic/eui"; +import { uiSettingsService } from "../../../../../common/utils"; +import { Autocomplete } from "../../../common/search/autocomplete"; +import DSLService from "public/services/requests/dsl"; +import React, { useState } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import{ getClearModal } from "../helpers/modal_containers"; + +interface LogConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + setIsFlyoutVisible: any; +} + +export const LogConfig = (props: LogConfigProps) => { + const { dslService, query, setQuery, setIsFlyoutVisible } = props; + const [logOpen, setLogOpen] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalLayout, setModalLayout] = useState(); + const tempQuery =''; + + const handleQueryChange = async (query: string) => setQuery(query); + + const showFlyout = () => { + setIsFlyoutVisible(true); + }; + + const onCancel = () => { + setIsModalVisible(false); + } + + const closeModal = () => { + setIsModalVisible(false); + }; + + const showModal = () => { + setIsModalVisible(true); + }; + + const onConfirm = () => { + handleQueryChange(''); + closeModal(); + } + + const clearAllModal = () => { + setModalLayout( + getClearModal( + onCancel, + onConfirm, + 'Clear log source', + 'Are you sure you would like to clear the log source?', + 'Clear' + ) + ); + showModal(); + }; + + return ( +
+ + +

Log Source

+
+ + + Configure your application base query + + + } + extraAction={Clear} + onToggle={(isOpen) => {setLogOpen(isOpen)}} + paddingSize="l" + > + + + {}} + dslService={dslService} + /> + showFlyout()} + onClickAriaLabel={"pplLinkShowFlyout"} + > + PPL + + + +
+ {isModalVisible && modalLayout} +
+ ); +} \ No newline at end of file diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx new file mode 100644 index 000000000..9afed9b4c --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/service_config.tsx @@ -0,0 +1,116 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { FilterType } from "../../../trace_analytics/components/common/filters/filters"; +import { ServiceObject } from "../../../trace_analytics/components/common/plots/service_map"; +import { ServiceMap } from "../../../trace_analytics/components/services"; +import { handleServiceMapRequest } from "../../../trace_analytics/requests/services_request_handler"; +import DSLService from "public/services/requests/dsl"; +import React, { useState } from "react"; +import { useEffect } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import { optionType } from "common/constants/application_analytics"; + +interface ServiceConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + selectedServices: Array; + setSelectedServices: (services: Array) => void; +} + +export const ServiceConfig = (props: ServiceConfigProps) => { + const { dslService, filters, setFilters, http, selectedServices, setSelectedServices } = props; + const [servicesOpen, setServicesOpen] = useState(false); + const [serviceMap, setServiceMap] = useState({}); + const [serviceMapIdSelected, setServiceMapIdSelected] = useState<'latency' | 'error_rate' | 'throughput'>('latency'); + + useEffect(() => { + handleServiceMapRequest(http, dslService, serviceMap, setServiceMap); + }, []) + + useEffect (() => { + const serviceOptions = filters.filter(f => f.field === 'serviceName').map((f) => { return { label: f.value }}); + const noDups = serviceOptions.filter((s, index) => { return serviceOptions.findIndex(ser => ser.label === s.label) === index }); + setSelectedServices(noDups); + }, [filters]) + + const addFilter = (filter: FilterType) => { + for (const addedFilter of filters) { + if ( + addedFilter.field === filter.field && + addedFilter.operator === filter.operator && + addedFilter.value === filter.value + ) { + return; + } + } + const newFilters = [...filters, filter]; + setFilters(newFilters); + }; + + const onServiceChange = (selectedServices: any) => { + const serviceFilters = selectedServices.map((option: optionType) => { + return { + field: 'serviceName', + operator: 'is', + value: option.label, + inverted: false, + disabled: false + } + }) + const nonServiceFilters = filters.filter((f) => f.field !== 'serviceName'); + setFilters([...nonServiceFilters, ...serviceFilters]); + }; + + const clearServices = () => { + const withoutServices = filters.filter((f) => f.field !== 'serviceName') + setFilters(withoutServices); + }; + + const services = Object.keys(serviceMap).map((service) => { return { label: service } }); + + return ( + + +

+ Services & Entities {selectedServices.length} +

+
+ + + Select services & entities to include in this application + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setServicesOpen(isOpen)}} + paddingSize="l" + > + + + + + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx new file mode 100644 index 000000000..e17fa78fd --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/config_components/trace_config.tsx @@ -0,0 +1,203 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dateMath from '@elastic/datemath'; +import { EuiAccordion, EuiBadge, EuiButton, EuiComboBox, EuiFormRow, EuiSpacer, EuiText } from "@elastic/eui"; +import { optionType } from "common/constants/application_analytics"; +import { filtersToDsl } from "../../../trace_analytics/components/common/helper_functions"; +import { handleDashboardRequest } from "../../../trace_analytics/requests/dashboard_request_handler"; +import DSLService from "public/services/requests/dsl"; +import React, { useEffect, useState } from "react"; +import { AppAnalyticsComponentDeps } from "../../home"; +import { DashboardTable } from '../../../trace_analytics/components/dashboard/dashboard_table'; +import { FilterType } from 'public/components/trace_analytics/components/common/filters/filters'; + +interface TraceConfigProps extends AppAnalyticsComponentDeps { + dslService: DSLService; + selectedTraces: Array; + setSelectedTraces: (traces: Array) => void; +} + +export const TraceConfig = (props: TraceConfigProps) => { + const { dslService, query, filters, setFilters, http, startTime, endTime, selectedTraces, setSelectedTraces } = props; + const [traceOpen, setTraceOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [traceItems, setTraceItems] = useState([]); + const [traceOptions, setTraceOptions] = useState>([]); + const [percentileMap, setPercentileMap] = useState<{ [traceGroup: string]: number[] }>({}); + const [redirect, setRedirect] = useState(true); + + useEffect(() => { + setLoading(true) + const timeFilterDSL = filtersToDsl([], '', startTime, endTime); + const latencyTrendStartTime = dateMath + .parse(endTime) + ?.subtract(24, 'hours') + .toISOString()!; + const latencyTrendDSL = filtersToDsl( + filters, + query, + latencyTrendStartTime, + endTime + ); + handleDashboardRequest( + http, + dslService, + timeFilterDSL, + latencyTrendDSL, + traceItems, + setTraceItems, + setPercentileMap + ).then(() => setLoading(false)); + setRedirect(false); + }, []) + + useEffect (() => { + const toOptions = traceItems.map((item: any) => { return { label: item.dashboard_trace_group_name }}); + setTraceOptions(toOptions); + }, [traceItems]) + + useEffect (() => { + const filteredOptions = filters.filter(f => f.field === 'traceGroup').map((f) => { return { label: f.value }}); + const noDups = filteredOptions.filter((t, index) => { return filteredOptions.findIndex(trace => trace.label === t.label) === index }); + setSelectedTraces(noDups); + }, [filters]) + + const addFilter = (filter: FilterType) => { + for (const addedFilter of filters) { + if ( + addedFilter.field === filter.field && + addedFilter.operator === filter.operator && + addedFilter.value === filter.value + ) { + return; + } + } + const newFilters = [...filters, filter]; + setFilters(newFilters); + }; + + const onTraceChange = (selectedTraces: any) => { + const traceFilters = selectedTraces.map((option: optionType) => { + return { + field: 'traceGroup', + operator: 'is', + value: option.label, + inverted: false, + disabled: false + } + }) + const nonTraceFilters = filters.filter((f) => f.field !== 'traceGroup'); + setFilters([...nonTraceFilters, ...traceFilters]); + }; + + const onCreateTrace = (searchValue: string, flattenedOptions: any) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + if (!normalizedSearchValue) { + return; + } + const newTraceOption = { + label: searchValue + } + const newTraceFilter = { + field: 'traceGroup', + operator: 'is', + value: searchValue, + inverted: false, + disabled: false + }; + // Create the option if it doesn't exist. + if ( + flattenedOptions.findIndex( + (option: optionType) => option.label.trim().toLowerCase() === normalizedSearchValue + ) === -1 + ) { + setTraceOptions([...traceOptions, newTraceOption]); + } + // Select the option. + setFilters([...filters, newTraceFilter]); + }; + + const addPercentileFilter = (condition = 'gte', additionalFilters = [] as FilterType[]) => { + if (traceItems.length === 0 || Object.keys(percentileMap).length === 0) return; + for (let i = 0; i < props.filters.length; i++) { + if (props.filters[i].custom) { + const newFilter = JSON.parse(JSON.stringify(props.filters[i])); + newFilter.custom.query.bool.should.forEach((should: any) => + should.bool.must.forEach((must: any) => { + const range = must?.range?.['traceGroupFields.durationInNanos']; + if (range) { + const duration = range.lt || range.lte || range.gt || range.gte; + if (duration || duration === 0) { + must.range['traceGroupFields.durationInNanos'] = { + [condition]: duration, + }; + } + } + }) + ); + newFilter.value = condition === 'gte' ? '>= 95th' : '< 95th'; + const newFilters = [...filters, ...additionalFilters]; + newFilters.splice(i, 1, newFilter); + setFilters(newFilters); + return; + } + } + } + + const clearTraces = () => { + const withoutTraces = filters.filter((f) => f.field !== 'traceGroup') + setFilters(withoutTraces); + }; + + return ( + + +

+ Trace Groups {selectedTraces.length} +

+
+ + + Constrain your application to specific trace groups + + + } + extraAction={Clear all} + onToggle={(isOpen) => {setTraceOpen(isOpen)}} + paddingSize="l" + > + + + + + +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/configuration.tsx b/dashboards-observability/public/components/application_analytics/components/configuration.tsx new file mode 100644 index 000000000..0cf800892 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/configuration.tsx @@ -0,0 +1,152 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiInMemoryTable, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiTableFieldDataColumnType, + EuiText, + EuiTitle +} from '@elastic/eui'; +import React from 'react'; + +const dummy = [{ + level: "Available", + definition: "error rate below or equal to 1%", + id: "1" +}]; + +const dummyLogSources = [ + {logName: "index_1"}, {logName: "ingest_logs_all"} +]; + +const dummyServicesEntities = [ + {serviceName: "Payment"}, {serviceName: "Users"}, {serviceName: "Purchase"} +]; + +const dummyTraceGroups = [ + {traceGroup: "Payment.auto"}, {traceGroup: "Users.admin"}, {traceGroup: "Purchase.source"} +]; + +export const Configuration = () => { + + const tableColumns = [ + { + field: 'level', + name: 'Level', + render: (value) => value, + }, + { + field: 'definition', + name: 'Definition', + render: (value) => value, + }, + ] as Array< + EuiTableFieldDataColumnType<{ + level: string; + id: string; + definition: string; + }> + >; + + return ( +
+ + + + + + +

+ Composition +

+
+
+ + + + {}}> + Edit composition + + + + +
+ + + + +
Log Sources
+
    + {dummyLogSources.map(function(item, index){ + return
  • {item.logName}
  • + })} +
+
+
+ + +
Services & Entities
+
    + {dummyServicesEntities.map(function(item, index){ + return
  • {item.serviceName}
  • + })} +
+
+
+ + +
Trace groups
+
    + {dummyTraceGroups.map(function(item, index){ + return
  • {item.traceGroup}
  • + })} +
+
+
+
+
+
+
+ + + + + + +

+ Availability +

+
+
+ + + + {}}> + Edit availability + + + + +
+ + +
+
+
+
+ ) +} diff --git a/dashboards-observability/public/components/application_analytics/components/create.tsx b/dashboards-observability/public/components/application_analytics/components/create.tsx new file mode 100644 index 000000000..f71389778 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/create.tsx @@ -0,0 +1,171 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + EuiButton, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiTitle, + EuiToolTip +} from "@elastic/eui"; +import DSLService from "public/services/requests/dsl"; +import React, { useEffect, useState } from "react"; +import { ChangeEvent } from "react"; +import { AppAnalyticsComponentDeps } from "../home"; +import { TraceConfig } from './config_components/trace_config'; +import { ServiceConfig } from "./config_components/service_config"; +import { LogConfig } from "./config_components/log_config"; +import { PPLReferenceFlyout } from "../../../components/common/helpers"; +import { optionType } from "common/constants/application_analytics"; + +interface CreateAppProps extends AppAnalyticsComponentDeps { + dslService: DSLService; +}; + +export const CreateApp = (props: CreateAppProps) => { + const { parentBreadcrumb, chrome, query } = props; + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [selectedServices, setSelectedServices] = useState>([]); + const [selectedTraces, setSelectedTraces] = useState>([]); + const [state, setState] = useState({ + name: '', + description: '' + }); + + useEffect(() => { + chrome.setBreadcrumbs( + [ + parentBreadcrumb, + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: 'Create', + href: '#/application_analytics/create', + }, + ]); + }, []) + + const closeFlyout = () => { + setIsFlyoutVisible(false); + }; + + let flyout; + if (isFlyoutVisible) { + flyout = ; + } + + const onChange = (e: ChangeEvent) => { + setState({ + ...state, + [e.target.name]: e.target.value + }); + }; + + const isDisabled = !state.name || !query || !selectedTraces.length || !selectedServices.length; + + const missingField = () => { + if (isDisabled) { + let popoverContent = ''; + if (!state.name) { + popoverContent = 'Name is required.' + } else if (!query) { + popoverContent = 'Log Source is required.' + } else if (!selectedServices.length) { + popoverContent = 'Services & Entities is required.' + } else if (!selectedTraces.length) { + popoverContent = 'Trace Groups are required.' + } + return

{popoverContent}

; + } + }; + + return ( +
+ + + + + +

Create application

+
+
+
+ + + + +

Application information

+
+
+
+ + + + onChange(e)} + /> + + + onChange(e)} + /> + + +
+ + + + + +

Composition

+
+
+
+ + + + + + +
+ + + + + Cancel + + + + + + Create + + + + +
+
+ {flyout} +
+ ); +} diff --git a/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx b/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx new file mode 100644 index 000000000..c9d54f9c2 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/components/helpers/modal_containers.tsx @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiOverlayMask, + EuiConfirmModal, +} from '@elastic/eui'; + +/* The file contains helper functions for modal layouts + * getDeleteModal - returns a confirm-modal with clear option + */ + +export const getClearModal = ( + onCancel: ( + event?: React.KeyboardEvent | React.MouseEvent + ) => void, + onConfirm: (event?: React.MouseEvent) => void, + title: string, + message: string, + confirmMessage?: string +) => { + return ( + + + {message} + + + ); +}; \ No newline at end of file diff --git a/dashboards-observability/public/components/application_analytics/home.tsx b/dashboards-observability/public/components/application_analytics/home.tsx new file mode 100644 index 000000000..85d7d8466 --- /dev/null +++ b/dashboards-observability/public/components/application_analytics/home.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + +import React, { useEffect, useState } from 'react'; +import { AppTable } from './components/app_table'; +import { Application } from './components/application'; +import { CreateApp } from './components/create' +import { Route, RouteComponentProps, Switch } from 'react-router'; +import { TraceAnalyticsComponentDeps, TraceAnalyticsCoreDeps } from '../trace_analytics/home'; +import { FilterType } from '../trace_analytics/components/common/filters/filters'; +import DSLService from 'public/services/requests/dsl'; +import PPLService from 'public/services/requests/ppl'; +import SavedObjects from 'public/services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; +import { handleIndicesExistRequest } from '../trace_analytics/requests/request_handler'; +import { ObservabilitySideBar } from '../common/side_nav'; +import { NotificationsStart } from '../../../../../src/core/public'; + +export interface AppAnalyticsCoreDeps extends TraceAnalyticsCoreDeps {} + +interface HomeProps extends RouteComponentProps, AppAnalyticsCoreDeps { + pplService: PPLService; + dslService: DSLService; + savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + notifications: NotificationsStart; +} + +export interface AppAnalyticsComponentDeps extends TraceAnalyticsComponentDeps {} + +export type ApplicationType = { + name: string; + id: string; + composition: string; + currentAvailability: string; + availabilityMetrics: string; + dateCreated: string; + dateModified: string; +}; + +const dateString = new Date().toISOString(); + +const dummyApplication: ApplicationType[] = [{ + name: "Cool Application", + id: "id", + composition: "Payment, user_db", + currentAvailability: "Available", + availabilityMetrics: "Error rate: 0.80%, Throughput: 0.94%, Latency: 600ms", + dateCreated: dateString, + dateModified: dateString +}]; + +export const Home = (props: HomeProps) => { + const { pplService, dslService, timestampUtils, savedObjects, parentBreadcrumb, http, chrome, notifications } = props; + const [indicesExist, setIndicesExist] = useState(true); + const storedFilters = sessionStorage.getItem('AppAnalyticsFilters'); + const [query, setQuery] = useState(sessionStorage.getItem('AppAnalyticsQuery') || ''); + const [filters, setFilters] = useState( + storedFilters ? JSON.parse(storedFilters) : [] + ); + const [startTime, setStartTime] = useState( + sessionStorage.getItem('AppAnalyticsStartTime') || 'now-24h' + ); + const [endTime, setEndTime] = useState( + sessionStorage.getItem('AppAnalyticsEndTime') || 'now' + ); + + const setFiltersWithStorage = (newFilters: FilterType[]) => { + setFilters(newFilters); + sessionStorage.setItem('AppAnalyticsFilters', JSON.stringify(newFilters)); + }; + const setQueryWithStorage = (newQuery: string) => { + setQuery(newQuery); + sessionStorage.setItem('AppAnalyticsQuery', newQuery); + }; + const setStartTimeWithStorage = (newStartTime: string) => { + setStartTime(newStartTime); + sessionStorage.setItem('AppAnalyticsStartTime', newStartTime); + }; + const setEndTimeWithStorage = (newEndTime: string) => { + setEndTime(newEndTime); + sessionStorage.setItem('AppAnalyticsEndTime', newEndTime); + }; + + useEffect(() => { + handleIndicesExistRequest(http, setIndicesExist); + }, []); + + const commonProps: AppAnalyticsComponentDeps = { + parentBreadcrumb: parentBreadcrumb, + http: http, + chrome: chrome, + query, + setQuery: setQueryWithStorage, + filters, + setFilters: setFiltersWithStorage, + startTime, + setStartTime: setStartTimeWithStorage, + endTime, + setEndTime: setEndTimeWithStorage, + indicesExist, + }; + + return ( +
+ + + + + + } + /> + + + } + /> + + + } + /> + +
+ ) +}; diff --git a/dashboards-observability/public/components/common/search/autocomplete.test.tsx b/dashboards-observability/public/components/common/search/autocomplete.test.tsx index e5b740d07..94b3ecdfb 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.test.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.test.tsx @@ -37,7 +37,7 @@ describe('renders autocomplete', function () { /> ); - const searchBar = utils.getByPlaceholderText('Enter PPL query to retrieve logs'); + const searchBar = utils.getByPlaceholderText('Enter PPL query'); it('handles query change', () => { act(() => { diff --git a/dashboards-observability/public/components/common/search/autocomplete.tsx b/dashboards-observability/public/components/common/search/autocomplete.tsx index a42b47ce7..410bcd9c6 100644 --- a/dashboards-observability/public/components/common/search/autocomplete.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.tsx @@ -106,7 +106,7 @@ export const Autocomplete = (props: IQueryBarProps) => { {...autocomplete.getInputProps({ id: 'autocomplete-textarea', "data-test-subj": "searchAutocompleteTextArea", - placeholder: 'Enter PPL query to retrieve logs', + placeholder: 'Enter PPL query', inputElement: null })} /> diff --git a/dashboards-observability/public/components/common/search/autocomplete_logic.ts b/dashboards-observability/public/components/common/search/autocomplete_logic.ts new file mode 100644 index 000000000..09b1c58a5 --- /dev/null +++ b/dashboards-observability/public/components/common/search/autocomplete_logic.ts @@ -0,0 +1,267 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getDataValueQuery } from './queries/data_queries'; +import DSLService from 'public/services/requests/dsl'; +import { firstCommand, statsCommands, numberTypes, pipeCommands, dataItem, fieldItem, indexItem, AutocompleteItem } from '../../../../common/constants/autocomplete'; + +let currIndex: string = ''; +let currField: string = ''; +let currFieldType: string = ''; + +let inFieldsCommaLoop: boolean = false; +let inMatch: boolean = false; +let nextWhere: number = Number.MAX_SAFE_INTEGER; +let nextStats: number = Number.MAX_SAFE_INTEGER; + +const indexList: string[] = []; +const fieldList: string[] = []; +const fieldsFromBackend: fieldItem[] = []; +const indicesFromBackend: indexItem[] = []; +const dataValuesFromBackend: dataItem[] = []; + +const getIndices = async (dslService: DSLService): Promise => { + if (indicesFromBackend.length === 0) { + const indices = (await dslService.fetchIndices()).filter(({ index } : { index: any }) => !index.startsWith('.')); + for (let i = 0; i < indices.length; i++) { + indicesFromBackend.push({ + label: indices[i].index, + }); + indexList.push(indices[i].index); + } + } +}; + +const getFields = async (dslService: DSLService): Promise => { + if (currIndex !== '') { + const res = await dslService.fetchFields(currIndex); + fieldsFromBackend.length = 0; + for (const element in res?.[currIndex].mappings.properties) { + if (res?.[currIndex].mappings.properties[element].properties || res?.[currIndex].mappings.properties[element].fields) { + fieldsFromBackend.push({ label: element, type: 'string' }); + } else if (res?.[currIndex].mappings.properties[element].type === 'keyword') { + fieldsFromBackend.push({ label: element, type: 'string' }); + } else { + fieldsFromBackend.push({ + label: element, + type: res?.[currIndex].mappings.properties[element].type, + }); + } + fieldList.push(element); + } + } +}; + +const getDataValues = async ( + index: string, + field: string, + fieldType: string, + dslService: DSLService +): Promise => { + const res = (await dslService.fetch(getDataValueQuery(index, field)))?.aggregations?.top_tags?.buckets || []; + dataValuesFromBackend.length = 0; + res.forEach((e: any) => { + if (fieldType === 'string') { + dataValuesFromBackend.push({ label: '"' + e.key + '"', doc_count: e.doc_count }); + } else if (fieldType === 'boolean') { + if (e.key === 1) { + dataValuesFromBackend.push({ label: 'True', doc_count: e.doc_count }); + } else { + dataValuesFromBackend.push({ label: 'False', doc_count: e.doc_count }); + } + } else if (fieldType !== 'geo_point') { + dataValuesFromBackend.push({ label: String(e.key), doc_count: e.doc_count }); + } + }); + return dataValuesFromBackend; +}; + +export const onItemSelect = async ({ setQuery, item }: { setQuery: any, item: any }, dslService: DSLService) => { + if (fieldsFromBackend.length === 0 && indexList.includes(item.itemName)) { + currIndex = item.itemName; + getFields(dslService); + } + setQuery(item.label + ' '); +}; + +// Function to create the array of objects to be suggested +const fillSuggestions = (str: string, word: string, items: any): AutocompleteItem[] => { + const lowerWord = word.toLowerCase(); + const filteredList = items.filter( + (item: { label: string }) => item.label.toLowerCase().startsWith(lowerWord) && lowerWord.localeCompare(item.label.toLowerCase()) + ); + const suggestionList = []; + for (let i = 0; i < filteredList.length; i++) { + suggestionList.push({ + label: str.substring(0, str.lastIndexOf(word)) + filteredList[i].label, + input: str, + suggestion: filteredList[i].label.substring(word.length), + itemName: filteredList[i].label, + }); + } + return suggestionList; +}; + +// Function for the first command in query, also needs to get available indices +const getFirstPipe = async (str: string, dslService: DSLService): Promise => { + const splittedModel = str.split(' '); + const prefix = splittedModel[splittedModel.length - 1]; + getIndices(dslService); + return fillSuggestions(str, prefix, firstCommand); +}; + +// Main logic behind autocomplete (Based on most recent inputs) +export const getSuggestions = async (str: string, dslService: DSLService): Promise => { + const splittedModel = str.split(' '); + const prefix = splittedModel[splittedModel.length - 1]; + const lowerPrefix = prefix.toLowerCase(); + const fullSuggestions: AutocompleteItem[] = []; + + // Check the last full word in the query, then suggest inputs based off that + if (splittedModel.length === 1) { + currField = ''; + currIndex = ''; + return getFirstPipe(str, dslService); + } else if (splittedModel.length > 1) { + if (splittedModel[splittedModel.length - 2] === '|') { + inFieldsCommaLoop = false; + inMatch = false; + nextWhere = Number.MAX_SAFE_INTEGER; + nextStats = Number.MAX_SAFE_INTEGER; + currField = ''; + currFieldType = ''; + return fillSuggestions(str, prefix, pipeCommands); + } else if (splittedModel[splittedModel.length - 2].includes(',')) { + if (inFieldsCommaLoop) { + return fillSuggestions(str, prefix, fieldsFromBackend); + } + if (inMatch) { + inMatch = true; + return fillSuggestions( + str, + prefix, + dataValuesFromBackend + ); + } + return fullSuggestions; + } else if ( + splittedModel[splittedModel.length - 2] === 'source' + ) { + return [{ label: str + '=', input: str, suggestion: '=', itemName: '=' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if ( + (splittedModel.length > 2 && splittedModel[splittedModel.length - 3] === 'source') + ) { + return fillSuggestions(str, prefix, indicesFromBackend); + } else if (indexList.includes(splittedModel[splittedModel.length - 2])) { + currIndex = splittedModel[splittedModel.length - 2]; + getFields(dslService); + return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if (splittedModel[splittedModel.length - 2] === 'stats') { + nextStats = splittedModel.length; + return fillSuggestions(str, prefix, statsCommands); + } else if (nextStats === splittedModel.length - 1) { + if (statsCommands.filter(c => c.label === splittedModel[splittedModel.length - 2]).length > 0) { + if (splittedModel[splittedModel.length - 2] === 'count()') { + return [ + { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else { + const numberFields = fieldsFromBackend.filter( + (field: { label: string, type: string }) => + field.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(field.label.toLowerCase()) && numberTypes.includes(field.type) + ); + for (let i = 0; i < numberFields.length; i++) { + var field: {label: string} = numberFields[i]; + fullSuggestions.push({ + label: str.substring(0, str.lastIndexOf(prefix)) + field.label + ' )', + input: str, + suggestion: field.label.substring(prefix.length) + ' )', + itemName: field.label + ' )', + }); + } + return fullSuggestions; + } + } + } else if (nextStats === splittedModel.length - 2 && splittedModel[splittedModel.length - 3] === 'count()') { + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (nextStats === splittedModel.length - 3) { + if (splittedModel[splittedModel.length - 3] === 'by') { + return [ + { label: str + '|', input: str, suggestion: '|', itemName: '|' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else { + return [ + { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' }, + { label: str + '|', input: str, suggestion: '|', itemName: '|' } + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } + } else if (nextStats === splittedModel.length - 4) { + return fillSuggestions(str, prefix, fieldsFromBackend); + } + else if (splittedModel[splittedModel.length - 2] === 'fields') { + inFieldsCommaLoop = true; + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (splittedModel[splittedModel.length - 2] === 'dedup') { + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (splittedModel[splittedModel.length - 2] === 'where') { + nextWhere = splittedModel.length; + return fillSuggestions(str, prefix, [{label: 'match('}, ...fieldsFromBackend]); + } else if (splittedModel[splittedModel.length - 2] === 'match(') { + inMatch = true; + return fillSuggestions(str, prefix, fieldsFromBackend); + } else if (nextWhere === splittedModel.length - 1) { + fullSuggestions.push({ + label: str + '=', + input: str, + suggestion: '=', + itemName: '=', + }); + currField = splittedModel[splittedModel.length - 2]; + currFieldType = fieldsFromBackend.find((field: {label: string, type: string}) => field.label === currField)?.type || ''; + await getDataValues(currIndex, currField, currFieldType, dslService); + return fullSuggestions.filter((suggestion: { label: string }) => suggestion.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(suggestion.label.toLowerCase())); + } else if (inMatch && fieldList.includes(splittedModel[splittedModel.length - 2])) { + currField = splittedModel[splittedModel.length - 2]; + currFieldType = fieldsFromBackend.find((field) => field.label === currField)?.type || ''; + await getDataValues(currIndex, currField, currFieldType, dslService); + return [{ label: str + ',', input: str, suggestion: ',', itemName: ','}].filter( + ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion + ); + } else if (nextWhere === splittedModel.length - 2) { + return fillSuggestions( + str, + prefix, + dataValuesFromBackend + ); + } else if (nextWhere === splittedModel.length - 3 || nextStats === splittedModel.length - 5 || nextWhere === splittedModel.length - 5) { + return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( + ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) + ); + } else if (inFieldsCommaLoop) { + return [ + { + label: str.substring(0, str.length - 1) + ',', + input: str.substring(0, str.length - 1), + suggestion: ',', + itemName: ',', + }, + { label: str + '|', input: str, suggestion: '|', itemName: '|' }, + ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase())); + } else if (inMatch) { + inMatch = false; + return [{ label: str + ')', input: str, suggestion: ')', itemName: ')' }].filter( + ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion + ); + } + return []; + } +}; diff --git a/dashboards-observability/public/components/common/search/autocomplete_logic.tsx b/dashboards-observability/public/components/common/search/autocomplete_logic.tsx deleted file mode 100644 index 375bd77dd..000000000 --- a/dashboards-observability/public/components/common/search/autocomplete_logic.tsx +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { getDataValueQuery } from './queries/data_queries'; -import DSLService from 'public/services/requests/dsl'; -import { firstCommand, statsCommands, numberTypes, pipeCommands, dataItem, fieldItem, indexItem, AutocompleteItem } from '../../../../common/constants/autocomplete'; - -let currIndex: string = ''; -let currField: string = ''; -let currFieldType: string = ''; - -let inFieldsCommaLoop: boolean = false; -let inMatch: boolean = false; -let nextWhere: number = Number.MAX_SAFE_INTEGER; -let nextStats: number = Number.MAX_SAFE_INTEGER; - -const indexList: string[] = []; -const fieldList: string[] = []; -const fieldsFromBackend: fieldItem[] = []; -const indicesFromBackend: indexItem[] = []; -const dataValuesFromBackend: dataItem[] = []; - -const getIndices = async (dslService: DSLService): Promise => { - if (indicesFromBackend.length === 0) { - const indices = (await dslService.fetchIndices()).filter(({ index } : { index: any }) => !index.startsWith('.')); - for (let i = 0; i < indices.length; i++) { - indicesFromBackend.push({ - label: indices[i].index, - }); - indexList.push(indices[i].index); - } - } - }; - - const getFields = async (dslService: DSLService): Promise => { - if (currIndex !== '') { - const res = await dslService.fetchFields(currIndex); - fieldsFromBackend.length = 0; - for (const element in res?.[currIndex].mappings.properties) { - if (res?.[currIndex].mappings.properties[element].properties || res?.[currIndex].mappings.properties[element].fields) { - fieldsFromBackend.push({ label: element, type: 'string' }); - } else if (res?.[currIndex].mappings.properties[element].type === 'keyword') { - fieldsFromBackend.push({ label: element, type: 'string' }); - } else { - fieldsFromBackend.push({ - label: element, - type: res?.[currIndex].mappings.properties[element].type, - }); - } - fieldList.push(element); - } - } - }; - - const getDataValues = async ( - index: string, - field: string, - fieldType: string, - dslService: DSLService - ): Promise => { - const res = (await dslService.fetch(getDataValueQuery(index, field)))?.aggregations?.top_tags?.buckets || []; - dataValuesFromBackend.length = 0; - res.forEach((e: any) => { - if (fieldType === 'string') { - dataValuesFromBackend.push({ label: '"' + e.key + '"', doc_count: e.doc_count }); - } else if (fieldType === 'boolean') { - if (e.key === 1) { - dataValuesFromBackend.push({ label: 'True', doc_count: e.doc_count }); - } else { - dataValuesFromBackend.push({ label: 'False', doc_count: e.doc_count }); - } - } else if (fieldType !== 'geo_point') { - dataValuesFromBackend.push({ label: String(e.key), doc_count: e.doc_count }); - } - }); - return dataValuesFromBackend; - }; - -export const onItemSelect = async ({ setQuery, item }: { setQuery: any, item: any }, dslService: DSLService) => { - if (fieldsFromBackend.length === 0 && indexList.includes(item.itemName)) { - currIndex = item.itemName; - getFields(dslService); - } - setQuery(item.label + ' '); - }; - -// Function to create the array of objects to be suggested - const fillSuggestions = (str: string, word: string, items: any): AutocompleteItem[] => { - const lowerWord = word.toLowerCase(); - const filteredList = items.filter( - (item: { label: string }) => item.label.toLowerCase().startsWith(lowerWord) && lowerWord.localeCompare(item.label.toLowerCase()) - ); - const suggestionList = []; - for (let i = 0; i < filteredList.length; i++) { - suggestionList.push({ - label: str.substring(0, str.lastIndexOf(word)) + filteredList[i].label, - input: str, - suggestion: filteredList[i].label.substring(word.length), - itemName: filteredList[i].label, - }); - } - return suggestionList; - }; - - // Function for the first command in query, also needs to get available indices - const getFirstPipe = async (str: string, dslService: DSLService): Promise => { - const splittedModel = str.split(' '); - const prefix = splittedModel[splittedModel.length - 1]; - getIndices(dslService); - return fillSuggestions(str, prefix, firstCommand); - }; - - // Main logic behind autocomplete (Based on most recent inputs) - export const getSuggestions = async (str: string, dslService: DSLService): Promise => { - const splittedModel = str.split(' '); - const prefix = splittedModel[splittedModel.length - 1]; - const lowerPrefix = prefix.toLowerCase(); - const fullSuggestions: AutocompleteItem[] = []; - - // Check the last full word in the query, then suggest inputs based off that - if (splittedModel.length === 1) { - currField = ''; - currIndex = ''; - return getFirstPipe(str, dslService); - } else if (splittedModel.length > 1) { - if (splittedModel[splittedModel.length - 2] === '|') { - inFieldsCommaLoop = false; - inMatch = false; - nextWhere = Number.MAX_SAFE_INTEGER; - nextStats = Number.MAX_SAFE_INTEGER; - currField = ''; - currFieldType = ''; - return fillSuggestions(str, prefix, pipeCommands); - } else if (splittedModel[splittedModel.length - 2].includes(',')) { - if (inFieldsCommaLoop) { - return fillSuggestions(str, prefix, fieldsFromBackend); - } - if (inMatch) { - inMatch = true; - return fillSuggestions( - str, - prefix, - dataValuesFromBackend - ); - } - return fullSuggestions; - } else if ( - splittedModel[splittedModel.length - 2] === 'source' - ) { - return [{ label: str + '=', input: str, suggestion: '=', itemName: '=' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if ( - (splittedModel.length > 2 && splittedModel[splittedModel.length - 3] === 'source') - ) { - return fillSuggestions(str, prefix, indicesFromBackend); - } else if (indexList.includes(splittedModel[splittedModel.length - 2])) { - currIndex = splittedModel[splittedModel.length - 2]; - getFields(dslService); - return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if (splittedModel[splittedModel.length - 2] === 'stats') { - nextStats = splittedModel.length; - return fillSuggestions(str, prefix, statsCommands); - } else if (nextStats === splittedModel.length - 1) { - if (statsCommands.filter(c => c.label === splittedModel[splittedModel.length - 2]).length > 0) { - if (splittedModel[splittedModel.length - 2] === 'count()') { - return [ - { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else { - const numberFields = fieldsFromBackend.filter( - (field: { label: string, type: string }) => - field.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(field.label.toLowerCase()) && numberTypes.includes(field.type) - ); - for (let i = 0; i < numberFields.length; i++) { - var field: {label: string} = numberFields[i]; - fullSuggestions.push({ - label: str.substring(0, str.lastIndexOf(prefix)) + field.label + ' )', - input: str, - suggestion: field.label.substring(prefix.length) + ' )', - itemName: field.label + ' )', - }); - } - return fullSuggestions; - } - } - } else if (nextStats === splittedModel.length - 2 && splittedModel[splittedModel.length - 3] === 'count()') { - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (nextStats === splittedModel.length - 3) { - if (splittedModel[splittedModel.length - 3] === 'by') { - return [ - { label: str + '|', input: str, suggestion: '|', itemName: '|' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else { - return [ - { label: str + 'by', input: str, suggestion: 'by'.substring(prefix.length), itemName: 'by' }, - { label: str + '|', input: str, suggestion: '|', itemName: '|' } - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } - } else if (nextStats === splittedModel.length - 4) { - return fillSuggestions(str, prefix, fieldsFromBackend); - } - else if (splittedModel[splittedModel.length - 2] === 'fields') { - inFieldsCommaLoop = true; - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (splittedModel[splittedModel.length - 2] === 'dedup') { - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (splittedModel[splittedModel.length - 2] === 'where') { - nextWhere = splittedModel.length; - return fillSuggestions(str, prefix, [{label: 'match('}, ...fieldsFromBackend]); - } else if (splittedModel[splittedModel.length - 2] === 'match(') { - inMatch = true; - return fillSuggestions(str, prefix, fieldsFromBackend); - } else if (nextWhere === splittedModel.length - 1) { - fullSuggestions.push({ - label: str + '=', - input: str, - suggestion: '=', - itemName: '=', - }); - currField = splittedModel[splittedModel.length - 2]; - currFieldType = fieldsFromBackend.find((field: {label: string, type: string}) => field.label === currField)?.type || ''; - await getDataValues(currIndex, currField, currFieldType, dslService); - return fullSuggestions.filter((suggestion: { label: string }) => suggestion.label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(suggestion.label.toLowerCase())); - } else if (inMatch && fieldList.includes(splittedModel[splittedModel.length - 2])) { - currField = splittedModel[splittedModel.length - 2]; - currFieldType = fieldsFromBackend.find((field) => field.label === currField)?.type || ''; - await getDataValues(currIndex, currField, currFieldType, dslService); - return [{ label: str + ',', input: str, suggestion: ',', itemName: ','}].filter( - ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion - ); - } else if (nextWhere === splittedModel.length - 2) { - return fillSuggestions( - str, - prefix, - dataValuesFromBackend - ); - } else if (nextWhere === splittedModel.length - 3 || nextStats === splittedModel.length - 5 || nextWhere === splittedModel.length - 5) { - return [{ label: str + '|', input: str, suggestion: '|', itemName: '|' }].filter( - ({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase()) - ); - } else if (inFieldsCommaLoop) { - return [ - { - label: str.substring(0, str.length - 1) + ',', - input: str.substring(0, str.length - 1), - suggestion: ',', - itemName: ',', - }, - { label: str + '|', input: str, suggestion: '|', itemName: '|' }, - ].filter(({ label }) => label.toLowerCase().startsWith(lowerPrefix) && lowerPrefix.localeCompare(label.toLowerCase())); - } else if (inMatch) { - inMatch = false; - return [{ label: str + ')', input: str, suggestion: ')', itemName: ')' }].filter( - ({ suggestion }) => suggestion.startsWith(prefix) && prefix !== suggestion - ); - } - return []; - } - }; - \ No newline at end of file diff --git a/dashboards-observability/public/components/common/search/search.test.tsx b/dashboards-observability/public/components/common/search/search.test.tsx index 37c934386..74dcd1a0a 100644 --- a/dashboards-observability/public/components/common/search/search.test.tsx +++ b/dashboards-observability/public/components/common/search/search.test.tsx @@ -48,7 +48,7 @@ describe('Search bar', () => { /> ); - const searchBar = utils.getByPlaceholderText('Enter PPL query to retrieve logs'); + const searchBar = utils.getByPlaceholderText('Enter PPL query'); fireEvent.change(searchBar, { target: { value: 'new query' } }); expect(handleQueryChange).toBeCalledWith('new query'); }); diff --git a/dashboards-observability/public/components/common/side_nav.tsx b/dashboards-observability/public/components/common/side_nav.tsx index 83129f315..c00a3e763 100644 --- a/dashboards-observability/public/components/common/side_nav.tsx +++ b/dashboards-observability/public/components/common/side_nav.tsx @@ -32,7 +32,7 @@ export function ObservabilitySideBar(props: { children: React.ReactNode }) { // Default page is Events Analytics // But it is kept as second option in side nav if (hash === '#/') { - items[0].items[1].isSelected = true; + items[0].items[2].isSelected = true; return true; } for (let i = 0; i < items.length; i++) { @@ -51,6 +51,11 @@ export function ObservabilitySideBar(props: { children: React.ReactNode }) { name: 'Observability', id: 0, items: [ + { + name: 'Application analytics', + id: 1, + href: '#/application_analytics', + }, { name: 'Trace analytics', id: 1, diff --git a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx index 3b4d2d569..185fc3beb 100644 --- a/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx @@ -28,7 +28,7 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import React, { CSSProperties, ReactElement, useEffect, useState } from 'react'; +import React, { ReactElement, useEffect, useState } from 'react'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; import { CREATE_PANEL_MESSAGE, @@ -40,12 +40,7 @@ import moment from 'moment'; import _ from 'lodash'; import { CustomPanelListType } from '../../../common/types/custom_panels'; import { getSampleDataModal } from '../common/helpers/add_sample_modal'; - -const pageStyles: CSSProperties = { - float: 'left', - width: '100%', - maxWidth: '1130px', -}; +import { pageStyles } from '../../../common/constants/shared'; /* * "CustomPanelTable" module, used to view all the saved panels diff --git a/dashboards-observability/public/components/explorer/event_analytics.tsx b/dashboards-observability/public/components/explorer/event_analytics.tsx index 248bdcacd..2b90c152d 100644 --- a/dashboards-observability/public/components/explorer/event_analytics.tsx +++ b/dashboards-observability/public/components/explorer/event_analytics.tsx @@ -5,6 +5,7 @@ import { EuiGlobalToastList } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { EmptyTabParams } from 'common/types/explorer'; import { isEmpty } from 'lodash'; import React, { ReactChild, useState } from 'react'; import { HashRouter, Route, Switch, useHistory } from 'react-router-dom'; @@ -37,10 +38,10 @@ export const EventAnalytics = ({ setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); }; - const getExistingEmptyTab = ({ tabIds, queries, explorerData }) => { + const getExistingEmptyTab = ({ tabIds, queries, explorerData }: EmptyTabParams) => { let emptyTabId = ''; - for (let i = 0; i < tabIds.length; i++) { - const tid = tabIds[i]; + for (let i = 0; i < tabIds!.length; i++) { + const tid = tabIds![i]; if (isEmpty(queries[tid][RAW_QUERY]) && isEmpty(explorerData[tid])) { emptyTabId = tid; break; @@ -80,7 +81,6 @@ export const EventAnalytics = ({ timestampUtils={timestampUtils} http={http} setToast={setToast} - chrome={chrome} getExistingEmptyTab={getExistingEmptyTab} history={history} notifications={notifications} @@ -106,10 +106,9 @@ export const EventAnalytics = ({ http={http} savedObjects={savedObjects} dslService={dslService} - timestampUtils={timestampUtils} + pplService={pplService} setToast={setToast} getExistingEmptyTab={getExistingEmptyTab} - history={history} /> ); diff --git a/dashboards-observability/public/components/explorer/explorer.tsx b/dashboards-observability/public/components/explorer/explorer.tsx index 3aa13d7e0..2eabe2637 100644 --- a/dashboards-observability/public/components/explorer/explorer.tsx +++ b/dashboards-observability/public/components/explorer/explorer.tsx @@ -72,7 +72,8 @@ export const Explorer = ({ setToast, history, notifications, - savedObjectId + savedObjectId, + tabCreatedTypes }: IExplorerProps) => { const dispatch = useDispatch(); const requestParams = { tabId, }; diff --git a/dashboards-observability/public/components/explorer/home.tsx b/dashboards-observability/public/components/explorer/home.tsx index 96c1b1ce9..9ff7aff01 100644 --- a/dashboards-observability/public/components/explorer/home.tsx +++ b/dashboards-observability/public/components/explorer/home.tsx @@ -86,8 +86,8 @@ export const Home = (props: IHomeProps) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedDateRange, setSelectedDateRange] = useState>(['now-15m', 'now']); - const [savedHistories, setSavedHistories] = useState([]); - const [selectedHisotries, setSelectedHisotries] = useState([]); + const [savedHistories, setSavedHistories] = useState>([]); + const [selectedHistories, setSelectedHistories] = useState>([]); const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); const [isTableLoading, setIsTableLoading] = useState(false); const [modalLayout, setModalLayout] = useState(); @@ -113,7 +113,7 @@ export const Home = (props: IHomeProps) => { }; const deleteHistoryList = async () => { - const objectIdsToDelete = selectedHisotries.map((history) => history.data.objectId); + const objectIdsToDelete = selectedHistories.map((history) => history.data.objectId); await savedObjects .deleteSavedObjectsList({ objectIdList: objectIdsToDelete }) .then(async (res) => { @@ -264,7 +264,7 @@ export const Home = (props: IHomeProps) => { }); }); setToast(`Sample events added successfully.`); - } catch (error) { + } catch (error: any) { setToast(`Cannot add sample events data, error: ${error}`, 'danger'); console.error(error.body.message); } finally { @@ -284,13 +284,13 @@ export const Home = (props: IHomeProps) => { ); const deleteHistory = () => { - const customPanelString = `${selectedHisotries.length > 1 ? 'histories' : 'history'}`; + const customPanelString = `${selectedHistories.length > 1 ? 'histories' : 'history'}`; setModalLayout( ); showModal(); @@ -299,7 +299,7 @@ export const Home = (props: IHomeProps) => { const popoverItems: ReactElement[] = [ { setIsActionsPopoverOpen(false); deleteHistory(); @@ -408,8 +408,8 @@ export const Home = (props: IHomeProps) => { savedHistories={savedHistories} handleHistoryClick={handleHistoryClick} isTableLoading={isTableLoading} - handleSelectHistory={setSelectedHisotries} - selectedHisotries={selectedHisotries} + handleSelectHistory={setSelectedHistories} + selectedHistories={selectedHistories} /> ) : ( <> diff --git a/dashboards-observability/public/components/explorer/home_table/history_table.tsx b/dashboards-observability/public/components/explorer/home_table/history_table.tsx index c8912f9f0..128384419 100644 --- a/dashboards-observability/public/components/explorer/home_table/history_table.tsx +++ b/dashboards-observability/public/components/explorer/home_table/history_table.tsx @@ -16,6 +16,7 @@ interface TableData { handleHistoryClick: (objectId: string) => void; handleSelectHistory: (selectedHistories: Array) => void; isTableLoading: boolean; + selectedHistories: Array; } export function Histories({ @@ -26,9 +27,9 @@ export function Histories({ }: TableData) { const [pageIndex, setPageIndex] = useState(0); const [pageSize, setPageSize] = useState(10); - const pageIndexRef = useRef(); + const pageIndexRef = useRef(); pageIndexRef.current = pageIndex; - const pageSizeRef = useRef(); + const pageSizeRef = useRef(); pageSizeRef.current = pageSize; const onTableChange = ({ page = {} }) => { @@ -44,7 +45,7 @@ export function Histories({ name: '', sortable: true, width: '40px', - render: (item) => { + render: (item: any) => { if (item == 'Visualization') { return (
@@ -66,7 +67,7 @@ export function Histories({ width: '70%', sortable: true, truncateText: true, - render: (item) => { + render: (item: any) => { return ( { diff --git a/dashboards-observability/public/components/explorer/log_explorer.tsx b/dashboards-observability/public/components/explorer/log_explorer.tsx index 91a9578c1..4d8cf3a07 100644 --- a/dashboards-observability/public/components/explorer/log_explorer.tsx +++ b/dashboards-observability/public/components/explorer/log_explorer.tsx @@ -36,6 +36,7 @@ export const LogExplorer = ({ getExistingEmptyTab, history, notifications, + http }: ILogExplorerProps) => { const dispatch = useDispatch(); @@ -178,17 +179,18 @@ export const LogExplorer = ({ <> + tabCreatedTypes={tabCreatedTypes} + http={http} + /> ) }; } diff --git a/dashboards-observability/public/components/notebooks/components/note_table.tsx b/dashboards-observability/public/components/notebooks/components/note_table.tsx index 8d38bcbe4..c3810c11c 100644 --- a/dashboards-observability/public/components/notebooks/components/note_table.tsx +++ b/dashboards-observability/public/components/notebooks/components/note_table.tsx @@ -27,7 +27,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import CSS from 'csstype'; import _ from 'lodash'; import moment from 'moment'; import React, { ReactElement, useEffect, useState } from 'react'; @@ -43,12 +42,7 @@ import { getSampleNotebooksModal, } from './helpers/modal_containers'; import { NotebookType } from './main'; - -const pageStyles: CSS.Properties = { - float: 'left', - width: '100%', - maxWidth: '1130px', -}; +import { pageStyles } from '../../../../common/constants/shared'; type NoteTableProps = { loading: boolean; diff --git a/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx index 15eaf5a33..1410bfea0 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx @@ -13,22 +13,23 @@ import { import _ from 'lodash'; import React from 'react'; -const getFields = (page: 'dashboard' | 'traces' | 'services') => +const getFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => ({ dashboard: ['traceGroup', 'serviceName', 'error', 'status.message', 'latency'], traces: ['traceId', 'traceGroup', 'serviceName', 'error', 'status.message', 'latency'], services: ['traceGroup', 'serviceName', 'error', 'status.message', 'latency'], + app: ['traceId', 'traceGroup', 'serviceName'], }[page]); // filters will take effect and can be manually added -export const getFilterFields = (page: 'dashboard' | 'traces' | 'services') => getFields(page); +export const getFilterFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => getFields(page); // filters will take effect -export const getValidFilterFields = (page: 'dashboard' | 'traces' | 'services') => { +export const getValidFilterFields = (page: 'dashboard' | 'traces' | 'services' | 'app') => { const fields = getFields(page); if (page !== 'services') return [...fields, 'Latency percentile within trace group']; return fields; }; -const getType = (field: string): string => { +const getType = (field: string): string | null => { const typeMapping = { attributes: { host: { @@ -106,7 +107,7 @@ export const getOperatorOptions = (field: string) => { }; const operators = [ ...operatorMapping.default_first, - ...operatorMapping[type], + ..._.get(operatorMapping, type), ...operatorMapping.default_last, ]; return operators; diff --git a/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx index 7fdf9fed2..af3870626 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx @@ -36,7 +36,7 @@ export interface FiltersProps { } interface FiltersOwnProps extends FiltersProps { - page: 'dashboard' | 'traces' | 'services'; + page: 'dashboard' | 'traces' | 'services' | 'app'; } export function Filters(props: FiltersOwnProps) { diff --git a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx index ec876522e..d1dfe5a61 100644 --- a/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx @@ -47,7 +47,7 @@ export interface SearchBarProps extends FiltersProps { interface SearchBarOwnProps extends SearchBarProps { refresh: () => void; - page: 'dashboard' | 'traces' | 'services'; + page: 'dashboard' | 'traces' | 'services' | 'app'; datepickerOnly?: boolean; } diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx index e306f0458..58c569d45 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx @@ -23,6 +23,7 @@ describe('Dashboard table component', () => { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={false} + page="dashboard" /> ); @@ -59,6 +60,7 @@ describe('Dashboard table component', () => { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={false} + page="dashboard" /> ); diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx index 642b56353..20a504a61 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx @@ -22,9 +22,14 @@ import { ThroughputPlt } from '../common/plots/throughput_plt'; import { SearchBar } from '../common/search_bar'; import { DashboardTable } from './dashboard_table'; -interface DashboardProps extends TraceAnalyticsComponentDeps {} +interface DashboardProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'dashboard' | 'traces' | 'services' | 'app'; +} export function Dashboard(props: DashboardProps) { + const { appId, appName, page, parentBreadcrumb } = props; const [tableItems, setTableItems] = useState([]); const [throughputPltItems, setThroughputPltItems] = useState({ items: [], fixedInterval: '1h' }); const [errorRatePltItems, setErrorRatePltItems] = useState({ items: [], fixedInterval: '1h' }); @@ -34,19 +39,35 @@ export function Dashboard(props: DashboardProps) { const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, + const breadCrumbs = page === 'app' ? + [ { - text: 'Trace analytics', - href: '#/trace_analytics/home', + text: 'Application analytics', + href: '#/application_analytics', }, { - text: 'Dashboards', - href: '#/trace_analytics/home', + text: `${appName}`, + href: `#/application_analytics/${appId}`, }, + ] : [ + { + text: 'Trace analytics', + href: '#/trace_analytics/home', + }, + { + text: 'Dashboards', + href: '#/trace_analytics/home', + }, + ] + + + useEffect(() => { + props.chrome.setBreadcrumbs( + [ + parentBreadcrumb, + ...breadCrumbs ]); - const validFilters = getValidFilterFields('dashboard'); + const validFilters = getValidFilterFields(page); props.setFilters([ ...props.filters.map((filter) => ({ ...filter, @@ -156,9 +177,13 @@ export function Dashboard(props: DashboardProps) { return ( <> + {page === 'app' ? + + :

Dashboard

+ } {props.indicesExist ? ( @@ -181,6 +206,7 @@ export function Dashboard(props: DashboardProps) { addPercentileFilter={addPercentileFilter} setRedirect={setRedirect} loading={loading} + page="dashboard" /> diff --git a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx index 5fe49c4e2..a334868e8 100644 --- a/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx @@ -33,6 +33,7 @@ export function DashboardTable(props: { addPercentileFilter: (condition?: 'gte' | 'lte', additionalFilters?: FilterType[]) => void; setRedirect: (redirect: boolean) => void; loading: boolean; + page: 'dashboard' | 'app'; }) { const getVarianceProps = (items: any[]) => { if (items.length === 0) { @@ -317,7 +318,7 @@ export function DashboardTable(props: { ), align: 'right', sortable: true, - render: (item, row) => ( + render: props.page === 'dashboard' ? (item, row) => ( { @@ -334,7 +335,7 @@ export function DashboardTable(props: { > - ), + ) : (item) => item }, ] as Array>; @@ -371,7 +372,7 @@ export function DashboardTable(props: { }; const varianceProps = useMemo(() => getVarianceProps(props.items), [props.items]); - const columns = useMemo(() => getColumns(), [props.items]); + const columns = useMemo(() => getColumns(), [props.items, props.filters]); const titleBar = useMemo(() => renderTitleBar(props.items?.length), [props.items]); const [sorting, setSorting] = useState<{ sort: PropertySort }>({ diff --git a/dashboards-observability/public/components/trace_analytics/components/services/services.tsx b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx index 706e618d5..e39fd4cc1 100644 --- a/dashboards-observability/public/components/trace_analytics/components/services/services.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx @@ -13,17 +13,30 @@ import { filtersToDsl } from '../common/helper_functions'; import { SearchBar } from '../common/search_bar'; import { ServicesTable } from './services_table'; -interface ServicesProps extends TraceAnalyticsComponentDeps {} +interface ServicesProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'dashboard' | 'traces' | 'services' | 'app'; +} export function Services(props: ServicesProps) { + const { appId, appName, parentBreadcrumb, page } = props; const [tableItems, setTableItems] = useState([]); const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, - { + const breadCrumbs = page === 'app' ? + [ + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: `${appName}`, + href: `#/application_analytics/${appId}`, + }, + ] : [ + { text: 'Trace analytics', href: '#/trace_analytics/home', }, @@ -31,6 +44,12 @@ export function Services(props: ServicesProps) { text: 'Services', href: '#/trace_analytics/services', }, + ] + + useEffect(() => { + props.chrome.setBreadcrumbs([ + parentBreadcrumb, + ...breadCrumbs ]); const validFilters = getValidFilterFields('services'); props.setFilters([ @@ -71,9 +90,13 @@ export function Services(props: ServicesProps) { return ( <> + {page==='app' ? + + :

Services

+ } { endTime="now" setEndTime={setEndTime} indicesExist={false} + page="traces" /> ); @@ -59,6 +59,7 @@ describe('Traces component', () => { endTime="now" setEndTime={setEndTime} indicesExist={true} + page="traces" /> ); diff --git a/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx index f06e48d86..bfe159bc9 100644 --- a/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx @@ -12,17 +12,30 @@ import { filtersToDsl } from '../common/helper_functions'; import { SearchBar } from '../common/search_bar'; import { TracesTable } from './traces_table'; -interface TracesProps extends TraceAnalyticsComponentDeps {} +interface TracesProps extends TraceAnalyticsComponentDeps { + appId?: string; + appName?: string; + page: 'traces' | 'app'; +} export function Traces(props: TracesProps) { + const { appId, appName, parentBreadcrumb, page } = props; const [tableItems, setTableItems] = useState([]); const [redirect, setRedirect] = useState(true); const [loading, setLoading] = useState(false); - useEffect(() => { - props.chrome.setBreadcrumbs([ - props.parentBreadcrumb, - { + const breadCrumbs = page === 'app' ? + [ + { + text: 'Application analytics', + href: '#/application_analytics', + }, + { + text: `${appName}`, + href: `#/application_analytics/${appId}`, + }, + ] : [ + { text: 'Trace analytics', href: '#/trace_analytics/home', }, @@ -30,6 +43,12 @@ export function Traces(props: TracesProps) { text: 'Traces', href: '#/trace_analytics/traces', }, + ] + + useEffect(() => { + props.chrome.setBreadcrumbs([ + parentBreadcrumb, + ...breadCrumbs ]); const validFilters = getValidFilterFields('traces'); props.setFilters([ @@ -55,9 +74,13 @@ export function Traces(props: TracesProps) { return ( <> + {page === 'app' ? + + :

Traces

+ } diff --git a/dashboards-observability/public/components/trace_analytics/home.tsx b/dashboards-observability/public/components/trace_analytics/home.tsx index ea198c8e7..f68e0edce 100644 --- a/dashboards-observability/public/components/trace_analytics/home.tsx +++ b/dashboards-observability/public/components/trace_analytics/home.tsx @@ -88,7 +88,7 @@ export const Home = (props: HomeProps) => { path={['/trace_analytics', '/trace_analytics/home']} render={(routerProps) => ( - + )} /> @@ -97,7 +97,7 @@ export const Home = (props: HomeProps) => { path="/trace_analytics/traces" render={(routerProps) => ( - + )} /> @@ -117,7 +117,7 @@ export const Home = (props: HomeProps) => { path="/trace_analytics/services" render={(routerProps) => ( - + )} /> diff --git a/dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts b/dashboards-observability/server/adaptors/application_analytics/application_adaptor.ts new file mode 100644 index 000000000..e69de29bb diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle index 74169b425..906a0aec8 100644 --- a/opensearch-observability/build.gradle +++ b/opensearch-observability/build.gradle @@ -9,7 +9,7 @@ import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask buildscript { ext { - opensearch_version = System.getProperty("opensearch.version", "1.2.0-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "1.2.1-SNAPSHOT") // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -254,7 +254,6 @@ testClusters.integTest { setting 'path.repo', repo.absolutePath } - String bwcVersion = "1.1.0-SNAPSHOT" String baseName = "obsBwcCluster" String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc/" @@ -263,7 +262,7 @@ String bwcFilePath = "src/test/kotlin/org/opensearch/observability/resources/bwc testClusters { "${baseName}$i" { testDistribution = "ARCHIVE" - versions = ["1.1.0","1.2.0-SNAPSHOT"] + versions = ["1.1.0","1.2.1-SNAPSHOT"] numberOfNodes = 3 plugin(provider(new Callable(){ @Override @@ -395,6 +394,24 @@ task bwcTestSuite(type: StandaloneRestIntegTestTask) { dependsOn tasks.named("${baseName}#fullRestartClusterTask") } +task integTestRemote(type: RestIntegTestTask) { + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + systemProperty 'tests.security.manager', 'false' + systemProperty 'java.io.tmpdir', opensearch_tmp_dir.absolutePath + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + + // Only rest case can run with remote cluster + if (System.getProperty("tests.rest.cluster") != null) { + filter { + includeTestsMatching "org.opensearch.observability.rest.*IT" + } + } +} + run { doFirst { // There seems to be an issue when running multi node run or integ tasks with unicast_hosts diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt new file mode 100644 index 000000000..d9f5f688d --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Application.kt @@ -0,0 +1,270 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.observability.model + +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.stringList +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Application main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "name": "Cool Application",
+ *   "description": "Application that includes multiple cool services",
+ *   "baseQuery": "source = opensearch_sample_database_flights",
+ *   "servicesEntities": [
+ *       "Payment",
+ *       "Users",
+ *       "Purchase"
+ *   ],
+ *   "traceGroups": [
+ *       "Payment.auto",
+ *       "Users.admin",
+ *       "Purchase.source"
+ *   ],
+ *   "availabilityLevels": [
+ *       {
+ *           "label": "Unavailable",
+ *           "color": "#D36086",
+ *           "condition": "when errorRate() is above or equal to 2%",
+ *           "order": "0",
+ *       }
+ *   ],
+ * }
+ * }
+ */ + +internal data class Application( + val name: String?, + val description: String?, + val baseQuery: String?, + val servicesEntities: List, + val traceGroups: List, + val availabilityLevels: List +) : BaseObjectData { + + internal companion object { + private val log by logger(Application::class.java) + private const val NAME_TAG = "name" + private const val DESCRIPTION_TAG = "description" + private const val BASE_QUERY_TAG = "baseQuery" + private const val SERVICES_ENTITIES_TAG = "servicesEntities" + private const val TRACE_GROUPS_TAG = "traceGroups" + private const val AVAILABILITY_LEVELS_TAG = "availabilityLevels" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Application(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the item list from parser + * @param parser data referenced at parser + * @return created list of items + */ + private fun parseItemList(parser: XContentParser): List { + val retList: MutableList = mutableListOf() + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.currentToken(), parser) + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + retList.add(AvailabilityLevel.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create ObservabilityObject object + * @param parser data referenced at parser + * @return created ObservabilityObject object + */ + fun parse(parser: XContentParser): Application { + var name: String? = null + var description: String? = null + var baseQuery: String? = null + var servicesEntities: List = listOf() + var traceGroups: List = listOf() + var availabilityLevels: List = listOf() + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (XContentParser.Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + NAME_TAG -> name = parser.text() + DESCRIPTION_TAG -> description = parser.text() + BASE_QUERY_TAG -> baseQuery = parser.text() + SERVICES_ENTITIES_TAG -> servicesEntities = parser.stringList() + TRACE_GROUPS_TAG -> traceGroups = parser.stringList() + AVAILABILITY_LEVELS_TAG -> availabilityLevels = parseItemList(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Application Skipping Unknown field $fieldName") + } + } + } + return Application(name, description, baseQuery, servicesEntities, traceGroups, availabilityLevels) + } + } + + /** + * create XContentBuilder from this object using [XContentFactory.jsonBuilder()] + * @param params XContent parameters + * @return created XContentBuilder object + */ + fun toXContent(params: ToXContent.Params = ToXContent.EMPTY_PARAMS): XContentBuilder? { + return toXContent(XContentFactory.jsonBuilder(), params) + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + name = input.readString(), + description = input.readString(), + baseQuery = input.readString(), + servicesEntities = input.readStringList(), + traceGroups = input.readStringList(), + availabilityLevels = input.readList(AvailabilityLevel.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(description) + output.writeString(baseQuery) + output.writeStringCollection(servicesEntities) + output.writeStringCollection(traceGroups) + output.writeCollection(availabilityLevels) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(DESCRIPTION_TAG, description) + .fieldIfNotNull(BASE_QUERY_TAG, baseQuery) + .fieldIfNotNull(SERVICES_ENTITIES_TAG, servicesEntities) + .fieldIfNotNull(TRACE_GROUPS_TAG, traceGroups) + .fieldIfNotNull(AVAILABILITY_LEVELS_TAG, availabilityLevels) + return builder.endObject() + } + + internal data class AvailabilityLevel( + val label: String?, + val color: String?, + val condition: String?, + val order: String? + ) : BaseModel { + internal companion object { + private const val LABEL_TAG = "label" + private const val COLOR_TAG = "color" + private const val CONDITION_TAG = "condition" + private const val ORDER_TAG = "order" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { AvailabilityLevel(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Trigger object + * @param parser data referenced at parser + * @return created Trigger object + */ + fun parse(parser: XContentParser): AvailabilityLevel { + var label: String? = null + var color: String? = null + var condition: String? = null + var order: String? = null + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (XContentParser.Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + LABEL_TAG -> label = parser.text() + COLOR_TAG -> color = parser.text() + CONDITION_TAG -> condition = parser.text() + ORDER_TAG -> order = parser.text() + else -> log.info("$LOG_PREFIX: AvailabilityLevel Skipping Unknown field $fieldName") + } + } + label ?: throw IllegalArgumentException("$LABEL_TAG field absent") + color ?: throw IllegalArgumentException("$COLOR_TAG field absent") + condition ?: throw IllegalArgumentException("$CONDITION_TAG field absent") + order ?: throw IllegalArgumentException("$ORDER_TAG field absent") + return AvailabilityLevel(label, color, condition, order) + } + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + label = input.readString(), + color = input.readString(), + condition = input.readString(), + order = input.readString(), + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(label) + output.writeString(color) + output.writeString(condition) + output.writeString(order) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(LABEL_TAG, label) + .field(COLOR_TAG, color) + .field(CONDITION_TAG, condition) + .field(ORDER_TAG, order) + builder.endObject() + return builder + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt index c9b4b6ce3..4da7f24bb 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt @@ -29,6 +29,10 @@ internal object ObservabilityObjectDataProperties { ObservabilityObjectType.OPERATIONAL_PANEL, ObjectProperty(OperationalPanel.reader, OperationalPanel.xParser) ), + Pair( + ObservabilityObjectType.APPLICATION, + ObjectProperty(Application.reader, Application.xParser) + ), Pair( ObservabilityObjectType.TIMESTAMP, ObjectProperty(Timestamp.reader, Timestamp.xParser) @@ -54,6 +58,7 @@ internal object ObservabilityObjectDataProperties { ObservabilityObjectType.SAVED_QUERY -> objectData is SavedQuery ObservabilityObjectType.SAVED_VISUALIZATION -> objectData is SavedVisualization ObservabilityObjectType.OPERATIONAL_PANEL -> objectData is OperationalPanel + ObservabilityObjectType.APPLICATION -> objectData is Application ObservabilityObjectType.TIMESTAMP -> objectData is Timestamp ObservabilityObjectType.NONE -> true } diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt index c5e4cd8e8..29aa51852 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + package org.opensearch.observability.model import org.opensearch.common.io.stream.StreamInput diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt index b1940af47..91e4edcd0 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt @@ -6,6 +6,7 @@ package org.opensearch.observability.model import org.opensearch.commons.utils.EnumParser +import org.opensearch.observability.model.RestTag.APPLICATION_FIELD import org.opensearch.observability.model.RestTag.NOTEBOOK_FIELD import org.opensearch.observability.model.RestTag.OPERATIONAL_PANEL_FIELD import org.opensearch.observability.model.RestTag.SAVED_QUERY_FIELD @@ -42,6 +43,11 @@ enum class ObservabilityObjectType(val tag: String) { return tag } }, + APPLICATION(APPLICATION_FIELD) { + override fun toString(): String { + return tag + } + }, TIMESTAMP(TIMESTAMP_FIELD) { override fun toString(): String { return tag diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt index 422582080..1bc390049 100644 --- a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt @@ -31,6 +31,7 @@ internal object RestTag { const val SAVED_QUERY_FIELD = "savedQuery" const val SAVED_VISUALIZATION_FIELD = "savedVisualization" const val OPERATIONAL_PANEL_FIELD = "operationalPanel" + const val APPLICATION_FIELD = "application" const val TIMESTAMP_FIELD = "timestamp" private val INCLUDE_ID = Pair(OBJECT_ID_FIELD, "true") private val EXCLUDE_ACCESS = Pair(ACCESS_LIST_FIELD, "false") diff --git a/opensearch-observability/src/main/resources/observability-mapping.yml b/opensearch-observability/src/main/resources/observability-mapping.yml index 4748204b8..beb9615bb 100644 --- a/opensearch-observability/src/main/resources/observability-mapping.yml +++ b/opensearch-observability/src/main/resources/observability-mapping.yml @@ -54,6 +54,14 @@ properties: fields: keyword: type: keyword + application: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword timestamp: type: object properties: diff --git a/release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md b/release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md new file mode 100644 index 000000000..1cb328ba1 --- /dev/null +++ b/release-notes/opensearch-trace-analytics.release-notes-1.2.1.0.md @@ -0,0 +1,6 @@ +## Version 1.2.1.0 Release Notes + +Compatible with OpenSearch Version 1.2.1 and OpenSearch Dashboards Version 1.2.0 + +### Maintenance +* Bump observability version for OpenSearch 1.2.1 release ([#313](https://github.com/opensearch-project/trace-analytics/pull/313)) \ No newline at end of file