diff --git a/.github/workflows/test-and-build-workflow.yml b/.github/workflows/dashboards-observability-test-and-build-workflow.yml similarity index 74% rename from .github/workflows/test-and-build-workflow.yml rename to .github/workflows/dashboards-observability-test-and-build-workflow.yml index 166afa013..4401b4e37 100644 --- a/.github/workflows/test-and-build-workflow.yml +++ b/.github/workflows/dashboards-observability-test-and-build-workflow.yml @@ -1,5 +1,5 @@ -name: Test and Build Trace Analytics +name: Test and Build Observability Dashboards Plugin on: [pull_request, push] @@ -15,6 +15,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: Checkout Plugin + uses: actions/checkout@v1 + - name: Checkout OpenSearch Dashboards uses: actions/checkout@v2 with: @@ -40,14 +43,12 @@ jobs: echo "Installing yarn ${{ steps.versions_step.outputs.yarn_version }}" npm i -g yarn@${{ steps.versions_step.outputs.yarn_version }} - - name: Checkout Plugin - uses: actions/checkout@v2 - with: - path: OpenSearch-Dashboards/plugins/observability + - name: Move Observability to Plugins Dir + run: mv dashboards-observability OpenSearch-Dashboards/plugins/dashboards-observability - name: Plugin Bootstrap run: | - cd OpenSearch-Dashboards/plugins/observability + cd OpenSearch-Dashboards/plugins/dashboards-observability yarn osd bootstrap # TODO enable unit tests when ready @@ -56,16 +57,16 @@ jobs: # cd OpenSearch-Dashboards/plugins/observability # yarn test --coverage - - name: Upload coverage - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - direcotry: ./OpenSearch-Dashboards/plugins/observability + # - name: Upload coverage + # uses: codecov/codecov-action@v1 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # direcotry: ./OpenSearch-Dashboards/plugins/dashboards-observability # TODO remove hard coded version when observability is ready - name: Build Artifact run: | - cd OpenSearch-Dashboards/plugins/observability + cd OpenSearch-Dashboards/plugins/dashboards-observability yarn build --opensearch-dashboards-version 1.1.0 mv ./build/*.zip ./build/${{ env.PLUGIN_NAME }}-${{ env.OPENSEARCH_PLUGIN_VERSION }}.zip @@ -73,5 +74,5 @@ jobs: uses: actions/upload-artifact@v1 with: name: observability - path: ./OpenSearch-Dashboards/plugins/observability/build + path: ./OpenSearch-Dashboards/plugins/dashboards-observability/build diff --git a/.github/workflows/opensearch-observability-test-and-build-workflow.yml b/.github/workflows/opensearch-observability-test-and-build-workflow.yml new file mode 100644 index 000000000..0f184b69b --- /dev/null +++ b/.github/workflows/opensearch-observability-test-and-build-workflow.yml @@ -0,0 +1,58 @@ +name: Test and Build OpenSearch Observability Backend Plugin + +on: [pull_request, push] + +env: + OPENSEARCH_VERSION: '1.1.0-SNAPSHOT' + OPENSEARCH_BRANCH: '1.1' + COMMON_UTILS_BRANCH: 'main' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Set up JDK 1.14 + uses: actions/setup-java@v1 + with: + java-version: 1.14 + + # dependencies: OpenSearch + - name: Checkout OpenSearch + uses: actions/checkout@v2 + with: + repository: 'opensearch-project/OpenSearch' + path: OpenSearch + ref: ${{ env.OPENSEARCH_BRANCH }} + - name: Build OpenSearch + working-directory: ./OpenSearch + run: ./gradlew publishToMavenLocal + + # dependencies: common-utils + - name: Checkout common-utils + uses: actions/checkout@v2 + with: + repository: 'opensearch-project/common-utils' + path: common-utils + ref: ${{ env.COMMON_UTILS_BRANCH }} + - name: Build common-utils + working-directory: ./common-utils + run: ./gradlew publishToMavenLocal -Dopensearch.version=${{ env.OPENSEARCH_VERSION }} + + - name: Build with Gradle + run: | + cd opensearch-observability + ./gradlew build -Dopensearch.version=${{ env.OPENSEARCH_VERSION }} + + - name: Create Artifact Path + run: | + mkdir -p opensearch-observability-builds + cp -r ./opensearch-observability/build/distributions/*.zip opensearch-observability-builds/ + + - name: Upload Artifacts + uses: actions/upload-artifact@v1 + with: + name: opensearch-observability + path: opensearch-observability-builds diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 1ef0de10e..113b0ce30 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -10,7 +10,14 @@ So you want to contribute code to this project? Excellent! We're glad you're her 1. Change your node version to the version specified in `.node-version` inside the OpenSearch Dashboards root directory. 1. cd into `plugins` directory in the OpenSearch Dashboards source code directory. 1. Check out this package from version control into the `plugins` directory. -1. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/trace-analytics`. +```bash +git clone git@github.com:opensearch-project/trace-analytics.git plugins --no-checkout +cd plugins +echo 'dashboards-observability/*' >> .git/info/sparse-checkout +git config core.sparseCheckout true +git checkout main +``` +6. Run `yarn osd bootstrap` inside `OpenSearch-Dashboards/plugins/dashboards-observability`. Ultimately, your directory structure should look like this: @@ -18,14 +25,14 @@ Ultimately, your directory structure should look like this: . ├── OpenSearch-Dashboards │ └── plugins -│ └── trace-analytics +│ └── dashboards-observability ``` ### Build To build the plugin's distributable zip simply run `yarn build`. -Example output: `./build/trace-analytics-dashboards*.zip` +Example output: `./build/observability*.zip` ### Run diff --git a/README.md b/README.md index 5aa4059da..784bacfa2 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Additionally the fields can be sorted and filtered. ## Documentation -Please see our technical [documentation](https://opensearch.org/docs/monitoring-plugins/trace/index/) to learn more about its features. +Please see our technical [documentation](https://opensearch.org/docs/latest/monitoring-plugins/trace/index/) to learn more about its features. ## Contributing diff --git a/.babelrc b/dashboards-observability/.babelrc similarity index 100% rename from .babelrc rename to dashboards-observability/.babelrc diff --git a/.cypress/integration/notebooks.spec.js b/dashboards-observability/.cypress/integration/notebooks.spec.js similarity index 100% rename from .cypress/integration/notebooks.spec.js rename to dashboards-observability/.cypress/integration/notebooks.spec.js diff --git a/.cypress/integration/trace_analytics_dashboard.spec.js b/dashboards-observability/.cypress/integration/trace_analytics_dashboard.spec.js similarity index 100% rename from .cypress/integration/trace_analytics_dashboard.spec.js rename to dashboards-observability/.cypress/integration/trace_analytics_dashboard.spec.js diff --git a/.cypress/integration/trace_analytics_services.spec.js b/dashboards-observability/.cypress/integration/trace_analytics_services.spec.js similarity index 100% rename from .cypress/integration/trace_analytics_services.spec.js rename to dashboards-observability/.cypress/integration/trace_analytics_services.spec.js diff --git a/.cypress/integration/trace_analytics_traces.spec.js b/dashboards-observability/.cypress/integration/trace_analytics_traces.spec.js similarity index 100% rename from .cypress/integration/trace_analytics_traces.spec.js rename to dashboards-observability/.cypress/integration/trace_analytics_traces.spec.js diff --git a/.cypress/plugins/index.js b/dashboards-observability/.cypress/plugins/index.js similarity index 100% rename from .cypress/plugins/index.js rename to dashboards-observability/.cypress/plugins/index.js diff --git a/.cypress/support/commands.js b/dashboards-observability/.cypress/support/commands.js similarity index 100% rename from .cypress/support/commands.js rename to dashboards-observability/.cypress/support/commands.js diff --git a/.cypress/support/constants.js b/dashboards-observability/.cypress/support/constants.js similarity index 100% rename from .cypress/support/constants.js rename to dashboards-observability/.cypress/support/constants.js diff --git a/.cypress/support/index.js b/dashboards-observability/.cypress/support/index.js similarity index 100% rename from .cypress/support/index.js rename to dashboards-observability/.cypress/support/index.js diff --git a/.cypress/tsconfig.json b/dashboards-observability/.cypress/tsconfig.json similarity index 100% rename from .cypress/tsconfig.json rename to dashboards-observability/.cypress/tsconfig.json diff --git a/.cypress/utils/constants.js b/dashboards-observability/.cypress/utils/constants.js similarity index 100% rename from .cypress/utils/constants.js rename to dashboards-observability/.cypress/utils/constants.js diff --git a/.cypress/utils/otel-v1-apm-service-map-mappings.json b/dashboards-observability/.cypress/utils/otel-v1-apm-service-map-mappings.json similarity index 100% rename from .cypress/utils/otel-v1-apm-service-map-mappings.json rename to dashboards-observability/.cypress/utils/otel-v1-apm-service-map-mappings.json diff --git a/.cypress/utils/otel-v1-apm-service-map.json b/dashboards-observability/.cypress/utils/otel-v1-apm-service-map.json similarity index 100% rename from .cypress/utils/otel-v1-apm-service-map.json rename to dashboards-observability/.cypress/utils/otel-v1-apm-service-map.json diff --git a/.cypress/utils/otel-v1-apm-span-000001-mappings.json b/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001-mappings.json similarity index 100% rename from .cypress/utils/otel-v1-apm-span-000001-mappings.json rename to dashboards-observability/.cypress/utils/otel-v1-apm-span-000001-mappings.json diff --git a/.cypress/utils/otel-v1-apm-span-000001.json b/dashboards-observability/.cypress/utils/otel-v1-apm-span-000001.json similarity index 100% rename from .cypress/utils/otel-v1-apm-span-000001.json rename to dashboards-observability/.cypress/utils/otel-v1-apm-span-000001.json diff --git a/.cypress/utils/otel-v1-apm-span-000002.json b/dashboards-observability/.cypress/utils/otel-v1-apm-span-000002.json similarity index 100% rename from .cypress/utils/otel-v1-apm-span-000002.json rename to dashboards-observability/.cypress/utils/otel-v1-apm-span-000002.json diff --git a/.eslintrc.js b/dashboards-observability/.eslintrc.js similarity index 100% rename from .eslintrc.js rename to dashboards-observability/.eslintrc.js diff --git a/.gitignore b/dashboards-observability/.gitignore similarity index 100% rename from .gitignore rename to dashboards-observability/.gitignore diff --git a/common/constants/custom_panels.ts b/dashboards-observability/common/constants/custom_panels.ts similarity index 50% rename from common/constants/custom_panels.ts rename to dashboards-observability/common/constants/custom_panels.ts index ae72d893d..d8db8a5fe 100644 --- a/common/constants/custom_panels.ts +++ b/dashboards-observability/common/constants/custom_panels.ts @@ -12,26 +12,3 @@ export const CUSTOM_PANELS_API_PREFIX = '/api/observability/operational_panels'; export const CUSTOM_PANELS_DOCUMENTATION_URL = 'https://www.opensearch.org'; export const CREATE_PANEL_MESSAGE = 'Enter a name to describe the purpose of this custom panel.'; -export const RENAME_VISUALIZATION_MESSAGE = - 'Enter a name to describe the purpose of this visualization.'; - -export type VisualizationType = { - id: string; - title: string; - x: number; - y: number; - w: number; - h: number; - query: string; - type: string; -}; - -export type PanelType = { - name: string; - dateCreated: string; - dateModified: string; - visualizations: VisualizationType[]; - timeRange: { to: string; from: string }; - queryFilter: { query: string; language: string }; - refreshConfig: { pause: string; value: string }; -}; diff --git a/common/constants/explorer.ts b/dashboards-observability/common/constants/explorer.ts similarity index 93% rename from common/constants/explorer.ts rename to dashboards-observability/common/constants/explorer.ts index 3f4999327..739d3f689 100644 --- a/common/constants/explorer.ts +++ b/dashboards-observability/common/constants/explorer.ts @@ -12,7 +12,8 @@ export const RAW_QUERY = 'rawQuery'; export const FINAL_QUERY = 'finalQuery'; export const SELECTED_DATE_RANGE = 'selectedDateRange'; -export const INDEX = 'indexPattern'; +export const INDEX = 'index'; +export const SELECTED_TIMESTAMP = 'selectedTimestamp'; export const SELECTED_FIELDS = 'selectedFields'; export const UNSELECTED_FIELDS = 'unselectedFields'; export const AVAILABLE_FIELDS = 'availableFields'; @@ -23,6 +24,7 @@ export const TAB_CHART_TITLE = 'Visualizations'; export const TAB_EVENT_TITLE = 'Events'; export const TAB_EVENT_ID_TXT_PFX = 'main-content-events-'; export const TAB_CHART_ID_TXT_PFX = 'main-content-vis-'; +export const HAS_SAVED_TIMESTAMP = 'hasSavedTimestamp'; export const DATE_PICKER_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export const TIME_INTERVAL_OPTIONS = [ diff --git a/common/constants/notebooks.ts b/dashboards-observability/common/constants/notebooks.ts similarity index 100% rename from common/constants/notebooks.ts rename to dashboards-observability/common/constants/notebooks.ts diff --git a/common/constants/shared.ts b/dashboards-observability/common/constants/shared.ts similarity index 94% rename from common/constants/shared.ts rename to dashboards-observability/common/constants/shared.ts index e82bbd3d5..d120a12a8 100644 --- a/common/constants/shared.ts +++ b/dashboards-observability/common/constants/shared.ts @@ -35,7 +35,7 @@ export const observabilityPluginOrder = 6000; export const UI_DATE_FORMAT = 'MM/DD/YYYY hh:mm A'; export const PPL_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; export const PPL_STATS_REGEX = /\|\s*stats/i; -export const PPL_INDEX_INSERT_POINT_REGEX = /search (source|index)\s*=\s*([^\s]+)(.*)/i; +export const PPL_INDEX_INSERT_POINT_REGEX = /(search source|source|index)\s*=\s*([^\s]+)(.*)/i; export const PPL_INDEX_REGEX = /(search source|source|index)\s*=\s*([^|\s]+)/i; // Observability plugin URI diff --git a/common/constants/trace_analytics.ts b/dashboards-observability/common/constants/trace_analytics.ts similarity index 100% rename from common/constants/trace_analytics.ts rename to dashboards-observability/common/constants/trace_analytics.ts diff --git a/common/types/custom_panels.ts b/dashboards-observability/common/types/custom_panels.ts similarity index 92% rename from common/types/custom_panels.ts rename to dashboards-observability/common/types/custom_panels.ts index 4d8f62145..36449130c 100644 --- a/common/types/custom_panels.ts +++ b/dashboards-observability/common/types/custom_panels.ts @@ -18,14 +18,11 @@ export type CustomPanelListType = { export type VisualizationType = { id: string; - title: string; + savedVisualizationId: string; x: number; y: number; w: number; h: number; - query: string; - type: string; - timeField: string; }; export type PanelType = { diff --git a/common/types/explorer.ts b/dashboards-observability/common/types/explorer.ts similarity index 61% rename from common/types/explorer.ts rename to dashboards-observability/common/types/explorer.ts index e4cf65af9..109104462 100644 --- a/common/types/explorer.ts +++ b/dashboards-observability/common/types/explorer.ts @@ -14,9 +14,17 @@ import { SELECTED_FIELDS, UNSELECTED_FIELDS, AVAILABLE_FIELDS, - QUERIED_FIELDS + QUERIED_FIELDS, + INDEX, + FINAL_QUERY, + SELECTED_TIMESTAMP, + SELECTED_DATE_RANGE } from '../constants/explorer'; + import { HttpStart } 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'; + import DSLService from '../../public/services/requests/dsl'; export interface IQueryTab { id: string; @@ -38,7 +46,11 @@ export interface ITabQueries { } export interface IQuery { - [RAW_QUERY]: string + [RAW_QUERY]: string; + [FINAL_QUERY]: string; + [INDEX]: string; + [SELECTED_DATE_RANGE]: Array; + [SELECTED_TIMESTAMP]: string; } export interface IExplorerTabFields { @@ -53,7 +65,15 @@ export interface IExplorerFields { } export interface ILogExplorerProps { - pplService: any; - dslService: any; + pplService: PPLService; + dslService: DSLService; savedObjects: SavedObjects; + http: HttpStart; + timestampUtils: TimestampUtils; + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void; } \ No newline at end of file diff --git a/common/types/notebooks.ts b/dashboards-observability/common/types/notebooks.ts similarity index 100% rename from common/types/notebooks.ts rename to dashboards-observability/common/types/notebooks.ts diff --git a/common/utils/index.ts b/dashboards-observability/common/utils/index.ts similarity index 87% rename from common/utils/index.ts rename to dashboards-observability/common/utils/index.ts index 62211e2c6..f66db0ef7 100644 --- a/common/utils/index.ts +++ b/dashboards-observability/common/utils/index.ts @@ -10,3 +10,4 @@ */ export { getIndexPatternFromRawQuery, insertDateRangeToQuery } from './query_utils'; +export { uiSettingsService } from './settings_service'; diff --git a/common/utils/query_utils.ts b/dashboards-observability/common/utils/query_utils.ts similarity index 87% rename from common/utils/query_utils.ts rename to dashboards-observability/common/utils/query_utils.ts index 5a3e72067..648f21c2e 100644 --- a/common/utils/query_utils.ts +++ b/dashboards-observability/common/utils/query_utils.ts @@ -29,7 +29,7 @@ export const insertDateRangeToQuery = ({ rawQuery, startTime, endTime, - timeField = 'utc_time', + timeField, }: { rawQuery: string; startTime: string; @@ -47,7 +47,7 @@ export const insertDateRangeToQuery = ({ const tokens = rawQuery.match(PPL_INDEX_INSERT_POINT_REGEX); if (isEmpty(tokens)) return finalQuery; - finalQuery = `search ${tokens![1]}=${tokens![2]} | where ${timeField} >= timestamp('${start}') and ${timeField} <= timestamp('${end}')${tokens![3]}`; + finalQuery = `${tokens![1]}=${tokens![2]} | where ${timeField} >= timestamp('${start}') and ${timeField} <= timestamp('${end}')${tokens![3]}`; return finalQuery; }; \ No newline at end of file diff --git a/dashboards-observability/common/utils/settings_service.ts b/dashboards-observability/common/utils/settings_service.ts new file mode 100644 index 000000000..97ea3ebfa --- /dev/null +++ b/dashboards-observability/common/utils/settings_service.ts @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { IUiSettingsClient } from '../../../../src/core/public'; + +let uiSettings: IUiSettingsClient; + +export const uiSettingsService = { + init: (client: IUiSettingsClient) => { + uiSettings = client; + }, + get: (key: string, defaultOverride?: any) => { + return uiSettings?.get(key, defaultOverride) || ''; + }, +}; \ No newline at end of file diff --git a/cypress.json b/dashboards-observability/cypress.json similarity index 100% rename from cypress.json rename to dashboards-observability/cypress.json diff --git a/opensearch-dashboards-plugin-helpers.dev.json b/dashboards-observability/opensearch-dashboards-plugin-helpers.dev.json similarity index 100% rename from opensearch-dashboards-plugin-helpers.dev.json rename to dashboards-observability/opensearch-dashboards-plugin-helpers.dev.json diff --git a/opensearch_dashboards.json b/dashboards-observability/opensearch_dashboards.json similarity index 100% rename from opensearch_dashboards.json rename to dashboards-observability/opensearch_dashboards.json diff --git a/package.json b/dashboards-observability/package.json similarity index 100% rename from package.json rename to dashboards-observability/package.json diff --git a/public/components/app.tsx b/dashboards-observability/public/components/app.tsx similarity index 83% rename from public/components/app.tsx rename to dashboards-observability/public/components/app.tsx index 4b5cdf2fe..e1bfdebea 100644 --- a/public/components/app.tsx +++ b/dashboards-observability/public/components/app.tsx @@ -17,7 +17,6 @@ import { CoreStart } from '../../../../src/core/public'; import { observabilityTitle } from '../../common/constants/shared'; import store from '../framework/redux/store'; import { AppPluginStartDependencies } from '../types'; -import { Home as ApplicationAnalyticsHome } from './application_analytics/home'; import { renderPageWithSidebar } from './common/side_nav'; import { Home as CustomPanelsHome } from './custom_panels/home'; import { EventAnalytics } from './explorer/event_analytics'; @@ -30,6 +29,7 @@ interface ObservabilityAppDeps { pplService: any; dslService: any; savedObjects: any; + timestampUtils: any; } export const App = ({ @@ -37,9 +37,9 @@ export const App = ({ DepsStart, pplService, dslService, - savedObjects + savedObjects, + timestampUtils }: ObservabilityAppDeps) => { - const { chrome, http, notifications } = CoreStart; const parentBreadcrumb = { text: observabilityTitle, @@ -57,31 +57,6 @@ export const App = ({ <> - { - chrome.setBreadcrumbs([ - parentBreadcrumb, - { - text: 'Application analytics', - href: '#/application_analytics', - }, - ]); - return renderPageWithSidebar(); - }} - /> - ( - - )} - /> ( @@ -107,6 +82,7 @@ export const App = ({ pplService={ pplService } dslService={ dslService } savedObjects={ savedObjects } + timestampUtils={ timestampUtils } http={ http } { ...props } /> @@ -114,7 +90,7 @@ export const App = ({ }} /> { chrome.setBreadcrumbs([parentBreadcrumb, customPanelBreadcrumb]); return ( @@ -128,6 +104,17 @@ export const App = ({ ); }} /> + ( + + )} + /> diff --git a/public/components/common/debounced_component/debounced_component.test.tsx b/dashboards-observability/public/components/common/debounced_component/debounced_component.test.tsx similarity index 100% rename from public/components/common/debounced_component/debounced_component.test.tsx rename to dashboards-observability/public/components/common/debounced_component/debounced_component.test.tsx diff --git a/public/components/common/debounced_component/debounced_component.tsx b/dashboards-observability/public/components/common/debounced_component/debounced_component.tsx similarity index 100% rename from public/components/common/debounced_component/debounced_component.tsx rename to dashboards-observability/public/components/common/debounced_component/debounced_component.tsx diff --git a/public/components/common/debounced_component/index.ts b/dashboards-observability/public/components/common/debounced_component/index.ts similarity index 100% rename from public/components/common/debounced_component/index.ts rename to dashboards-observability/public/components/common/debounced_component/index.ts diff --git a/public/components/common/field_button/__snapshots__/field_button.test.tsx.snap b/dashboards-observability/public/components/common/field_button/__snapshots__/field_button.test.tsx.snap similarity index 100% rename from public/components/common/field_button/__snapshots__/field_button.test.tsx.snap rename to dashboards-observability/public/components/common/field_button/__snapshots__/field_button.test.tsx.snap diff --git a/public/components/common/field_button/field_button.scss b/dashboards-observability/public/components/common/field_button/field_button.scss similarity index 100% rename from public/components/common/field_button/field_button.scss rename to dashboards-observability/public/components/common/field_button/field_button.scss diff --git a/public/components/common/field_button/field_button.test.tsx b/dashboards-observability/public/components/common/field_button/field_button.test.tsx similarity index 100% rename from public/components/common/field_button/field_button.test.tsx rename to dashboards-observability/public/components/common/field_button/field_button.test.tsx diff --git a/public/components/common/field_button/field_button.tsx b/dashboards-observability/public/components/common/field_button/field_button.tsx similarity index 100% rename from public/components/common/field_button/field_button.tsx rename to dashboards-observability/public/components/common/field_button/field_button.tsx diff --git a/public/components/common/field_button/index.ts b/dashboards-observability/public/components/common/field_button/index.ts similarity index 100% rename from public/components/common/field_button/index.ts rename to dashboards-observability/public/components/common/field_button/index.ts diff --git a/public/components/common/field_icon/__snapshots__/field_icon.test.tsx.snap b/dashboards-observability/public/components/common/field_icon/__snapshots__/field_icon.test.tsx.snap similarity index 100% rename from public/components/common/field_icon/__snapshots__/field_icon.test.tsx.snap rename to dashboards-observability/public/components/common/field_icon/__snapshots__/field_icon.test.tsx.snap diff --git a/public/components/common/field_icon/field_icon.test.tsx b/dashboards-observability/public/components/common/field_icon/field_icon.test.tsx similarity index 100% rename from public/components/common/field_icon/field_icon.test.tsx rename to dashboards-observability/public/components/common/field_icon/field_icon.test.tsx diff --git a/public/components/common/field_icon/field_icon.tsx b/dashboards-observability/public/components/common/field_icon/field_icon.tsx similarity index 100% rename from public/components/common/field_icon/field_icon.tsx rename to dashboards-observability/public/components/common/field_icon/field_icon.tsx diff --git a/public/components/common/field_icon/index.ts b/dashboards-observability/public/components/common/field_icon/index.ts similarity index 100% rename from public/components/common/field_icon/index.ts rename to dashboards-observability/public/components/common/field_icon/index.ts diff --git a/public/components/common/field_name/__snapshots__/field_name.test.tsx.snap b/dashboards-observability/public/components/common/field_name/__snapshots__/field_name.test.tsx.snap similarity index 100% rename from public/components/common/field_name/__snapshots__/field_name.test.tsx.snap rename to dashboards-observability/public/components/common/field_name/__snapshots__/field_name.test.tsx.snap diff --git a/public/components/common/field_name/field_name.test.tsx b/dashboards-observability/public/components/common/field_name/field_name.test.tsx similarity index 100% rename from public/components/common/field_name/field_name.test.tsx rename to dashboards-observability/public/components/common/field_name/field_name.test.tsx diff --git a/public/components/common/field_name/field_name.tsx b/dashboards-observability/public/components/common/field_name/field_name.tsx similarity index 100% rename from public/components/common/field_name/field_name.tsx rename to dashboards-observability/public/components/common/field_name/field_name.tsx diff --git a/public/components/common/field_name/field_type_name.ts b/dashboards-observability/public/components/common/field_name/field_type_name.ts similarity index 100% rename from public/components/common/field_name/field_type_name.ts rename to dashboards-observability/public/components/common/field_name/field_type_name.ts diff --git a/public/components/common/helpers/format_number_with_commas.ts b/dashboards-observability/public/components/common/helpers/format_number_with_commas.ts similarity index 100% rename from public/components/common/helpers/format_number_with_commas.ts rename to dashboards-observability/public/components/common/helpers/format_number_with_commas.ts diff --git a/public/components/common/helpers/index.ts b/dashboards-observability/public/components/common/helpers/index.ts similarity index 100% rename from public/components/common/helpers/index.ts rename to dashboards-observability/public/components/common/helpers/index.ts diff --git a/public/components/common/loading_spinner/loading_spinner.test.tsx b/dashboards-observability/public/components/common/loading_spinner/loading_spinner.test.tsx similarity index 100% rename from public/components/common/loading_spinner/loading_spinner.test.tsx rename to dashboards-observability/public/components/common/loading_spinner/loading_spinner.test.tsx diff --git a/public/components/common/loading_spinner/loading_spinner.tsx b/dashboards-observability/public/components/common/loading_spinner/loading_spinner.tsx similarity index 100% rename from public/components/common/loading_spinner/loading_spinner.tsx rename to dashboards-observability/public/components/common/loading_spinner/loading_spinner.tsx diff --git a/public/components/common/search/autocomplete.tsx b/dashboards-observability/public/components/common/search/autocomplete.tsx similarity index 97% rename from public/components/common/search/autocomplete.tsx rename to dashboards-observability/public/components/common/search/autocomplete.tsx index 9075107ee..dc2125222 100644 --- a/public/components/common/search/autocomplete.tsx +++ b/dashboards-observability/public/components/common/search/autocomplete.tsx @@ -45,7 +45,7 @@ export function Autocomplete(props: IQueryBarProps) { render({ children }, root) { render(children, root); }, - initialState: { query: query[RAW_QUERY] }, + initialState: { query: query }, openOnFocus: true, placeholder: 'Enter PPL query to retrieve log, traces, and metrics', plugins: [PPLSuggestionPlugin], diff --git a/public/components/common/search/autocomplete_plugin.ts b/dashboards-observability/public/components/common/search/autocomplete_plugin.ts similarity index 98% rename from public/components/common/search/autocomplete_plugin.ts rename to dashboards-observability/public/components/common/search/autocomplete_plugin.ts index 319e509e7..e616618bd 100644 --- a/public/components/common/search/autocomplete_plugin.ts +++ b/dashboards-observability/public/components/common/search/autocomplete_plugin.ts @@ -268,7 +268,7 @@ export function createPPLSuggestionsPlugin( ): AutocompletePlugin { return { onStateChange: ({ state }) => { - if (options.query.rawQuery !== state.query) { + if (options.query !== state.query) { options.handleQueryChange(state.query, currIndex); } }, @@ -293,10 +293,11 @@ export function createPPLSuggestionsPlugin( }, templates: { item({ item, createElement }) { + const prefix = item.input.split(' '); return createElement('div', { dangerouslySetInnerHTML: { __html: `
- ${item.input}${item.suggestion} + ${prefix[prefix.length-1]}${item.suggestion}
`, }, }); diff --git a/public/components/common/search/date_picker.tsx b/dashboards-observability/public/components/common/search/date_picker.tsx similarity index 92% rename from public/components/common/search/date_picker.tsx rename to dashboards-observability/public/components/common/search/date_picker.tsx index 79700b23d..53dd0733b 100644 --- a/public/components/common/search/date_picker.tsx +++ b/dashboards-observability/public/components/common/search/date_picker.tsx @@ -17,6 +17,7 @@ import { import { IDatePickerProps } from './search'; +import { uiSettingsService } from '../../../../common/utils'; export function DatePicker(props: IDatePickerProps) { @@ -66,7 +67,7 @@ export function DatePicker(props: IDatePickerProps) { start={ startTime } end={ endTime } showUpdateButton={ false } - dateFormat="MM/DD/YYYY hh:mm:ss A" + dateFormat={uiSettingsService.get('dateFormat')} onTimeChange={(e) => { const start = e.start; const end = e.start === e.end ? 'now' : e.end; diff --git a/public/components/common/search/queries/data_queries.ts b/dashboards-observability/public/components/common/search/queries/data_queries.ts similarity index 100% rename from public/components/common/search/queries/data_queries.ts rename to dashboards-observability/public/components/common/search/queries/data_queries.ts diff --git a/public/components/common/search/request_handler.tsx b/dashboards-observability/public/components/common/search/request_handler.tsx similarity index 100% rename from public/components/common/search/request_handler.tsx rename to dashboards-observability/public/components/common/search/request_handler.tsx diff --git a/public/components/common/search/search.scss b/dashboards-observability/public/components/common/search/search.scss similarity index 100% rename from public/components/common/search/search.scss rename to dashboards-observability/public/components/common/search/search.scss diff --git a/public/components/common/search/search.tsx b/dashboards-observability/public/components/common/search/search.tsx similarity index 90% rename from public/components/common/search/search.tsx rename to dashboards-observability/public/components/common/search/search.tsx index eca65b7b5..76fcc59d1 100644 --- a/public/components/common/search/search.tsx +++ b/dashboards-observability/public/components/common/search/search.tsx @@ -23,6 +23,7 @@ import { EuiPopoverFooter, } from '@elastic/eui'; import _ from 'lodash'; +import { IQuery } from '../../../../common/types/explorer'; import { DatePicker } from './date_picker'; import '@algolia/autocomplete-theme-classic'; import { Autocomplete } from './autocomplete'; @@ -30,7 +31,7 @@ import { SavePanel } from '../../explorer/save_panel'; import { useCallback } from 'react'; export interface IQueryBarProps { - query: any; + query: IQuery; handleQueryChange: (query: string, index: string) => void; handleQuerySearch: () => void; dslService: any; @@ -67,7 +68,8 @@ export const Search = (props: any) => { isPanelTextFieldInvalid, savedObjects, showSavePanelOptionsList, - showSaveButton = true + showSaveButton = true, + setToast } = props; const [isSavePanelOpen, setIsSavePanelOpen] = useState(false); @@ -90,7 +92,7 @@ export const Search = (props: any) => { ); @@ -115,7 +117,14 @@ export const Search = (props: any) => {
- {renderAutocomplete({ query, handleQueryChange, handleQuerySearch, dslService })} + { + renderAutocomplete({ + query, + handleQueryChange, + handleQuerySearch: memorizedHandleQuerySearch, + dslService + }) + }
{ handleSavingObject()}> + onClick={() => { + handleSavingObject(); + setIsSavePanelOpen(false); + }}> { "Save" } diff --git a/public/components/common/search/searchindex.tsx b/dashboards-observability/public/components/common/search/searchindex.tsx similarity index 100% rename from public/components/common/search/searchindex.tsx rename to dashboards-observability/public/components/common/search/searchindex.tsx diff --git a/public/components/common/side_nav.tsx b/dashboards-observability/public/components/common/side_nav.tsx similarity index 92% rename from public/components/common/side_nav.tsx rename to dashboards-observability/public/components/common/side_nav.tsx index 5e3a728fc..5e610ab17 100644 --- a/public/components/common/side_nav.tsx +++ b/dashboards-observability/public/components/common/side_nav.tsx @@ -38,43 +38,38 @@ export const renderPageWithSidebar = (BodyComponent: React.ReactNode) => { name: 'Observability', id: 0, items: [ - { - name: 'Application analytics', - id: 1, - href: '#/application_analytics/home', - }, { name: 'Trace analytics', - id: 2, + id: 1, href: '#/trace_analytics/home', items: [ { name: 'Traces', - id: 2.1, + id: 1.1, href: '#/trace_analytics/traces', }, { name: 'Services', - id: 2.2, + id: 1.2, href: '#/trace_analytics/services', }, ], }, { name: 'Event analytics', - id: 3, + id: 2, href: '#/event_analytics', }, { name: 'Operational panels', - id: 4, + id: 3, href: '#/operational_panels/', }, { name: 'Notebooks', - id: 5, + id: 4, href: '#/notebooks', - } + }, ], }, ]; diff --git a/public/components/custom_panels/custom_panel_table.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx similarity index 98% rename from public/components/custom_panels/custom_panel_table.tsx rename to dashboards-observability/public/components/custom_panels/custom_panel_table.tsx index 8c33c5005..1c1f5bd34 100644 --- a/public/components/custom_panels/custom_panel_table.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_table.tsx @@ -36,7 +36,6 @@ import { } from '@elastic/eui'; import React, { CSSProperties, ReactElement, useEffect, useState } from 'react'; import { ChromeBreadcrumb } from '../../../../../src/core/public'; -import { CustomPanelListType } from './home'; import { CREATE_PANEL_MESSAGE, CUSTOM_PANELS_DOCUMENTATION_URL, @@ -45,6 +44,7 @@ import { UI_DATE_FORMAT } from '../../../common/constants/shared'; import { getCustomModal, DeletePanelModal } from './helpers/modal_containers'; import moment from 'moment'; import _ from 'lodash'; +import { CustomPanelListType } from '../../../common/types/custom_panels'; const pageStyles: CSSProperties = { float: 'left', @@ -65,12 +65,11 @@ const pageStyles: CSSProperties = { * renameCustomPanel: rename function for the panel * cloneCustomPanel: clone function for the panel * deleteCustomPanelList: delete function for the panels - * setToast: create Toast function */ type Props = { loading: boolean; - fetchCustomPanels: () => Promise; + fetchCustomPanels: () => void; customPanels: Array; createCustomPanel: (newCustomPanelName: string) => void; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; diff --git a/public/components/custom_panels/custom_panel_view.tsx b/dashboards-observability/public/components/custom_panels/custom_panel_view.tsx similarity index 68% rename from public/components/custom_panels/custom_panel_view.tsx rename to dashboards-observability/public/components/custom_panels/custom_panel_view.tsx index d5373dc63..c8ed267ba 100644 --- a/public/components/custom_panels/custom_panel_view.tsx +++ b/dashboards-observability/public/components/custom_panels/custom_panel_view.tsx @@ -13,6 +13,7 @@ import { EuiBreadcrumb, EuiButton, EuiContextMenu, + EuiContextMenuPanelDescriptor, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -25,9 +26,8 @@ import { EuiPopover, EuiSpacer, EuiSuperDatePicker, - EuiSuperDatePickerProps, EuiTitle, - htmlIdGenerator, + OnTimeChangeProps, ShortDate, } from '@elastic/eui'; import _ from 'lodash'; @@ -35,7 +35,6 @@ import React, { useEffect, useState } from 'react'; import { CoreStart } from '../../../../../src/core/public'; import { EmptyPanelView } from './panel_modules/empty_panel'; import { - RENAME_VISUALIZATION_MESSAGE, CREATE_PANEL_MESSAGE, CUSTOM_PANELS_API_PREFIX, } from '../../../common/constants/custom_panels'; @@ -49,6 +48,7 @@ import { UI_DATE_FORMAT } from '../../../common/constants/shared'; import { ChangeEvent } from 'react'; import moment from 'moment'; import { VisaulizationFlyout } from './panel_modules/visualization_flyout'; +import { uiSettingsService } from '../../../common/utils'; /* * "CustomPanelsView" module used to render an Operational Panel @@ -60,6 +60,7 @@ import { VisaulizationFlyout } from './panel_modules/visualization_flyout'; * chrome: chrome core service * parentBreadcrumb: parent breadcrumb * renameCustomPanel: Rename function for the panel + * cloneCustomPanel: Clone function for the panel * deleteCustomPanel: Delete function for the panel * setToast: create Toast function */ @@ -75,6 +76,7 @@ type Props = { editedCustomPanelId: string ) => Promise | undefined; deleteCustomPanel: (customPanelId: string, customPanelName: string) => Promise; + cloneCustomPanel: (clonedCustomPanelName: string, clonedCustomPanelId: string) => Promise; setToast: ( title: string, color?: string, @@ -91,6 +93,7 @@ export const CustomPanelView = ({ parentBreadcrumb, renameCustomPanel, deleteCustomPanel, + cloneCustomPanel, setToast, }: Props) => { const [openPanelName, setOpenPanelName] = useState(''); @@ -101,6 +104,7 @@ export const CustomPanelView = ({ const [inputDisabled, setInputDisabled] = useState(true); const [addVizDisabled, setAddVizDisabled] = useState(false); const [editDisabled, setEditDisabled] = useState(false); + const [dateDisabled, setDateDisabled] = useState(false); const [panelVisualizations, setPanelVisualizations] = useState>([]); const [editMode, setEditMode] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle @@ -109,6 +113,8 @@ export const CustomPanelView = ({ const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); // Add Visualization Flyout const [isFlyoutReplacement, setisFlyoutReplacement] = useState(false); const [replaceVisualizationId, setReplaceVisualizationId] = useState(''); + const [panelsMenuPopover, setPanelsMenuPopover] = useState(false); + const [editActionType, setEditActionType] = useState(''); // DateTimePicker States const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); @@ -183,9 +189,27 @@ export const CustomPanelView = ({ setIsModalVisible(true); }; + const onKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') onRefreshFilters(); + }; + + const onDatePickerChange = (props: OnTimeChangeProps) => { + onTimeChange( + props.start, + props.end, + recentlyUsedRanges, + setRecentlyUsedRanges, + setStart, + setEnd + ); + onRefreshFilters(); + }; + const onDelete = async () => { deleteCustomPanel(panelId, openPanelName).then((res) => { - window.location.assign(`${_.last(parentBreadcrumb).href}`); + setTimeout(() => { + window.location.assign(`${_.last(parentBreadcrumb).href}`); + }, 1000); }); closeModal(); }; @@ -225,14 +249,36 @@ export const CustomPanelView = ({ showModal(); }; + const onClone = async (newCustomPanelName: string) => { + cloneCustomPanel(newCustomPanelName, panelId).then((id: string) => { + setTimeout(() => { + window.location.assign(`${_.last(parentBreadcrumb).href}${id}`); + }, 1000); + }); + closeModal(); + }; + + const clonePanel = () => { + setModalLayout( + getCustomModal( + onClone, + closeModal, + 'Name', + 'Duplicate Panel', + 'Cancel', + 'Duplicate', + openPanelName + ' (copy)', + CREATE_PANEL_MESSAGE + ) + ); + showModal(); + }; + // toggle between panel edit mode - const editPanel = () => { - if (editMode) { - // Save layout - setEditMode(false); - } else { - setEditMode(true); - } + const editPanel = (editType: string) => { + editMode ? setEditMode(false) : setEditMode(true); + if (editType === 'cancel') fetchCustomPanel(); + setEditActionType(editType); }; const closeFlyout = () => { @@ -250,16 +296,29 @@ export const CustomPanelView = ({ }; const checkDisabledInputs = () => { - if (panelVisualizations.length == 0) { + // When not in edit mode and panel has no visualizations + if (panelVisualizations.length === 0 && !editMode) { setEditDisabled(true); setInputDisabled(true); - } else { + setAddVizDisabled(false); + setDateDisabled(false); + } + + // When panel has visualizations + if (panelVisualizations.length > 0) { + setEditDisabled(false); + setInputDisabled(false); + setAddVizDisabled(false); + setDateDisabled(false); + } + + // When in edit mode + if (editMode) { setEditDisabled(false); - if (editMode) setInputDisabled(true); - else setInputDisabled(false); + setInputDisabled(true); + setAddVizDisabled(true); + setDateDisabled(true); } - if (editMode) setAddVizDisabled(true); - else setAddVizDisabled(false); }; const onRefreshFilters = () => { @@ -292,79 +351,22 @@ export const CustomPanelView = ({ }); }; - const cloneVisualization = ( - newVisualizationTitle: string, - pplQuery: string, - newVisualizationType: string, - newVisualizationTimeField: string - ) => { - setModalLayout( - getCustomModal( - onCloneVisualization, - closeModal, - 'Name', - 'Duplicate Visualization', - 'Cancel', - 'Duplicate', - newVisualizationTitle + ' (copy)', - RENAME_VISUALIZATION_MESSAGE, - [pplQuery, newVisualizationType, newVisualizationTimeField] - ) - ); - showModal(); - }; - - const onCloneVisualization = ( - newVisualizationTitle: string, - pplQuery: string, - newVisualizationType: string, - newVisualizationTimeField: string - ) => { + const cloneVisualization = (visualzationTitle: string, savedVisualizationId: string) => { http .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { body: JSON.stringify({ panelId: panelId, - newVisualization: { - id: 'panelViz_' + htmlIdGenerator()(), - title: newVisualizationTitle, - query: pplQuery, - type: newVisualizationType, - timeField: newVisualizationTimeField, - }, + savedVisualizationId: savedVisualizationId, }), }) .then(async (res) => { setPanelVisualizations(res.visualizations); - setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); + setToast(`Visualization ${visualzationTitle} successfully added!`, 'success'); }) .catch((err) => { - setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + setToast(`Error in adding ${visualzationTitle} visualization to the panel`, 'danger'); console.error(err); }); - closeModal(); - }; - - const removeVisualization = (visualizationId: string) => { - const newVisualizationList = _.reject(panelVisualizations, { - id: visualizationId, - }); - if (newVisualizationList.length === 0) { - setEditMode(false); - http - .put(`${CUSTOM_PANELS_API_PREFIX}/visualizations/edit`, { - body: JSON.stringify({ - panelId: panelId, - visualizationParams: [], - }), - }) - .then(async (res) => { - setPanelVisualizations(res.visualizations); - }) - .catch((err) => { - console.error(err); - }); - } - setPanelVisualizations(newVisualizationList); }; //Add Visualization Button @@ -386,6 +388,7 @@ export const CustomPanelView = ({ { + setPanelsMenuPopover(false); + renamePanel(); + }, + }, + { + name: 'Duplicate panel', + onClick: () => { + setPanelsMenuPopover(false); + clonePanel(); + }, + }, + { + name: 'Delete panel', + onClick: () => { + setPanelsMenuPopover(false); + deletePanel(); + }, + }, + ], + }, + ]; + // Fetch the custom panel on Initial Mount useEffect(() => { fetchCustomPanel(); - }, []); + }, [panelId]); // Check Validity of Time useEffect(() => { @@ -423,7 +456,7 @@ export const CustomPanelView = ({ href: `${_.last(parentBreadcrumb).href}${panelId}`, }, ]); - }, [openPanelName]); + }, [panelId, openPanelName]); return (
@@ -441,22 +474,52 @@ export const CustomPanelView = ({ - - - Delete - - - - Rename - - - + + editPanel('cancel')} + > + Cancel + + + + editPanel('save')}> + Save + + + + ) : ( + + editPanel('edit')} + disabled={editDisabled} + > + Edit + + + )} + + setPanelsMenuPopover(true)} + > + Panel actions + + } + isOpen={panelsMenuPopover} + closePopover={() => setPanelsMenuPopover(false)} > - {editMode ? 'Save' : 'Edit'} - + + @@ -468,27 +531,20 @@ export const CustomPanelView = ({ placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" value={pplFilterValue} fullWidth={true} - onChange={(e) => onChange(e)} + onChange={onChange} + onKeyPress={onKeyPress} disabled={inputDisabled} /> ) => - onTimeChange( - props.start, - props.end, - recentlyUsedRanges, - setRecentlyUsedRanges, - setStart, - setEnd - ) - } + onTimeChange={onDatePickerChange} showUpdateButton={false} recentlyUsedRanges={recentlyUsedRanges} + isDisabled={dateDisabled} /> @@ -519,24 +575,24 @@ export const CustomPanelView = ({ getVizContextPanels={getVizContextPanels} /> ) : ( - + <> )} - <> + diff --git a/public/components/custom_panels/helpers/custom_input_modal.tsx b/dashboards-observability/public/components/custom_panels/helpers/custom_input_modal.tsx similarity index 100% rename from public/components/custom_panels/helpers/custom_input_modal.tsx rename to dashboards-observability/public/components/custom_panels/helpers/custom_input_modal.tsx diff --git a/public/components/custom_panels/helpers/flyout_containers.tsx b/dashboards-observability/public/components/custom_panels/helpers/flyout_containers.tsx similarity index 100% rename from public/components/custom_panels/helpers/flyout_containers.tsx rename to dashboards-observability/public/components/custom_panels/helpers/flyout_containers.tsx diff --git a/public/components/custom_panels/helpers/modal_containers.tsx b/dashboards-observability/public/components/custom_panels/helpers/modal_containers.tsx similarity index 100% rename from public/components/custom_panels/helpers/modal_containers.tsx rename to dashboards-observability/public/components/custom_panels/helpers/modal_containers.tsx diff --git a/public/components/custom_panels/helpers/utils.tsx b/dashboards-observability/public/components/custom_panels/helpers/utils.tsx similarity index 64% rename from public/components/custom_panels/helpers/utils.tsx rename to dashboards-observability/public/components/custom_panels/helpers/utils.tsx index 5ebbcfbc3..955ffd9e8 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/dashboards-observability/public/components/custom_panels/helpers/utils.tsx @@ -20,16 +20,23 @@ import React from 'react'; import { Bar } from '../../visualizations/charts/bar'; import { HorizontalBar } from '../../visualizations/charts/horizontal_bar'; import { Line } from '../../visualizations/charts/line'; +import { CoreStart } from '../../../../../../src/core/public'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; +import { VisualizationType, SavedVisualizationType } from '../../../../common/types/custom_panels'; +import { Layout } from 'react-grid-layout'; /* * "Utils" This file contains different reused functions in operational panels + * * isNameValid - Validates string to length > 0 and < 50 * convertDateTime - Converts input datetime string to required format + * mergeLayoutAndVisualizations - Function to merge current panel layout into the visualizations list * getQueryResponse - Get response of PPL query to load visualizations + * renderSavedVisualization - Fetches savedVisualization by Id and runs getQueryResponse * onTimeChange - Function to store recently used time filters and set start and end time. * isDateValid - Function to check date validity * isPPLFilterValid - Validate if the panel PPL query doesn't contain any Index/Time/Field filters - * displayVisualization - This function renders the visualzation based of its type + * displayVisualization - Function to render the visualzation based of its type */ // Name validation 0>Name<=50 @@ -50,6 +57,30 @@ export const convertDateTime = (datetime: string, isStart = true, formatted = tr return returnTime; }; +// Merges new layout into visualizations +export const mergeLayoutAndVisualizations = ( + layout: Layout[], + newVisualizationList: VisualizationType[], + setPanelVisualizations: (value: React.SetStateAction) => void +) => { + let newPanelVisualizations: VisualizationType[] = []; + + for (var i = 0; i < newVisualizationList.length; i++) { + for (var j = 0; j < layout.length; j++) { + if (newVisualizationList[i].id == layout[j].i) { + newPanelVisualizations.push({ + ...newVisualizationList[i], + x: layout[j].x, + y: layout[j].y, + w: layout[j].w, + h: layout[j].h, + }); + } + } + } + setPanelVisualizations(newPanelVisualizations); +}; + /* Builds Final Query by adding time and query filters(From panel UI) to the original visualization query * -> Final Query is as follows: * -> finalQuery = indexPartOfQuery + timeQueryFilter + panelFilterQuery + filterPartOfQuery @@ -102,6 +133,27 @@ const pplServiceRequestor = async ( }); }; +//Fetched Saved Visualization By Id +const fetchVisualizationById = async ( + http: CoreStart['http'], + savedVisualizationId: string, + setIsError: React.Dispatch> +) => { + let savedVisualization = {} as SavedVisualizationType; + await http + .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations/${savedVisualizationId}`) + .then((res) => { + savedVisualization = res.visualization; + }) + .catch((err) => { + const errorMessage = 'Issue in fetching the saved Visualization by Id'; + setIsError(errorMessage); + console.error(errorMessage, err); + }); + + return savedVisualization; +}; + // Get PPL Query Response export const getQueryResponse = ( pplService: PPLService, @@ -122,7 +174,9 @@ export const getQueryResponse = ( try { finalQuery = queryAccumulator(query, timestampField, startTime, endTime, filterQuery); } catch (error) { - console.error('Issue in building final query', error.stack); + const errorMessage = 'Issue in building final query'; + setIsError(errorMessage); + console.error(errorMessage, error); setIsLoading(false); return; } @@ -130,6 +184,48 @@ export const getQueryResponse = ( pplServiceRequestor(pplService, finalQuery, type, setVisualizationData, setIsLoading, setIsError); }; +// Fetches savedVisualization by Id and runs getQueryResponse +export const renderSavedVisualization = async ( + http: CoreStart['http'], + pplService: PPLService, + savedVisualizationId: string, + startTime: string, + endTime: string, + filterQuery: string, + setVisualizationTitle: React.Dispatch>, + setVisualizationType: React.Dispatch>, + setVisualizationData: React.Dispatch>, + setIsLoading: React.Dispatch>, + setIsError: React.Dispatch> +) => { + setIsLoading(true); + setIsError(''); + + let visualization = {} as SavedVisualizationType; + visualization = await fetchVisualizationById(http, savedVisualizationId, setIsError); + + if (visualization.name) { + setVisualizationTitle(visualization.name); + } + + if (visualization.type) { + setVisualizationType(visualization.type); + } + + getQueryResponse( + pplService, + visualization.query, + visualization.type, + startTime, + endTime, + setVisualizationData, + setIsLoading, + setIsError, + filterQuery, + visualization.timeField + ); +}; + // Function to store recently used time filters and set start and end time. export const onTimeChange = ( start: ShortDate, @@ -191,48 +287,32 @@ export const isPPLFilterValid = ( }; // This function renders the visualzation based of its type -export const displayVisualization = (data: any, type: string) => { +export const displayVisualization = (data: any, type: string, editMode?: boolean) => { if (data === undefined) return; + const layoutObject = { + xaxis: { + automargin: true, + fixedrange: editMode ? true : false, + }, + yaxis: { + automargin: true, + fixedrange: editMode ? true : false, + }, + }; + let vizComponent!: JSX.Element; switch (type) { case 'bar': { - vizComponent = ( - - ); + vizComponent = ; break; } case 'horizontal_bar': { - vizComponent = ( - - ); + vizComponent = ; break; } case 'line': { - vizComponent = ( - - ); + vizComponent = ; break; } default: { diff --git a/public/components/custom_panels/home.tsx b/dashboards-observability/public/components/custom_panels/home.tsx similarity index 97% rename from public/components/custom_panels/home.tsx rename to dashboards-observability/public/components/custom_panels/home.tsx index 4742b0cd6..f0d7c92f1 100644 --- a/public/components/custom_panels/home.tsx +++ b/dashboards-observability/public/components/custom_panels/home.tsx @@ -59,7 +59,8 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } // Fetches all saved Custom Panels const fetchCustomPanels = () => { - return http + setLoading(true); + http .get(`${CUSTOM_PANELS_API_PREFIX}/panels`) .then((res) => { setcustomPanelData(res.panels); @@ -67,6 +68,7 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } .catch((err) => { console.error('Issue in fetching the operational panels', err.body.message); }); + setLoading(false); }; // Creates a new CustomPanel @@ -134,7 +136,10 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } }; // Clones an existing Custom Panel, return new Custom Panel id - const cloneCustomPanel = (clonedCustomPanelName: string, clonedCustomPanelId: string) => { + const cloneCustomPanel = ( + clonedCustomPanelName: string, + clonedCustomPanelId: string + ): Promise => { if (!isNameValid(clonedCustomPanelName)) { setToast('Invalid Operational Panel name', 'danger'); return Promise.reject(); @@ -161,6 +166,7 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } ]; }); setToast(`Operational Panel "${clonedCustomPanelName}" successfully created!`); + return res.clonePanelId; }) .catch((err) => { setToast( @@ -254,6 +260,7 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } chrome={chrome} parentBreadcrumb={parentBreadcrumb} renameCustomPanel={renameCustomPanel} + cloneCustomPanel={cloneCustomPanel} deleteCustomPanel={deleteCustomPanel} setToast={setToast} /> diff --git a/public/components/custom_panels/panel_modules/empty_panel.tsx b/dashboards-observability/public/components/custom_panels/panel_modules/empty_panel.tsx similarity index 100% rename from public/components/custom_panels/panel_modules/empty_panel.tsx rename to dashboards-observability/public/components/custom_panels/panel_modules/empty_panel.tsx diff --git a/public/components/custom_panels/panel_modules/panel_grid/index.ts b/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/index.ts similarity index 100% rename from public/components/custom_panels/panel_modules/panel_grid/index.ts rename to dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/index.ts diff --git a/public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss b/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss similarity index 100% rename from public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss rename to dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss diff --git a/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx b/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx similarity index 77% rename from public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx rename to dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx index 039f9f4e3..d7b77dcee 100644 --- a/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx +++ b/dashboards-observability/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx @@ -15,10 +15,11 @@ import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout'; import useObservable from 'react-use/lib/useObservable'; import { CoreStart } from '../../../../../../../src/core/public'; import PPLService from '../../../../services/requests/ppl'; -import { VisualizationContainer } from '../visualiation_container'; +import { VisualizationContainer } from '../visualization_container'; import { VisualizationType } from '../../../../../common/types/custom_panels'; import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; import './panel_grid.scss'; +import { mergeLayoutAndVisualizations } from '../../helpers/utils'; // HOC container to provide dynamic width for Grid layout const ResponsiveGridLayout = WidthProvider(Responsive); @@ -29,6 +30,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive); * Props taken in as params are: * http: http core service; * chrome: chrome core service; + * panelId: OpenPanel Id * panelVisualizations: list of panel visualizations * setPanelVisualizations: function to set panel visualizations * editMode: boolean to check if the panel is in edit mode @@ -39,7 +41,7 @@ const ResponsiveGridLayout = WidthProvider(Responsive); * cloneVisualization: function to clone a visualization in panel * pplFilterValue: string with panel PPL filter value * showFlyout: function to show the flyout - * removeVisualization: function to remove all the visualizations + * editActionType: Type of action done while clicking the edit button */ type Props = { @@ -53,15 +55,10 @@ type Props = { startTime: string; endTime: string; onRefresh: boolean; - cloneVisualization: ( - newVisualizationTitle: string, - pplQuery: string, - newVisualizationType: string, - newVisualizationTimeField: string - ) => void; + cloneVisualization: (visualzationTitle: string, savedVisualizationId: string) => void; pplFilterValue: string; showFlyout: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; - removeVisualization: (visualizationId: string) => void; + editActionType: string; }; export const PanelGrid = ({ @@ -78,16 +75,16 @@ export const PanelGrid = ({ cloneVisualization, pplFilterValue, showFlyout, - removeVisualization, + editActionType, }: Props) => { - const [layout, setLayout] = useState([]); - const [editedLayout, setEditedLayout] = useState([]); + const [currentLayout, setCurrentLayout] = useState([]); + const [postEditLayout, setPostEditLayout] = useState([]); const isLocked = useObservable(chrome.getIsNavDrawerLocked$()); // Reset Size of Visualizations when layout is changed const layoutChanged = (currentLayout: Layout[], allLayouts: Layouts) => { window.dispatchEvent(new Event('resize')); - setEditedLayout(currentLayout); + setPostEditLayout(currentLayout); }; // Reload the Layout @@ -102,9 +99,18 @@ export const PanelGrid = ({ static: !editMode, } as Layout; }); - setLayout(tempLayout); + setCurrentLayout(tempLayout); }; + // remove visualization from panel in edit mode + const removeVisualization = (visualizationId: string) => { + const newVisualizationList = _.reject(panelVisualizations, { + id: visualizationId, + }); + mergeLayoutAndVisualizations(postEditLayout, newVisualizationList, setPanelVisualizations); + }; + + // Save Visualization Layouts when not in edit mode anymore (after users saves the panel) const saveVisualizationLayouts = async (panelId: string, visualizationParams: any) => { return http .put(`${CUSTOM_PANELS_API_PREFIX}/visualizations/edit`, { @@ -126,12 +132,12 @@ export const PanelGrid = ({ if (editMode) { reloadLayout(); } else { - const newLayout = editedLayout.map((element) => { - return { ...element, static: true }; - }); - const visualizationParams = newLayout.map((layout) => _.omit(layout, ['static', 'moved'])); - setLayout(newLayout); - if (visualizationParams.length !== 0) saveVisualizationLayouts(panelId, visualizationParams); + if (editActionType === 'save') { + const visualizationParams = postEditLayout.map((layout) => + _.omit(layout, ['static', 'moved']) + ); + saveVisualizationLayouts(panelId, visualizationParams); + } } }, [editMode]); @@ -149,7 +155,7 @@ export const PanelGrid = ({ return ( (
void; + cloneVisualization: (visualzationTitle: string, savedVisualizationId: string) => void; pplFilterValue: string; showFlyout: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; removeVisualization: (visualizationId: string) => void; }; export const VisualizationContainer = ({ + http, editMode, visualizationId, - visualizationTitle, + savedVisualizationId, pplService, - query, - type, - timeField, fromTime, toTime, onRefresh, @@ -86,6 +81,8 @@ export const VisualizationContainer = ({ }: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [disablePopover, setDisablePopover] = useState(false); + const [visualizationTitle, setVisualizationTitle] = useState(''); + const [visualizationType, setVisualizationType] = useState(''); const [visualizationData, setVisualizationData] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isError, setIsError] = useState(''); @@ -108,7 +105,7 @@ export const VisualizationContainer = ({ disabled={disablePopover} onClick={() => { closeActionsMenu(); - cloneVisualization(visualizationTitle, query, type, timeField); + cloneVisualization(visualizationTitle, savedVisualizationId); }} > Duplicate @@ -116,17 +113,18 @@ export const VisualizationContainer = ({ ]; const loadVisaulization = async () => { - await getQueryResponse( + await renderSavedVisualization( + http, pplService, - query, - type, + savedVisualizationId, fromTime, toTime, + pplFilterValue, + setVisualizationTitle, + setVisualizationType, setVisualizationData, setIsLoading, - setIsError, - pplFilterValue, - timeField + setIsError ); }; @@ -149,11 +147,11 @@ export const VisualizationContainer = ({
) : ( - displayVisualization(visualizationData, type) + displayVisualization(visualizationData, visualizationType, editMode) )}
), - [onRefresh, isLoading, isError, visualizationData, type] + [onRefresh, isLoading, isError, visualizationData, visualizationType] ); useEffect(() => { diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/index.ts b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/index.ts similarity index 100% rename from public/components/custom_panels/panel_modules/visualization_flyout/index.ts rename to dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/index.ts diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss similarity index 100% rename from public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss rename to dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx similarity index 87% rename from public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx rename to dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx index 4b90d439b..23cf1af08 100644 --- a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx +++ b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx @@ -29,7 +29,6 @@ import { EuiSpacer, EuiText, EuiTitle, - htmlIdGenerator, ShortDate, } from '@elastic/eui'; import _ from 'lodash'; @@ -66,6 +65,7 @@ import './visualization_flyout.scss'; type Props = { panelId: string; + pplFilterValue: string; closeFlyout: () => void; start: ShortDate; end: ShortDate; @@ -84,6 +84,7 @@ type Props = { export const VisaulizationFlyout = ({ panelId, + pplFilterValue, closeFlyout, start, end, @@ -143,14 +144,8 @@ export const VisaulizationFlyout = ({ .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations/replace`, { body: JSON.stringify({ panelId: panelId, + savedVisualizationId: selectValue, oldVisualizationId: replaceVisualizationId, - newVisualization: { - id: 'panelViz_' + htmlIdGenerator()(), - title: newVisualizationTitle, - query: pplQuery, - type: newVisualizationType, - timeField: newVisualizationTimeField, - }, }), }) .then(async (res) => { @@ -166,13 +161,7 @@ export const VisaulizationFlyout = ({ .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { body: JSON.stringify({ panelId: panelId, - newVisualization: { - id: 'panelViz_' + htmlIdGenerator()(), - title: newVisualizationTitle, - query: pplQuery, - type: newVisualizationType, - timeField: newVisualizationTimeField, - }, + savedVisualizationId: selectValue, }), }) .then(async (res) => { @@ -199,7 +188,7 @@ export const VisaulizationFlyout = ({ setPreviewData, setPreviewLoading, setIsPreviewError, - '', + pplFilterValue, newVisualizationTimeField ); }; @@ -216,7 +205,7 @@ export const VisaulizationFlyout = ({ endDate={endDate} isInvalid={startDate > endDate} aria-label="Start date" - dateFormat={UI_DATE_FORMAT} + dateFormat={uiSettingsService.get('dateFormat')} /> } endDateControl={ @@ -226,7 +215,7 @@ export const VisaulizationFlyout = ({ endDate={endDate} isInvalid={startDate > endDate} aria-label="End date" - dateFormat={UI_DATE_FORMAT} + dateFormat={uiSettingsService.get('dateFormat')} /> } /> @@ -341,28 +330,30 @@ export const VisaulizationFlyout = ({ const previewTemplate = ( <> {timeRange} - {previewLoading ? ( - - ) : isPreviewError != '' ? ( -
- - - - -

Error in rendering the visualizaiton

-
- - -

{isPreviewError}

-
-
- ) : ( - - - {displayVisualization(previewData, newVisualizationType)} - - - )} + + + {previewLoading ? ( + + ) : isPreviewError != '' ? ( +
+ + + + +

Error in rendering the visualizaiton

+
+ + +

{isPreviewError}

+
+
+ ) : ( +
+ {displayVisualization(previewData, newVisualizationType)} +
+ )} +
+
); setPreviewArea(previewTemplate); diff --git a/public/components/explorer/data_grid.scss b/dashboards-observability/public/components/explorer/data_grid.scss similarity index 100% rename from public/components/explorer/data_grid.scss rename to dashboards-observability/public/components/explorer/data_grid.scss diff --git a/public/components/explorer/data_grid.tsx b/dashboards-observability/public/components/explorer/data_grid.tsx similarity index 100% rename from public/components/explorer/data_grid.tsx rename to dashboards-observability/public/components/explorer/data_grid.tsx diff --git a/public/components/explorer/docTable/detailTable/docDetailTable.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/docDetailTable.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/docDetailTable.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/docDetailTable.tsx diff --git a/public/components/explorer/docTable/detailTable/docDetailTitle.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/docDetailTitle.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/docDetailTitle.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/docDetailTitle.tsx diff --git a/public/components/explorer/docTable/detailTable/table_helper.test.ts b/dashboards-observability/public/components/explorer/docTable/detailTable/table_helper.test.ts similarity index 100% rename from public/components/explorer/docTable/detailTable/table_helper.test.ts rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_helper.test.ts diff --git a/public/components/explorer/docTable/detailTable/table_helper.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_helper.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_helper.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_helper.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_btn_collapse.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_collapse.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_btn_collapse.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_collapse.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_btn_filter_add.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_filter_add.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_btn_filter_add.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_filter_add.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_btn_filter_exists.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_filter_exists.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_btn_filter_exists.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_filter_exists.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_btn_filter_remove.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_filter_remove.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_btn_filter_remove.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_filter_remove.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_btn_toggle_column.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_toggle_column.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_btn_toggle_column.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_btn_toggle_column.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_icon_no_mapping.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_icon_no_mapping.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_icon_no_mapping.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_icon_no_mapping.tsx diff --git a/public/components/explorer/docTable/detailTable/table_row_icon_underscore.tsx b/dashboards-observability/public/components/explorer/docTable/detailTable/table_row_icon_underscore.tsx similarity index 100% rename from public/components/explorer/docTable/detailTable/table_row_icon_underscore.tsx rename to dashboards-observability/public/components/explorer/docTable/detailTable/table_row_icon_underscore.tsx diff --git a/public/components/explorer/docTable/docViewRow.tsx b/dashboards-observability/public/components/explorer/docTable/docViewRow.tsx similarity index 100% rename from public/components/explorer/docTable/docViewRow.tsx rename to dashboards-observability/public/components/explorer/docTable/docViewRow.tsx diff --git a/public/components/explorer/docTable/docViewer.tsx b/dashboards-observability/public/components/explorer/docTable/docViewer.tsx similarity index 100% rename from public/components/explorer/docTable/docViewer.tsx rename to dashboards-observability/public/components/explorer/docTable/docViewer.tsx diff --git a/public/components/explorer/docTable/index.ts b/dashboards-observability/public/components/explorer/docTable/index.ts similarity index 100% rename from public/components/explorer/docTable/index.ts rename to dashboards-observability/public/components/explorer/docTable/index.ts diff --git a/public/components/explorer/docTable/json_code_block/__snapshots__/json_code_block.test.tsx.snap b/dashboards-observability/public/components/explorer/docTable/json_code_block/__snapshots__/json_code_block.test.tsx.snap similarity index 100% rename from public/components/explorer/docTable/json_code_block/__snapshots__/json_code_block.test.tsx.snap rename to dashboards-observability/public/components/explorer/docTable/json_code_block/__snapshots__/json_code_block.test.tsx.snap diff --git a/public/components/explorer/docTable/json_code_block/json_code_block.test.tsx b/dashboards-observability/public/components/explorer/docTable/json_code_block/json_code_block.test.tsx similarity index 100% rename from public/components/explorer/docTable/json_code_block/json_code_block.test.tsx rename to dashboards-observability/public/components/explorer/docTable/json_code_block/json_code_block.test.tsx diff --git a/public/components/explorer/docTable/json_code_block/json_code_block.tsx b/dashboards-observability/public/components/explorer/docTable/json_code_block/json_code_block.tsx similarity index 100% rename from public/components/explorer/docTable/json_code_block/json_code_block.tsx rename to dashboards-observability/public/components/explorer/docTable/json_code_block/json_code_block.tsx diff --git a/public/components/explorer/event_analytics.tsx b/dashboards-observability/public/components/explorer/event_analytics.tsx similarity index 61% rename from public/components/explorer/event_analytics.tsx rename to dashboards-observability/public/components/explorer/event_analytics.tsx index 38d4c88fe..a873fed53 100644 --- a/public/components/explorer/event_analytics.tsx +++ b/dashboards-observability/public/components/explorer/event_analytics.tsx @@ -9,8 +9,10 @@ * GitHub history for details. */ -import React from 'react'; +import React, { useState, ReactChild } from 'react'; import { HashRouter, Route, Switch } from 'react-router-dom'; +import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { EuiGlobalToastList } from '@elastic/eui'; import { LogExplorer } from './log_explorer'; import { Home as EventExplorerHome } from './home'; import { renderPageWithSidebar } from '../common/side_nav'; @@ -21,17 +23,33 @@ export const EventAnalytics = ({ pplService, dslService, savedObjects, + timestampUtils, http, ...props }: any) => { + const [toasts, setToasts] = useState>([]); + const eventAnalyticsBreadcrumb = { text: 'Event analytics', href: '#/event_analytics', }; + const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { + if (!text) text = ''; + setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); + }; + return ( - + <> + { + setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); + }} + toastLifeTimeMs={6000} + /> + ); }} @@ -65,10 +85,18 @@ export const EventAnalytics = ({ href: '#/event_analytics', } ]); - return renderPageWithSidebar(); + return renderPageWithSidebar( + + ); }} /> + ); } \ No newline at end of file diff --git a/public/components/explorer/explorer.tsx b/dashboards-observability/public/components/explorer/explorer.tsx similarity index 71% rename from public/components/explorer/explorer.tsx rename to dashboards-observability/public/components/explorer/explorer.tsx index 992f29ec2..188729a2c 100644 --- a/public/components/explorer/explorer.tsx +++ b/dashboards-observability/public/components/explorer/explorer.tsx @@ -9,31 +9,25 @@ * GitHub history for details. */ -import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; +import React, { useState, useMemo, useEffect, useRef } from 'react'; import { batch, useDispatch, useSelector } from 'react-redux'; import { uniqueId, isEmpty, cloneDeep, isEqual, - concat + has } from 'lodash'; import { FormattedMessage } from '@osd/i18n/react'; import { EuiText, - EuiButton, EuiButtonIcon, EuiTabbedContent, EuiTabbedContentTab, EuiFlexGroup, EuiFlexItem, - EuiPopover, - EuiPopoverTitle, - EuiPopoverFooter, - EuiButtonEmpty, - htmlIdGenerator } from '@elastic/eui'; import classNames from 'classnames'; import { Search } from '../common/search/search'; @@ -44,7 +38,6 @@ import { NoResults } from './no_results'; import { HitsCounter } from './hits_counter/hits_counter'; import { TimechartHeader } from './timechart_header'; import { ExplorerVisualizations } from './visualizations'; -import { IndexPicker } from '../common/search/searchindex'; import { IField, IQueryTab @@ -57,10 +50,12 @@ import { RAW_QUERY, SELECTED_DATE_RANGE, SELECTED_FIELDS, + SELECTED_TIMESTAMP, UNSELECTED_FIELDS, AVAILABLE_FIELDS, INDEX, - TIME_INTERVAL_OPTIONS + TIME_INTERVAL_OPTIONS, + HAS_SAVED_TIMESTAMP } from '../../../common/constants/explorer'; import { PPL_STATS_REGEX } from '../../../common/constants/shared'; import { @@ -87,6 +82,7 @@ import { selectExplorerVisualization } from './slices/visualization_slice'; import PPLService from '../../services/requests/ppl'; import DSLService from '../../services/requests/dsl'; import SavedObjects from '../../services/saved_objects/event_analytics/saved_objects'; +import TimestampUtils from 'public/services/timestamp/timestamp'; const TAB_EVENT_ID = uniqueId(TAB_EVENT_ID_TXT_PFX); const TAB_CHART_ID = uniqueId(TAB_CHART_ID_TXT_PFX); @@ -96,14 +92,22 @@ interface IExplorerProps { dslService: DSLService; tabId: string; savedObjects: SavedObjects; + timestampUtils: TimestampUtils; + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void; } export const Explorer = ({ pplService, dslService, - http, tabId, - savedObjects + savedObjects, + timestampUtils, + setToast }: IExplorerProps) => { const dispatch = useDispatch(); @@ -130,66 +134,101 @@ export const Explorer = ({ const explorerFields = useSelector(selectFields)[tabId]; const countDistribution = useSelector(selectCountDistribution)[tabId]; const explorerVisualizations = useSelector(selectExplorerVisualization)[tabId]; - const [selectedContentTabId, setSelectedContentTab] = useState(TAB_EVENT_ID); + + const [selectedContentTabId, setSelectedContentTab] = useState(TAB_EVENT_ID); const [selectedCustomPanelOptions, setSelectedCustomPanelOptions] = useState([]); const [selectedPanelName, setSelectedPanelName] = useState(''); - const [curVisId, setCurVisId] = useState('bar'); - const [isPanelTextFieldInvalid, setIsPanelTextFieldInvalid ] = useState(false); - const [liveStreamChecked, setLiveStreamChecked] = useState(false); - const [isSidebarClosed, setIsSidebarClosed] = useState(false); - const [fixedScrollEl, setFixedScrollEl] = useState(); + const [curVisId, setCurVisId] = useState('bar'); + const [prevIndex, setPrevIndex] = useState(''); + const [isPanelTextFieldInvalid, setIsPanelTextFieldInvalid ] = useState(false); + const [isSidebarClosed, setIsSidebarClosed] = useState(false); + const queryRef = useRef(); const selectedPanelNameRef = useRef(); const explorerFieldsRef = useRef(); queryRef.current = query; selectedPanelNameRef.current = selectedPanelName; explorerFieldsRef.current = explorerFields; - - const fixedScrollRef = useCallback( - (node: HTMLElement) => { - if (node !== null) { - setFixedScrollEl(node); - } - }, - [setFixedScrollEl] - ); - const composeFinalQuery = (curQuery: any) => { + const composeFinalQuery = (curQuery: any, timeField: string) => { if (isEmpty(curQuery![RAW_QUERY])) return ''; return insertDateRangeToQuery({ rawQuery: curQuery![RAW_QUERY], - startTime: curQuery!['selectedDateRange'][0], - endTime: curQuery!['selectedDateRange'][1] + startTime: curQuery![SELECTED_DATE_RANGE][0], + endTime: curQuery![SELECTED_DATE_RANGE][1], + timeField, }); }; const fetchData = async () => { const curQuery = queryRef.current; const rawQueryStr = curQuery![RAW_QUERY]; + const curIndex = getIndexPatternFromRawQuery(rawQueryStr); + if (isEmpty(rawQueryStr)) return; - const index = getIndexPatternFromRawQuery(rawQueryStr); - if (!isEmpty(index)) getAvailableFields(`search source=${index}`); + if (isEmpty(curIndex)) { + setToast('Query does not include vaild index.', 'danger'); + return; + } + + let curTimestamp = ''; + let hasSavedTimestamp = false; + + // determines timestamp for search + if (isEmpty(curQuery![SELECTED_TIMESTAMP]) || !isEqual(curIndex, prevIndex)) { + const savedTimestamps = await savedObjects.fetchSavedObjects({ + objectId: curIndex + }).catch((error: any) => { + console.log(`Unable to get saved timestamp for this index: ${error.message}`); + }); + if (savedTimestamps?.observabilityObjectList[0]?.timestamp?.name) { + // from saved objects + hasSavedTimestamp = true; + curTimestamp = savedTimestamps.observabilityObjectList[0].timestamp.name; + } else { + // from index mappings + hasSavedTimestamp = false; + const timestamps = await timestampUtils.getTimestamp(curIndex); + curTimestamp = timestamps!.default_timestamp + } + } - const finalQuery = composeFinalQuery(curQuery); + if (isEmpty(curTimestamp)) { + setToast('Index does not contain time field.', 'danger'); + return; + } + + // compose final query + const finalQuery = composeFinalQuery(curQuery, curTimestamp || curQuery![SELECTED_TIMESTAMP]); await dispatch(changeQuery({ tabId, query: { finalQuery, + [SELECTED_TIMESTAMP]: curTimestamp || curQuery![SELECTED_TIMESTAMP], + [HAS_SAVED_TIMESTAMP]: hasSavedTimestamp } })); - + + // search if (rawQueryStr.match(PPL_STATS_REGEX)) { getVisualizations(); + getAvailableFields(`search source=${curIndex}`); } else { getEvents(); getCountVisualizations('h'); } + + // for comparing usage if for the same tab, user changed index from one to another + setPrevIndex(curTimestamp || curQuery![SELECTED_TIMESTAMP]); }; + // should run in two usecases + // 1. load explorer for the first time + // 2. when overrides default timestamp useEffect(() => { fetchData(); - }, []); + }, [query[SELECTED_TIMESTAMP]]); const handleAddField = (field: IField) => toggleFields(field, AVAILABLE_FIELDS, SELECTED_FIELDS); @@ -245,6 +284,56 @@ export const Explorer = ({ 'col-md-12': isSidebarClosed, }); + const handleOverrideTimestamp = async (timestamp: IField) => { + const curQuery = queryRef.current; + const rawQueryStr = curQuery![RAW_QUERY]; + const curIndex = getIndexPatternFromRawQuery(rawQueryStr); + const requests = { + index: curIndex, + name: timestamp.name, + type: timestamp.type, + dsl_type: 'date' + }; + if (isEmpty(rawQueryStr) || isEmpty(curIndex)) { + setToast('Cannot override timestamp because there was no valid index found.', 'danger'); + return; + } + + let saveTimestampRes; + if (curQuery![HAS_SAVED_TIMESTAMP]) { + saveTimestampRes = await savedObjects.updateTimestamp({ + ...requests + }) + .then((res: any) => { + setToast(`Timestamp has been overridden successfully.`, 'success'); + return res; + }) + .catch((error: any) => { + setToast(`Cannot override timestamp, error: ${error.message}`, 'danger'); + }); + } else { + saveTimestampRes = await savedObjects.createSavedTimestamp({ + ...requests + }) + .then((res: any) => { + setToast(`Timestamp has been overridden successfully.`, 'success'); + return res; + }) + .catch((error: any) => { + setToast(`Cannot override timestamp, error: ${error.message}`, 'danger'); + }); + } + + if (!has(saveTimestampRes, 'objectId')) return; + + await dispatch(changeQuery({ + tabId, + query: { + [SELECTED_TIMESTAMP]: '' + } + })); + }; + const getMainContent = () => { return ( @@ -260,6 +349,8 @@ export const Explorer = ({ handleAddField(field) } handleRemoveField={ (field: IField) => handleRemoveField(field) } /> @@ -324,7 +415,6 @@ export const Explorer = ({

{ + const handleSavingObject = async () => { const currQuery = queryRef.current; const currFields = explorerFieldsRef.current; - if (isEmpty(currQuery![RAW_QUERY])) return; + if (isEmpty(currQuery![RAW_QUERY])) { + setToast('No query to save.', 'danger'); + return; + }; if (isEmpty(selectedPanelNameRef.current)) { setIsPanelTextFieldInvalid(true); + setToast('Name field cannot be empty.', 'danger'); return; - } else { - setIsPanelTextFieldInvalid(false); } + setIsPanelTextFieldInvalid(false); if (isEqual(selectedContentTabId, TAB_EVENT_ID)) { @@ -460,7 +553,14 @@ export const Explorer = ({ query: currQuery![RAW_QUERY], fields: currFields![SELECTED_FIELDS], dateRange: currQuery![SELECTED_DATE_RANGE], - name: selectedPanelNameRef.current + name: selectedPanelNameRef.current, + timestamp: currQuery![SELECTED_TIMESTAMP] + }) + .then((res: any) => { + setToast(`Query '${selectedPanelNameRef.current}' has been successfully saved.`, 'success'); + }) + .catch((error: any) => { + setToast(`Cannot save query '${selectedPanelNameRef.current}', error: ${error.message}`, 'danger'); }); // to-dos - update selected custom panel @@ -471,23 +571,36 @@ export const Explorer = ({ } else if (isEqual(selectedContentTabId, TAB_CHART_ID)) { // create new saved visualization - savedObjects.createSavedVisualization({ + const savingVisRes = await savedObjects.createSavedVisualization({ query: currQuery![RAW_QUERY], fields: currFields![SELECTED_FIELDS], dateRange: currQuery![SELECTED_DATE_RANGE], type: curVisId, - name: selectedPanelNameRef.current + name: selectedPanelNameRef.current, + timestamp: currQuery![SELECTED_TIMESTAMP] + }) + .then((res: any) => { + setToast(`Visualization '${selectedPanelNameRef.current}' has been successfully saved.`, 'success'); + return res; + }) + .catch((error: any) => { + setToast(`Cannot save Visualization '${selectedPanelNameRef.current}', error: ${error.message}`, 'danger'); }); + if (!has(savingVisRes, 'objectId')) return; + // update custom panel - visualization if (!isEmpty(selectedCustomPanelOptions)) { savedObjects.bulkUpdateCustomPanel({ selectedCustomPanels: selectedCustomPanelOptions, - query: currQuery![RAW_QUERY], - type: curVisId, - timeField: !isEmpty(currQuery!['selectedTimestamp']) ? currQuery!['selectedTimestamp'] : 'utc_time', // temprary - name: selectedPanelNameRef.current + savedVisualizationId: savingVisRes?.objectId + }) + .then((res: any) => { + setToast(`Visualization '${selectedPanelNameRef.current}' has been successfully saved to operation panels.`, 'success'); + }) + .catch((error: any) => { + setToast(`Cannot add Visualization '${selectedPanelNameRef.current}' to operation panels, error: ${error.message}`, 'danger'); }); } } @@ -500,7 +613,7 @@ export const Explorer = ({
{ handleQueryChange(query, index) } } handleQuerySearch={ () => { handleQuerySearch() } } dslService = { dslService } @@ -516,11 +629,6 @@ export const Explorer = ({ savedObjects={ savedObjects } showSavePanelOptionsList={ isEqual(selectedContentTabId, TAB_CHART_ID) } /> - {/* { handleQueryChange(query, index) } } - /> */} { @@ -45,11 +54,11 @@ export const Home = (props: IHomeProps) => { pplService, dslService, savedObjects, - http } = props; const history = useHistory(); const dispatch = useDispatch(); - const query = useSelector(selectQueries)[initialTabId][RAW_QUERY]; + const [searchQuery, setSearchQuery] = useState(''); + const [selectedDateRange, setSelectedDateRange] = useState>(['now-15m', 'now']); const [savedHistories, setSavedHistories] = useState([]); const fetchHistories = async () => { @@ -62,10 +71,49 @@ export const Home = (props: IHomeProps) => { setSavedHistories(res['observabilityObjectList'] || []); }; + const addNewTab = async () => { + //get a new tabId + const tabId = uniqueId(TAB_ID_TXT_PFX); + + // create a new tab + await batch(() => { + dispatch(initQuery({ tabId, })); + dispatch(initQueryResult({ tabId, })); + dispatch(initFields({ tabId, })); + dispatch(addTab({ tabId, })); + }); + + return tabId; + }; + useEffect(() => { fetchHistories(); }, []); + const addSearchInput = async (tabId: string) => { + dispatch(changeQuery({ + tabId, + query: { + [RAW_QUERY]: searchQuery, + [SELECTED_DATE_RANGE]: selectedDateRange + } + })); + } + + const handleQuerySearch = async () => { + // create new tab + const newTabId = await addNewTab(); + + // update this new tab with data + await addSearchInput(newTabId); + + // redirect to explorer + history.push('/event_analytics/explorer'); + } + + const handleQueryChange = async (query: string, index: string) => setSearchQuery(query); + + const handleTimePickerChange = async (timeRange: Array) => setSelectedDateRange(timeRange); return (
@@ -81,23 +129,14 @@ export const Home = (props: IHomeProps) => { { - dispatch(changeQuery({ - tabId: initialTabId, - query: { - [RAW_QUERY]: query, - [INDEX]: index - } - })); - } } - handleQuerySearch={ () => { - history.push('/event_analytics/explorer'); - } } + query={ searchQuery } + handleQueryChange={ handleQueryChange } + handleQuerySearch={ handleQuerySearch } + handleTimePickerChange={ handleTimePickerChange } pplService={ pplService } dslService={ dslService } - startTime={ 'now-15m' } - endTime={ 'now' } + startTime={ selectedDateRange[0] } + endTime={ selectedDateRange[1] } setStartTime={ () => {} } setEndTime={ () => {} } setIsOutputStale={ () => {} } diff --git a/public/components/explorer/hooks/index.ts b/dashboards-observability/public/components/explorer/hooks/index.ts similarity index 100% rename from public/components/explorer/hooks/index.ts rename to dashboards-observability/public/components/explorer/hooks/index.ts diff --git a/public/components/explorer/hooks/use_fetch_events.ts b/dashboards-observability/public/components/explorer/hooks/use_fetch_events.ts similarity index 100% rename from public/components/explorer/hooks/use_fetch_events.ts rename to dashboards-observability/public/components/explorer/hooks/use_fetch_events.ts diff --git a/public/components/explorer/hooks/use_fetch_visualizations.ts b/dashboards-observability/public/components/explorer/hooks/use_fetch_visualizations.ts similarity index 96% rename from public/components/explorer/hooks/use_fetch_visualizations.ts rename to dashboards-observability/public/components/explorer/hooks/use_fetch_visualizations.ts index 79e3e3c32..87c9cc134 100644 --- a/public/components/explorer/hooks/use_fetch_visualizations.ts +++ b/dashboards-observability/public/components/explorer/hooks/use_fetch_visualizations.ts @@ -73,7 +73,7 @@ export const useFetchVisualizations = ({ const cur = queriesRef.current; const rawQuery = cur![requestParams.tabId][FINAL_QUERY]; fetchVisualizations({ - query: `${rawQuery} | stats count() by span(timestamp, '1${interval = interval ? interval: 'm' }')` }, + query: `${rawQuery} | stats count() by span(timestamp, 1${interval = interval ? interval: 'm' })` }, 'viz', (res: any) => { dispatch(renderCountDis({ diff --git a/public/components/explorer/log_explorer.scss b/dashboards-observability/public/components/explorer/log_explorer.scss similarity index 100% rename from public/components/explorer/log_explorer.scss rename to dashboards-observability/public/components/explorer/log_explorer.scss diff --git a/public/components/explorer/log_explorer.tsx b/dashboards-observability/public/components/explorer/log_explorer.tsx similarity index 96% rename from public/components/explorer/log_explorer.tsx rename to dashboards-observability/public/components/explorer/log_explorer.tsx index fedde6f06..fafb3af32 100644 --- a/public/components/explorer/log_explorer.tsx +++ b/dashboards-observability/public/components/explorer/log_explorer.tsx @@ -52,7 +52,9 @@ export const LogExplorer = ({ pplService, dslService, savedObjects, - http + timestampUtils, + http, + setToast }: ILogExplorerProps) => { const dispatch = useDispatch(); @@ -77,7 +79,7 @@ export const LogExplorer = ({ const handleTabClose = (TabIdToBeClosed: string) => { if (tabIds.length === 1) { - console.log('Have to have at least one tab'); + setToast('Have to have at least one tab', 'danger'); return; } @@ -145,9 +147,10 @@ export const LogExplorer = ({ key={`explorer_${tabId}`} pplService={ pplService } dslService={ dslService } - http={ http } tabId={ tabId } savedObjects={ savedObjects } + timestampUtils={ timestampUtils } + setToast={ setToast } /> ) }; diff --git a/public/components/explorer/no_results.tsx b/dashboards-observability/public/components/explorer/no_results.tsx similarity index 100% rename from public/components/explorer/no_results.tsx rename to dashboards-observability/public/components/explorer/no_results.tsx diff --git a/public/components/explorer/reducers/fetch_reducers.ts b/dashboards-observability/public/components/explorer/reducers/fetch_reducers.ts similarity index 100% rename from public/components/explorer/reducers/fetch_reducers.ts rename to dashboards-observability/public/components/explorer/reducers/fetch_reducers.ts diff --git a/public/components/explorer/reducers/index.ts b/dashboards-observability/public/components/explorer/reducers/index.ts similarity index 100% rename from public/components/explorer/reducers/index.ts rename to dashboards-observability/public/components/explorer/reducers/index.ts diff --git a/public/components/explorer/reducers/query_reducers.ts b/dashboards-observability/public/components/explorer/reducers/query_reducers.ts similarity index 100% rename from public/components/explorer/reducers/query_reducers.ts rename to dashboards-observability/public/components/explorer/reducers/query_reducers.ts diff --git a/public/components/explorer/save_panel/index.ts b/dashboards-observability/public/components/explorer/save_panel/index.ts similarity index 100% rename from public/components/explorer/save_panel/index.ts rename to dashboards-observability/public/components/explorer/save_panel/index.ts diff --git a/public/components/explorer/save_panel/savePanel.tsx b/dashboards-observability/public/components/explorer/save_panel/savePanel.tsx similarity index 92% rename from public/components/explorer/save_panel/savePanel.tsx rename to dashboards-observability/public/components/explorer/save_panel/savePanel.tsx index ce909a336..42fd9774f 100644 --- a/public/components/explorer/save_panel/savePanel.tsx +++ b/dashboards-observability/public/components/explorer/save_panel/savePanel.tsx @@ -52,8 +52,12 @@ export const SavePanel = ({ const [options, setOptions] = useState([]); const getCustomPabnelList = async (savedObjects: SavedObjects) => { - const optionRes = await savedObjects.fetchCustomPanels(); - setOptions(optionRes['panels']); + const optionRes = await savedObjects.fetchCustomPanels() + .then((res: any) => { + return res; + }) + .catch((error: any) => console.error(error)); + setOptions(optionRes?.panels || []); }; useEffect(() => { diff --git a/public/components/explorer/sidebar/field.tsx b/dashboards-observability/public/components/explorer/sidebar/field.tsx similarity index 81% rename from public/components/explorer/sidebar/field.tsx rename to dashboards-observability/public/components/explorer/sidebar/field.tsx index 4c39d416c..2d9749638 100644 --- a/public/components/explorer/sidebar/field.tsx +++ b/dashboards-observability/public/components/explorer/sidebar/field.tsx @@ -11,10 +11,13 @@ import React, { useState } from 'react'; import { i18n } from '@osd/i18n'; +import { isEqual } from 'lodash'; import { EuiPopover, EuiButtonIcon, - EuiToolTip + EuiToolTip, + EuiButton, + EuiMark } from '@elastic/eui'; import { FieldButton } from '../../common/field_button'; import { FieldIcon } from '../../common/field_icon'; @@ -22,6 +25,8 @@ import { IField } from '../../../../common/types/explorer'; interface IFieldProps { field: IField; + selectedTimestamp: string; + handleOverrideTimestamp: (timestamp: { name: string, type: string }) => void; selected: boolean; showToggleButton: boolean; onToggleField: (field: IField) => void; @@ -31,6 +36,8 @@ export const Field = (props: IFieldProps) => { const { field, + selectedTimestamp, + handleOverrideTimestamp, selected, showToggleButton = true, onToggleField @@ -70,6 +77,19 @@ export const Field = (props: IFieldProps) => { ) } > + <> + { isEqual(field.type, 'timestamp') ? + isEqual(selectedTimestamp, field.name) ? { 'Default Timestamp' } : + handleOverrideTimestamp(field)} + > + { 'Override' } + : null + } { showToggleButton ? ( { data-test-subj={`fieldToggle-${field.name}`} aria-label={ selected ? removeLabelAria : addLabelAria } /> - ) : <> + ) : null } + ); }; @@ -108,7 +129,7 @@ export const Field = (props: IFieldProps) => { isActive={ isFieldDetailsOpen } dataTestSubj={`field-${field.name}-showDetails`} fieldIcon={} fieldName={ void; handleAddField: (field: IField) => void; handleRemoveField: (field: IField) => void; } @@ -36,6 +38,8 @@ export const Sidebar = (props: ISidebarProps) => { const { explorerFields, explorerData, + selectedTimestamp, + handleOverrideTimestamp, handleAddField, handleRemoveField } = props; @@ -90,6 +94,8 @@ export const Sidebar = (props: ISidebarProps) => { > { > @@ -188,6 +196,8 @@ export const Sidebar = (props: ISidebarProps) => { > diff --git a/public/components/explorer/skip_bottom_button/index.ts b/dashboards-observability/public/components/explorer/skip_bottom_button/index.ts similarity index 100% rename from public/components/explorer/skip_bottom_button/index.ts rename to dashboards-observability/public/components/explorer/skip_bottom_button/index.ts diff --git a/public/components/explorer/skip_bottom_button/skip_bottom_button.test.tsx b/dashboards-observability/public/components/explorer/skip_bottom_button/skip_bottom_button.test.tsx similarity index 100% rename from public/components/explorer/skip_bottom_button/skip_bottom_button.test.tsx rename to dashboards-observability/public/components/explorer/skip_bottom_button/skip_bottom_button.test.tsx diff --git a/public/components/explorer/skip_bottom_button/skip_bottom_button.tsx b/dashboards-observability/public/components/explorer/skip_bottom_button/skip_bottom_button.tsx similarity index 100% rename from public/components/explorer/skip_bottom_button/skip_bottom_button.tsx rename to dashboards-observability/public/components/explorer/skip_bottom_button/skip_bottom_button.tsx diff --git a/public/components/explorer/slices/count_distribution_slice.ts b/dashboards-observability/public/components/explorer/slices/count_distribution_slice.ts similarity index 100% rename from public/components/explorer/slices/count_distribution_slice.ts rename to dashboards-observability/public/components/explorer/slices/count_distribution_slice.ts diff --git a/public/components/explorer/slices/field_slice.ts b/dashboards-observability/public/components/explorer/slices/field_slice.ts similarity index 100% rename from public/components/explorer/slices/field_slice.ts rename to dashboards-observability/public/components/explorer/slices/field_slice.ts diff --git a/public/components/explorer/slices/query_result_slice.ts b/dashboards-observability/public/components/explorer/slices/query_result_slice.ts similarity index 100% rename from public/components/explorer/slices/query_result_slice.ts rename to dashboards-observability/public/components/explorer/slices/query_result_slice.ts diff --git a/public/components/explorer/slices/query_slice.ts b/dashboards-observability/public/components/explorer/slices/query_slice.ts similarity index 96% rename from public/components/explorer/slices/query_slice.ts rename to dashboards-observability/public/components/explorer/slices/query_slice.ts index e09b3e8d9..63751dc0a 100644 --- a/public/components/explorer/slices/query_slice.ts +++ b/dashboards-observability/public/components/explorer/slices/query_slice.ts @@ -18,13 +18,15 @@ import { FINAL_QUERY, SELECTED_DATE_RANGE, REDUX_EXPL_SLICE_QUERIES, - INDEX + INDEX, + SELECTED_TIMESTAMP } from '../../../../common/constants/explorer'; const initialQueryState = { [RAW_QUERY]: '', [FINAL_QUERY]: '', [INDEX]: '', + [SELECTED_TIMESTAMP]: '', [SELECTED_DATE_RANGE]: ['now-15m', 'now'] }; diff --git a/public/components/explorer/slices/query_tab_slice.ts b/dashboards-observability/public/components/explorer/slices/query_tab_slice.ts similarity index 100% rename from public/components/explorer/slices/query_tab_slice.ts rename to dashboards-observability/public/components/explorer/slices/query_tab_slice.ts diff --git a/public/components/explorer/slices/visualization_slice.ts b/dashboards-observability/public/components/explorer/slices/visualization_slice.ts similarity index 100% rename from public/components/explorer/slices/visualization_slice.ts rename to dashboards-observability/public/components/explorer/slices/visualization_slice.ts diff --git a/public/components/explorer/timechart_header/index.ts b/dashboards-observability/public/components/explorer/timechart_header/index.ts similarity index 100% rename from public/components/explorer/timechart_header/index.ts rename to dashboards-observability/public/components/explorer/timechart_header/index.ts diff --git a/public/components/explorer/timechart_header/timechart_header.test.tsx b/dashboards-observability/public/components/explorer/timechart_header/timechart_header.test.tsx similarity index 100% rename from public/components/explorer/timechart_header/timechart_header.test.tsx rename to dashboards-observability/public/components/explorer/timechart_header/timechart_header.test.tsx diff --git a/public/components/explorer/timechart_header/timechart_header.tsx b/dashboards-observability/public/components/explorer/timechart_header/timechart_header.tsx similarity index 100% rename from public/components/explorer/timechart_header/timechart_header.tsx rename to dashboards-observability/public/components/explorer/timechart_header/timechart_header.tsx diff --git a/public/components/explorer/visualizations/_mixins.scss b/dashboards-observability/public/components/explorer/visualizations/_mixins.scss similarity index 100% rename from public/components/explorer/visualizations/_mixins.scss rename to dashboards-observability/public/components/explorer/visualizations/_mixins.scss diff --git a/public/components/explorer/visualizations/_variables.scss b/dashboards-observability/public/components/explorer/visualizations/_variables.scss similarity index 100% rename from public/components/explorer/visualizations/_variables.scss rename to dashboards-observability/public/components/explorer/visualizations/_variables.scss diff --git a/public/components/explorer/visualizations/app.scss b/dashboards-observability/public/components/explorer/visualizations/app.scss similarity index 100% rename from public/components/explorer/visualizations/app.scss rename to dashboards-observability/public/components/explorer/visualizations/app.scss diff --git a/public/components/explorer/visualizations/assets/chart_bar.tsx b/dashboards-observability/public/components/explorer/visualizations/assets/chart_bar.tsx similarity index 100% rename from public/components/explorer/visualizations/assets/chart_bar.tsx rename to dashboards-observability/public/components/explorer/visualizations/assets/chart_bar.tsx diff --git a/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx b/dashboards-observability/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx similarity index 100% rename from public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx rename to dashboards-observability/public/components/explorer/visualizations/assets/chart_bar_horizontal.tsx diff --git a/public/components/explorer/visualizations/assets/chart_line.tsx b/dashboards-observability/public/components/explorer/visualizations/assets/chart_line.tsx similarity index 100% rename from public/components/explorer/visualizations/assets/chart_line.tsx rename to dashboards-observability/public/components/explorer/visualizations/assets/chart_line.tsx diff --git a/public/components/explorer/visualizations/assets/legend.tsx b/dashboards-observability/public/components/explorer/visualizations/assets/legend.tsx similarity index 100% rename from public/components/explorer/visualizations/assets/legend.tsx rename to dashboards-observability/public/components/explorer/visualizations/assets/legend.tsx diff --git a/public/components/explorer/visualizations/config_panel/configPanelItem.tsx b/dashboards-observability/public/components/explorer/visualizations/config_panel/configPanelItem.tsx similarity index 100% rename from public/components/explorer/visualizations/config_panel/configPanelItem.tsx rename to dashboards-observability/public/components/explorer/visualizations/config_panel/configPanelItem.tsx diff --git a/public/components/explorer/visualizations/config_panel/config_panel.scss b/dashboards-observability/public/components/explorer/visualizations/config_panel/config_panel.scss similarity index 100% rename from public/components/explorer/visualizations/config_panel/config_panel.scss rename to dashboards-observability/public/components/explorer/visualizations/config_panel/config_panel.scss diff --git a/public/components/explorer/visualizations/config_panel/config_panel.tsx b/dashboards-observability/public/components/explorer/visualizations/config_panel/config_panel.tsx similarity index 100% rename from public/components/explorer/visualizations/config_panel/config_panel.tsx rename to dashboards-observability/public/components/explorer/visualizations/config_panel/config_panel.tsx diff --git a/public/components/explorer/visualizations/config_panel/index.ts b/dashboards-observability/public/components/explorer/visualizations/config_panel/index.ts similarity index 100% rename from public/components/explorer/visualizations/config_panel/index.ts rename to dashboards-observability/public/components/explorer/visualizations/config_panel/index.ts diff --git a/public/components/explorer/visualizations/count_distribution/count_distribution.tsx b/dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx similarity index 67% rename from public/components/explorer/visualizations/count_distribution/count_distribution.tsx rename to dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx index a31df4287..c332986b6 100644 --- a/public/components/explorer/visualizations/count_distribution/count_distribution.tsx +++ b/dashboards-observability/public/components/explorer/visualizations/count_distribution/count_distribution.tsx @@ -16,11 +16,13 @@ export const CountDistribution = ({ countDistribution }: any) => { - if (!countDistribution || !countDistribution.data) return null; + if ( + !countDistribution || + !countDistribution.data || + !countDistribution.metadata || + !countDistribution.metadata.fields + ) return null; - const meta = countDistribution.metadata; - const xkey = meta?.xfield?.name; - const ykey = meta?.yfield?.name; const layout = { showlegend: true, margin: { @@ -32,24 +34,12 @@ export const CountDistribution = ({ }, height: 220 }; - const config = {}; - const xaxis = { - autorange: true - }; - const yaxis = { - fixedrange: true - }; - - if (!xkey || !ykey) { - return null; - } return ( ); }; \ No newline at end of file diff --git a/public/components/explorer/visualizations/count_distribution/index.ts b/dashboards-observability/public/components/explorer/visualizations/count_distribution/index.ts similarity index 100% rename from public/components/explorer/visualizations/count_distribution/index.ts rename to dashboards-observability/public/components/explorer/visualizations/count_distribution/index.ts diff --git a/public/components/explorer/visualizations/datapanel.scss b/dashboards-observability/public/components/explorer/visualizations/datapanel.scss similarity index 100% rename from public/components/explorer/visualizations/datapanel.scss rename to dashboards-observability/public/components/explorer/visualizations/datapanel.scss diff --git a/public/components/explorer/visualizations/datapanel.tsx b/dashboards-observability/public/components/explorer/visualizations/datapanel.tsx similarity index 100% rename from public/components/explorer/visualizations/datapanel.tsx rename to dashboards-observability/public/components/explorer/visualizations/datapanel.tsx diff --git a/public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap b/dashboards-observability/public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap similarity index 100% rename from public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/__snapshots__/drag_drop.test.tsx.snap diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.scss b/dashboards-observability/public/components/explorer/visualizations/drag_drop/drag_drop.scss similarity index 100% rename from public/components/explorer/visualizations/drag_drop/drag_drop.scss rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/drag_drop.scss diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx b/dashboards-observability/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx similarity index 100% rename from public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/drag_drop.test.tsx diff --git a/public/components/explorer/visualizations/drag_drop/drag_drop.tsx b/dashboards-observability/public/components/explorer/visualizations/drag_drop/drag_drop.tsx similarity index 100% rename from public/components/explorer/visualizations/drag_drop/drag_drop.tsx rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/drag_drop.tsx diff --git a/public/components/explorer/visualizations/drag_drop/index.ts b/dashboards-observability/public/components/explorer/visualizations/drag_drop/index.ts similarity index 100% rename from public/components/explorer/visualizations/drag_drop/index.ts rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/index.ts diff --git a/public/components/explorer/visualizations/drag_drop/providers.test.tsx b/dashboards-observability/public/components/explorer/visualizations/drag_drop/providers.test.tsx similarity index 100% rename from public/components/explorer/visualizations/drag_drop/providers.test.tsx rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/providers.test.tsx diff --git a/public/components/explorer/visualizations/drag_drop/providers.tsx b/dashboards-observability/public/components/explorer/visualizations/drag_drop/providers.tsx similarity index 100% rename from public/components/explorer/visualizations/drag_drop/providers.tsx rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/providers.tsx diff --git a/public/components/explorer/visualizations/drag_drop/readme.md b/dashboards-observability/public/components/explorer/visualizations/drag_drop/readme.md similarity index 100% rename from public/components/explorer/visualizations/drag_drop/readme.md rename to dashboards-observability/public/components/explorer/visualizations/drag_drop/readme.md diff --git a/public/components/explorer/visualizations/fieldList.tsx b/dashboards-observability/public/components/explorer/visualizations/fieldList.tsx similarity index 100% rename from public/components/explorer/visualizations/fieldList.tsx rename to dashboards-observability/public/components/explorer/visualizations/fieldList.tsx diff --git a/public/components/explorer/visualizations/field_item.scss b/dashboards-observability/public/components/explorer/visualizations/field_item.scss similarity index 100% rename from public/components/explorer/visualizations/field_item.scss rename to dashboards-observability/public/components/explorer/visualizations/field_item.scss diff --git a/public/components/explorer/visualizations/field_item.tsx b/dashboards-observability/public/components/explorer/visualizations/field_item.tsx similarity index 100% rename from public/components/explorer/visualizations/field_item.tsx rename to dashboards-observability/public/components/explorer/visualizations/field_item.tsx diff --git a/public/components/explorer/visualizations/field_list.scss b/dashboards-observability/public/components/explorer/visualizations/field_list.scss similarity index 100% rename from public/components/explorer/visualizations/field_list.scss rename to dashboards-observability/public/components/explorer/visualizations/field_list.scss diff --git a/public/components/explorer/visualizations/fields_accordion.tsx b/dashboards-observability/public/components/explorer/visualizations/fields_accordion.tsx similarity index 100% rename from public/components/explorer/visualizations/fields_accordion.tsx rename to dashboards-observability/public/components/explorer/visualizations/fields_accordion.tsx diff --git a/public/components/explorer/visualizations/frame_layout.scss b/dashboards-observability/public/components/explorer/visualizations/frame_layout.scss similarity index 100% rename from public/components/explorer/visualizations/frame_layout.scss rename to dashboards-observability/public/components/explorer/visualizations/frame_layout.scss diff --git a/public/components/explorer/visualizations/frame_layout.tsx b/dashboards-observability/public/components/explorer/visualizations/frame_layout.tsx similarity index 88% rename from public/components/explorer/visualizations/frame_layout.tsx rename to dashboards-observability/public/components/explorer/visualizations/frame_layout.tsx index a06e49f37..a7d5bc3c7 100644 --- a/public/components/explorer/visualizations/frame_layout.tsx +++ b/dashboards-observability/public/components/explorer/visualizations/frame_layout.tsx @@ -25,9 +25,6 @@ export function FrameLayout(props: FrameLayoutProps) { return (
- - {props.dataPanel} - {props.workspacePanel} diff --git a/public/components/explorer/visualizations/index.tsx b/dashboards-observability/public/components/explorer/visualizations/index.tsx similarity index 100% rename from public/components/explorer/visualizations/index.tsx rename to dashboards-observability/public/components/explorer/visualizations/index.tsx diff --git a/public/components/explorer/visualizations/lens_field_icon.test.tsx b/dashboards-observability/public/components/explorer/visualizations/lens_field_icon.test.tsx similarity index 100% rename from public/components/explorer/visualizations/lens_field_icon.test.tsx rename to dashboards-observability/public/components/explorer/visualizations/lens_field_icon.test.tsx diff --git a/public/components/explorer/visualizations/lens_field_icon.tsx b/dashboards-observability/public/components/explorer/visualizations/lens_field_icon.tsx similarity index 100% rename from public/components/explorer/visualizations/lens_field_icon.tsx rename to dashboards-observability/public/components/explorer/visualizations/lens_field_icon.tsx diff --git a/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx similarity index 100% rename from public/components/explorer/visualizations/shared_components/empty_placeholder.tsx rename to dashboards-observability/public/components/explorer/visualizations/shared_components/empty_placeholder.tsx diff --git a/public/components/explorer/visualizations/shared_components/index.ts b/dashboards-observability/public/components/explorer/visualizations/shared_components/index.ts similarity index 100% rename from public/components/explorer/visualizations/shared_components/index.ts rename to dashboards-observability/public/components/explorer/visualizations/shared_components/index.ts diff --git a/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx similarity index 100% rename from public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx rename to dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.test.tsx diff --git a/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx similarity index 100% rename from public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx rename to dashboards-observability/public/components/explorer/visualizations/shared_components/legend_settings_popover.tsx diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.scss b/dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_button.scss similarity index 100% rename from public/components/explorer/visualizations/shared_components/toolbar_button.scss rename to dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_button.scss diff --git a/public/components/explorer/visualizations/shared_components/toolbar_button.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_button.tsx similarity index 100% rename from public/components/explorer/visualizations/shared_components/toolbar_button.tsx rename to dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_button.tsx diff --git a/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx b/dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx similarity index 100% rename from public/components/explorer/visualizations/shared_components/toolbar_popover.tsx rename to dashboards-observability/public/components/explorer/visualizations/shared_components/toolbar_popover.tsx diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.scss b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/chart_switch.scss similarity index 100% rename from public/components/explorer/visualizations/workspace_panel/chart_switch.scss rename to dashboards-observability/public/components/explorer/visualizations/workspace_panel/chart_switch.scss diff --git a/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx similarity index 100% rename from public/components/explorer/visualizations/workspace_panel/chart_switch.tsx rename to dashboards-observability/public/components/explorer/visualizations/workspace_panel/chart_switch.tsx diff --git a/public/components/explorer/visualizations/workspace_panel/index.ts b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/index.ts similarity index 100% rename from public/components/explorer/visualizations/workspace_panel/index.ts rename to dashboards-observability/public/components/explorer/visualizations/workspace_panel/index.ts diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx similarity index 97% rename from public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx rename to dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx index dae5982c4..7e7459567 100644 --- a/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx +++ b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel.tsx @@ -143,8 +143,7 @@ export function WorkspacePanel({ setVis={ setCurVisId } vis={ getCurChart() } visualizationTypes={ memorizedVisualizationTypes } - handleSavePanelNameChange={ (name: string) => { - console.log('vis updating state name: ', name); + handleSavePanelNameChange={ (name: string) => { setSavePanelName(name) } } savePanelName={ savePanelName } diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss similarity index 100% rename from public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss rename to dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.scss diff --git a/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx b/dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx similarity index 100% rename from public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx rename to dashboards-observability/public/components/explorer/visualizations/workspace_panel/workspace_panel_wrapper.tsx diff --git a/public/components/index.tsx b/dashboards-observability/public/components/index.tsx similarity index 92% rename from public/components/index.tsx rename to dashboards-observability/public/components/index.tsx index 8aa74880a..0e93f0f40 100644 --- a/public/components/index.tsx +++ b/dashboards-observability/public/components/index.tsx @@ -21,7 +21,8 @@ export const Observability = ( AppMountParameters: AppMountParameters, pplService: any, dslService: any, - savedObjects: any + savedObjects: any, + timestampUtils: any, ) => { ReactDOM.render( , AppMountParameters.element ); diff --git a/public/components/notebooks/components/__test__/default_parser.test.tsx b/dashboards-observability/public/components/notebooks/components/__test__/default_parser.test.tsx similarity index 100% rename from public/components/notebooks/components/__test__/default_parser.test.tsx rename to dashboards-observability/public/components/notebooks/components/__test__/default_parser.test.tsx diff --git a/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts b/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts new file mode 100644 index 000000000..2097084c4 --- /dev/null +++ b/dashboards-observability/public/components/notebooks/components/__test__/legacy_route_helpers.test.ts @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { RedirectProps } from 'react-router-dom'; +import { convertLegacyNotebooksUrl } from '../helpers/legacy_route_helpers'; + +describe('Test legacy route helpers', () => { + it('converts legacy notebooks url', () => { + const locations = [ + { + pathname: '/app/notebooks-dashboards', + search: '', + hash: '#/GQ5icXwBJCegTOBKO4Um', + }, + { + pathname: '/app/notebooks-dashboards', + search: '?view=view_both', + hash: '#/clPiPXwBEM7l9gC0xTpA', + }, + { + pathname: '/testBasePath/app/notebooks-dashboards', + search: '?view=output_only&security_tenant=global', + hash: `#/GQ5icXwBJCegTOBKO4Um?_g=(time:(from:'2021-10-15T20:25:09.556Z',to:'2021-10-15T20:55:09.556Z'))`, + }, + ] as Location[]; + const expected = [ + '/app/observability#/notebooks/GQ5icXwBJCegTOBKO4Um', + '/app/observability#/notebooks/clPiPXwBEM7l9gC0xTpA?view=view_both', + `/testBasePath/app/observability#/notebooks/GQ5icXwBJCegTOBKO4Um?_g=(time:(from:'2021-10-15T20:25:09.556Z',to:'2021-10-15T20:55:09.556Z'))&view=output_only&security_tenant=global`, + ] as RedirectProps['to'][]; + expect(locations.map((location) => convertLegacyNotebooksUrl(location))).toEqual(expected); + }); +}); diff --git a/public/components/notebooks/components/__test__/sampleDefaultNotebooks.tsx b/dashboards-observability/public/components/notebooks/components/__test__/sampleDefaultNotebooks.tsx similarity index 100% rename from public/components/notebooks/components/__test__/sampleDefaultNotebooks.tsx rename to dashboards-observability/public/components/notebooks/components/__test__/sampleDefaultNotebooks.tsx diff --git a/public/components/notebooks/components/__test__/sampleZeppelinNotebooks.tsx b/dashboards-observability/public/components/notebooks/components/__test__/sampleZeppelinNotebooks.tsx similarity index 100% rename from public/components/notebooks/components/__test__/sampleZeppelinNotebooks.tsx rename to dashboards-observability/public/components/notebooks/components/__test__/sampleZeppelinNotebooks.tsx diff --git a/public/components/notebooks/components/__test__/zeppelin_parser.test.tsx b/dashboards-observability/public/components/notebooks/components/__test__/zeppelin_parser.test.tsx similarity index 100% rename from public/components/notebooks/components/__test__/zeppelin_parser.test.tsx rename to dashboards-observability/public/components/notebooks/components/__test__/zeppelin_parser.test.tsx diff --git a/public/components/notebooks/components/helpers/custom_modals/custom_input_modal.tsx b/dashboards-observability/public/components/notebooks/components/helpers/custom_modals/custom_input_modal.tsx similarity index 100% rename from public/components/notebooks/components/helpers/custom_modals/custom_input_modal.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/custom_modals/custom_input_modal.tsx diff --git a/public/components/notebooks/components/helpers/custom_modals/reporting_loading_modal.tsx b/dashboards-observability/public/components/notebooks/components/helpers/custom_modals/reporting_loading_modal.tsx similarity index 100% rename from public/components/notebooks/components/helpers/custom_modals/reporting_loading_modal.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/custom_modals/reporting_loading_modal.tsx diff --git a/public/components/notebooks/components/helpers/default_parser.tsx b/dashboards-observability/public/components/notebooks/components/helpers/default_parser.tsx similarity index 100% rename from public/components/notebooks/components/helpers/default_parser.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/default_parser.tsx diff --git a/public/components/notebooks/components/helpers/download_json.tsx b/dashboards-observability/public/components/notebooks/components/helpers/download_json.tsx similarity index 100% rename from public/components/notebooks/components/helpers/download_json.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/download_json.tsx diff --git a/dashboards-observability/public/components/notebooks/components/helpers/legacy_route_helpers.ts b/dashboards-observability/public/components/notebooks/components/helpers/legacy_route_helpers.ts new file mode 100644 index 000000000..ebb241759 --- /dev/null +++ b/dashboards-observability/public/components/notebooks/components/helpers/legacy_route_helpers.ts @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export const convertLegacyNotebooksUrl = (location: Location)=> { + const pathname = location.pathname.replace('notebooks-dashboards', 'observability'); + const hash = `#/notebooks${location.hash.replace(/^#/, '')}${ + location.hash.includes('?') ? location.search.replace(/^\?/, '&') : location.search + }`; + return pathname + hash; +}; diff --git a/public/components/notebooks/components/helpers/modal_containers.tsx b/dashboards-observability/public/components/notebooks/components/helpers/modal_containers.tsx similarity index 100% rename from public/components/notebooks/components/helpers/modal_containers.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/modal_containers.tsx diff --git a/public/components/notebooks/components/helpers/panel_wrapper.tsx b/dashboards-observability/public/components/notebooks/components/helpers/panel_wrapper.tsx similarity index 100% rename from public/components/notebooks/components/helpers/panel_wrapper.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/panel_wrapper.tsx diff --git a/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx b/dashboards-observability/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx similarity index 100% rename from public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/reporting_context_menu_helper.tsx diff --git a/public/components/notebooks/components/helpers/zeppelin_parser.tsx b/dashboards-observability/public/components/notebooks/components/helpers/zeppelin_parser.tsx similarity index 100% rename from public/components/notebooks/components/helpers/zeppelin_parser.tsx rename to dashboards-observability/public/components/notebooks/components/helpers/zeppelin_parser.tsx diff --git a/public/components/notebooks/components/main.tsx b/dashboards-observability/public/components/notebooks/components/main.tsx similarity index 100% rename from public/components/notebooks/components/main.tsx rename to dashboards-observability/public/components/notebooks/components/main.tsx diff --git a/public/components/notebooks/components/note_table.tsx b/dashboards-observability/public/components/notebooks/components/note_table.tsx similarity index 100% rename from public/components/notebooks/components/note_table.tsx rename to dashboards-observability/public/components/notebooks/components/note_table.tsx diff --git a/public/components/notebooks/components/notebook.tsx b/dashboards-observability/public/components/notebooks/components/notebook.tsx similarity index 100% rename from public/components/notebooks/components/notebook.tsx rename to dashboards-observability/public/components/notebooks/components/notebook.tsx diff --git a/public/components/notebooks/components/paragraph_components/para_input.tsx b/dashboards-observability/public/components/notebooks/components/paragraph_components/para_input.tsx similarity index 98% rename from public/components/notebooks/components/paragraph_components/para_input.tsx rename to dashboards-observability/public/components/notebooks/components/paragraph_components/para_input.tsx index 18846ef41..41c967fb4 100644 --- a/public/components/notebooks/components/paragraph_components/para_input.tsx +++ b/dashboards-observability/public/components/notebooks/components/paragraph_components/para_input.tsx @@ -47,6 +47,7 @@ import { EuiTextArea, } from '@elastic/eui'; import { Input, Prompt, Source } from '@nteract/presentational-components'; +import { uiSettingsService } from '../../../../../common/utils'; import React, { useState } from 'react'; import { ParaType } from '../../../../../common/types/notebooks'; @@ -169,7 +170,7 @@ export const ParaInput = (props: { start={props.startTime} end={props.endTime} showUpdateButton={false} - dateFormat="MM/DD/YYYY hh:mm:ss A" + dateFormat={uiSettingsService.get('dateFormat')} onTimeChange={(e) => { props.setStartTime(e.start); props.setEndTime(e.end); diff --git a/public/components/notebooks/components/paragraph_components/para_output.tsx b/dashboards-observability/public/components/notebooks/components/paragraph_components/para_output.tsx similarity index 100% rename from public/components/notebooks/components/paragraph_components/para_output.tsx rename to dashboards-observability/public/components/notebooks/components/paragraph_components/para_output.tsx diff --git a/public/components/notebooks/components/paragraph_components/para_query_grid.tsx b/dashboards-observability/public/components/notebooks/components/paragraph_components/para_query_grid.tsx similarity index 100% rename from public/components/notebooks/components/paragraph_components/para_query_grid.tsx rename to dashboards-observability/public/components/notebooks/components/paragraph_components/para_query_grid.tsx diff --git a/public/components/notebooks/components/paragraph_components/paragraphs.tsx b/dashboards-observability/public/components/notebooks/components/paragraph_components/paragraphs.tsx similarity index 100% rename from public/components/notebooks/components/paragraph_components/paragraphs.tsx rename to dashboards-observability/public/components/notebooks/components/paragraph_components/paragraphs.tsx diff --git a/public/components/notebooks/docs/dev/API_Documentation.md b/dashboards-observability/public/components/notebooks/docs/dev/API_Documentation.md similarity index 100% rename from public/components/notebooks/docs/dev/API_Documentation.md rename to dashboards-observability/public/components/notebooks/docs/dev/API_Documentation.md diff --git a/public/components/notebooks/docs/dev/Build_Documentation.md b/dashboards-observability/public/components/notebooks/docs/dev/Build_Documentation.md similarity index 100% rename from public/components/notebooks/docs/dev/Build_Documentation.md rename to dashboards-observability/public/components/notebooks/docs/dev/Build_Documentation.md diff --git a/public/components/notebooks/docs/dev/OpenSearch-Dashboards-Notebooks-Design-Proposal.md b/dashboards-observability/public/components/notebooks/docs/dev/OpenSearch-Dashboards-Notebooks-Design-Proposal.md similarity index 100% rename from public/components/notebooks/docs/dev/OpenSearch-Dashboards-Notebooks-Design-Proposal.md rename to dashboards-observability/public/components/notebooks/docs/dev/OpenSearch-Dashboards-Notebooks-Design-Proposal.md diff --git a/public/components/notebooks/docs/dev/Usage_Documentation.md b/dashboards-observability/public/components/notebooks/docs/dev/Usage_Documentation.md similarity index 100% rename from public/components/notebooks/docs/dev/Usage_Documentation.md rename to dashboards-observability/public/components/notebooks/docs/dev/Usage_Documentation.md diff --git a/public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md b/dashboards-observability/public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md similarity index 100% rename from public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md rename to dashboards-observability/public/components/notebooks/docs/dev/Zeppelin_backend_adaptor.md diff --git a/public/components/notebooks/docs/dev/images/Default_Notebooks_Schemav2.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Default_Notebooks_Schemav2.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Default_Notebooks_Schemav2.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Default_Notebooks_Schemav2.png diff --git a/public/components/notebooks/docs/dev/images/Embeddable_API.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Embeddable_API.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Embeddable_API.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Embeddable_API.png diff --git a/public/components/notebooks/docs/dev/images/Markdown_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Markdown_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Markdown_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Markdown_ss.png diff --git a/public/components/notebooks/docs/dev/images/Multi-timeline_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Multi-timeline_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Multi-timeline_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Multi-timeline_ss.png diff --git a/public/components/notebooks/docs/dev/images/Notebooks_v1.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Notebooks_v1.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Notebooks_v1.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Notebooks_v1.png diff --git a/public/components/notebooks/docs/dev/images/Notebooks_v2.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Notebooks_v2.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Notebooks_v2.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Notebooks_v2.png diff --git a/public/components/notebooks/docs/dev/images/Notebooks_v3.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Notebooks_v3.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Notebooks_v3.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Notebooks_v3.png diff --git a/public/components/notebooks/docs/dev/images/UI.png b/dashboards-observability/public/components/notebooks/docs/dev/images/UI.png similarity index 100% rename from public/components/notebooks/docs/dev/images/UI.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/UI.png diff --git a/public/components/notebooks/docs/dev/images/Zeppelin_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/Zeppelin_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/Zeppelin_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/Zeppelin_ss.png diff --git a/public/components/notebooks/docs/dev/images/dashboards-notebooks.gif b/dashboards-observability/public/components/notebooks/docs/dev/images/dashboards-notebooks.gif similarity index 100% rename from public/components/notebooks/docs/dev/images/dashboards-notebooks.gif rename to dashboards-observability/public/components/notebooks/docs/dev/images/dashboards-notebooks.gif diff --git a/public/components/notebooks/docs/dev/images/dashboards_notebooks_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/dashboards_notebooks_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/dashboards_notebooks_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/dashboards_notebooks_ss.png diff --git a/public/components/notebooks/docs/dev/images/default_backend_model.png b/dashboards-observability/public/components/notebooks/docs/dev/images/default_backend_model.png similarity index 100% rename from public/components/notebooks/docs/dev/images/default_backend_model.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/default_backend_model.png diff --git a/public/components/notebooks/docs/dev/images/default_operation_notebook.png b/dashboards-observability/public/components/notebooks/docs/dev/images/default_operation_notebook.png similarity index 100% rename from public/components/notebooks/docs/dev/images/default_operation_notebook.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/default_operation_notebook.png diff --git a/public/components/notebooks/docs/dev/images/default_view_notebook.png b/dashboards-observability/public/components/notebooks/docs/dev/images/default_view_notebook.png similarity index 100% rename from public/components/notebooks/docs/dev/images/default_view_notebook.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/default_view_notebook.png diff --git a/public/components/notebooks/docs/dev/images/matplot_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/matplot_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/matplot_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/matplot_ss.png diff --git a/public/components/notebooks/docs/dev/images/notebook-buttons.png b/dashboards-observability/public/components/notebooks/docs/dev/images/notebook-buttons.png similarity index 100% rename from public/components/notebooks/docs/dev/images/notebook-buttons.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/notebook-buttons.png diff --git a/public/components/notebooks/docs/dev/images/opensearch-zeppelin-settings.png b/dashboards-observability/public/components/notebooks/docs/dev/images/opensearch-zeppelin-settings.png similarity index 100% rename from public/components/notebooks/docs/dev/images/opensearch-zeppelin-settings.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/opensearch-zeppelin-settings.png diff --git a/public/components/notebooks/docs/dev/images/opensearch-zeppelin.png b/dashboards-observability/public/components/notebooks/docs/dev/images/opensearch-zeppelin.png similarity index 100% rename from public/components/notebooks/docs/dev/images/opensearch-zeppelin.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/opensearch-zeppelin.png diff --git a/public/components/notebooks/docs/dev/images/opensearch_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/opensearch_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/opensearch_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/opensearch_ss.png diff --git a/public/components/notebooks/docs/dev/images/opensearch_ss_zepcontext.png b/dashboards-observability/public/components/notebooks/docs/dev/images/opensearch_ss_zepcontext.png similarity index 100% rename from public/components/notebooks/docs/dev/images/opensearch_ss_zepcontext.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/opensearch_ss_zepcontext.png diff --git a/public/components/notebooks/docs/dev/images/paragraph-buttons.png b/dashboards-observability/public/components/notebooks/docs/dev/images/paragraph-buttons.png similarity index 100% rename from public/components/notebooks/docs/dev/images/paragraph-buttons.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/paragraph-buttons.png diff --git a/public/components/notebooks/docs/dev/images/python_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/python_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/python_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/python_ss.png diff --git a/public/components/notebooks/docs/dev/images/vizembed_ss.png b/dashboards-observability/public/components/notebooks/docs/dev/images/vizembed_ss.png similarity index 100% rename from public/components/notebooks/docs/dev/images/vizembed_ss.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/vizembed_ss.png diff --git a/public/components/notebooks/docs/dev/images/zeppelin_architecture.png b/dashboards-observability/public/components/notebooks/docs/dev/images/zeppelin_architecture.png similarity index 100% rename from public/components/notebooks/docs/dev/images/zeppelin_architecture.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/zeppelin_architecture.png diff --git a/public/components/notebooks/docs/dev/images/zeppelin_notebooks_sequence.png b/dashboards-observability/public/components/notebooks/docs/dev/images/zeppelin_notebooks_sequence.png similarity index 100% rename from public/components/notebooks/docs/dev/images/zeppelin_notebooks_sequence.png rename to dashboards-observability/public/components/notebooks/docs/dev/images/zeppelin_notebooks_sequence.png diff --git a/public/components/notebooks/docs/example_notebooks/default/Introduction Notebook.json b/dashboards-observability/public/components/notebooks/docs/example_notebooks/default/Introduction Notebook.json similarity index 100% rename from public/components/notebooks/docs/example_notebooks/default/Introduction Notebook.json rename to dashboards-observability/public/components/notebooks/docs/example_notebooks/default/Introduction Notebook.json diff --git a/public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json b/dashboards-observability/public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json similarity index 100% rename from public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json rename to dashboards-observability/public/components/notebooks/docs/example_notebooks/zeppelin/Introduction Notebook-Zeppelin.json diff --git a/public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json b/dashboards-observability/public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json similarity index 100% rename from public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json rename to dashboards-observability/public/components/notebooks/docs/example_notebooks/zeppelin/Log Analysis-Zeppelin.json diff --git a/public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md b/dashboards-observability/public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md similarity index 100% rename from public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md rename to dashboards-observability/public/components/notebooks/docs/poc/OpenSearch_Dashboards_Embeddable_Documentation.md diff --git a/public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md b/dashboards-observability/public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md similarity index 100% rename from public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md rename to dashboards-observability/public/components/notebooks/docs/poc/Zeppelin_OpenSearch_Storage.md diff --git a/public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md b/dashboards-observability/public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md similarity index 100% rename from public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md rename to dashboards-observability/public/components/notebooks/docs/poc/docs/Zeppelin_OpenSearch_Storage.md diff --git a/public/components/notebooks/docs/poc/zeppelin-patch b/dashboards-observability/public/components/notebooks/docs/poc/zeppelin-patch similarity index 100% rename from public/components/notebooks/docs/poc/zeppelin-patch rename to dashboards-observability/public/components/notebooks/docs/poc/zeppelin-patch diff --git a/public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/pom.xml b/dashboards-observability/public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/pom.xml similarity index 100% rename from public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/pom.xml rename to dashboards-observability/public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/pom.xml diff --git a/public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/src/main/java/org/apache/zeppelin/notebook/repo/OpenSearchNotebookRepo.java b/dashboards-observability/public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/src/main/java/org/apache/zeppelin/notebook/repo/OpenSearchNotebookRepo.java similarity index 100% rename from public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/src/main/java/org/apache/zeppelin/notebook/repo/OpenSearchNotebookRepo.java rename to dashboards-observability/public/components/notebooks/docs/poc/zeppelin/zeppelin-plugins/notebookrepo/opensearch/src/main/java/org/apache/zeppelin/notebook/repo/OpenSearchNotebookRepo.java diff --git a/public/components/notebooks/index.scss b/dashboards-observability/public/components/notebooks/index.scss similarity index 100% rename from public/components/notebooks/index.scss rename to dashboards-observability/public/components/notebooks/index.scss diff --git a/public/components/trace_analytics/components/common/__tests__/__snapshots__/helper_functions.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/__tests__/__snapshots__/helper_functions.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/__tests__/__snapshots__/helper_functions.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/__tests__/__snapshots__/helper_functions.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap similarity index 99% rename from public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap index d8be845df..011b8cad0 100644 --- a/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap +++ b/dashboards-observability/public/components/trace_analytics/components/common/__tests__/__snapshots__/search_bar.test.tsx.snap @@ -46,7 +46,7 @@ exports[`Search bar components renders date picker 1`] = ` }, ] } - dateFormat="MMM D, YYYY @ HH:mm:ss.SSS" + dateFormat="" end="now" isAutoRefreshOnly={false} isDisabled={false} @@ -120,7 +120,7 @@ exports[`Search bar components renders date picker 1`] = ` }, ] } - dateFormat="MMM D, YYYY @ HH:mm:ss.SSS" + dateFormat="" end="now" isAutoRefreshOnly={false} isDisabled={false} @@ -181,7 +181,7 @@ exports[`Search bar components renders date picker 1`] = ` }, ] } - dateFormat="MMM D, YYYY @ HH:mm:ss.SSS" + dateFormat="" end="now" isAutoRefreshOnly={false} isDisabled={false} @@ -537,7 +537,7 @@ exports[`Search bar components renders search bar 1`] = ` }, ] } - dateFormat="MMM D, YYYY @ HH:mm:ss.SSS" + dateFormat="" end="now" isAutoRefreshOnly={false} isDisabled={false} @@ -611,7 +611,7 @@ exports[`Search bar components renders search bar 1`] = ` }, ] } - dateFormat="MMM D, YYYY @ HH:mm:ss.SSS" + dateFormat="" end="now" isAutoRefreshOnly={false} isDisabled={false} @@ -672,7 +672,7 @@ exports[`Search bar components renders search bar 1`] = ` }, ] } - dateFormat="MMM D, YYYY @ HH:mm:ss.SSS" + dateFormat="" end="now" isAutoRefreshOnly={false} isDisabled={false} diff --git a/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/__tests__/helper_functions.test.tsx diff --git a/public/components/trace_analytics/components/common/__tests__/search_bar.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/__tests__/search_bar.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/__tests__/search_bar.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/__tests__/search_bar.test.tsx diff --git a/public/components/trace_analytics/components/common/color_palette.ts b/dashboards-observability/public/components/trace_analytics/components/common/color_palette.ts similarity index 100% rename from public/components/trace_analytics/components/common/color_palette.ts rename to dashboards-observability/public/components/trace_analytics/components/common/color_palette.ts diff --git a/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_edit_popover.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_edit_popover.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_edit_popover.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_edit_popover.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_helpers.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_helpers.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_helpers.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filter_helpers.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filters.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filters.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filters.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/__snapshots__/filters.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/filters/__tests__/filter_edit_popover.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/filter_edit_popover.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/filters/__tests__/filter_edit_popover.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/filter_edit_popover.test.tsx diff --git a/public/components/trace_analytics/components/common/filters/__tests__/filter_helpers.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/filter_helpers.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/filters/__tests__/filter_helpers.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/filter_helpers.test.tsx diff --git a/public/components/trace_analytics/components/common/filters/__tests__/filters.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/filters.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/filters/__tests__/filters.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/filters/__tests__/filters.test.tsx diff --git a/public/components/trace_analytics/components/common/filters/filter_edit_popover.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_edit_popover.tsx similarity index 100% rename from public/components/trace_analytics/components/common/filters/filter_edit_popover.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/filters/filter_edit_popover.tsx diff --git a/public/components/trace_analytics/components/common/filters/filter_helpers.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx similarity index 100% rename from public/components/trace_analytics/components/common/filters/filter_helpers.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/filters/filter_helpers.tsx diff --git a/public/components/trace_analytics/components/common/filters/filters.tsx b/dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx similarity index 100% rename from public/components/trace_analytics/components/common/filters/filters.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/filters/filters.tsx diff --git a/public/components/trace_analytics/components/common/helper_functions.tsx b/dashboards-observability/public/components/trace_analytics/components/common/helper_functions.tsx similarity index 100% rename from public/components/trace_analytics/components/common/helper_functions.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/helper_functions.tsx diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/box_plt.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/box_plt.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/box_plt.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/box_plt.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/error_rate_plt_.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/error_rate_plt_.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/error_rate_plt_.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/error_rate_plt_.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/latency_trend_plt.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/latency_trend_plt.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/latency_trend_plt.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/latency_trend_plt.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map_scale.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map_scale.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map_scale.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/service_map_scale.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/throughput_plt.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/throughput_plt.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/throughput_plt.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/__snapshots__/throughput_plt.test.tsx.snap diff --git a/public/components/trace_analytics/components/common/plots/__tests__/box_plt.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/box_plt.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/box_plt.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/box_plt.test.tsx diff --git a/public/components/trace_analytics/components/common/plots/__tests__/error_rate_plt_.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/error_rate_plt_.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/error_rate_plt_.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/error_rate_plt_.test.tsx diff --git a/public/components/trace_analytics/components/common/plots/__tests__/latency_trend_plt.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/latency_trend_plt.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/latency_trend_plt.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/latency_trend_plt.test.tsx diff --git a/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/service_map.test.tsx diff --git a/public/components/trace_analytics/components/common/plots/__tests__/service_map_scale.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/service_map_scale.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/service_map_scale.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/service_map_scale.test.tsx diff --git a/public/components/trace_analytics/components/common/plots/__tests__/throughput_plt.test.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/throughput_plt.test.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/__tests__/throughput_plt.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/__tests__/throughput_plt.test.tsx diff --git a/public/components/trace_analytics/components/common/plots/box_plt.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/box_plt.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/box_plt.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/box_plt.tsx diff --git a/public/components/trace_analytics/components/common/plots/error_rate_plt.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/error_rate_plt.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/error_rate_plt.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/error_rate_plt.tsx diff --git a/public/components/trace_analytics/components/common/plots/latency_trend_plt.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/latency_trend_plt.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/latency_trend_plt.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/latency_trend_plt.tsx diff --git a/public/components/trace_analytics/components/common/plots/service_map.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/service_map.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/service_map.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/service_map.tsx diff --git a/public/components/trace_analytics/components/common/plots/service_map_scale.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/service_map_scale.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/service_map_scale.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/service_map_scale.tsx diff --git a/public/components/trace_analytics/components/common/plots/throughput_plt.tsx b/dashboards-observability/public/components/trace_analytics/components/common/plots/throughput_plt.tsx similarity index 100% rename from public/components/trace_analytics/components/common/plots/throughput_plt.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/plots/throughput_plt.tsx diff --git a/public/components/trace_analytics/components/common/search_bar.tsx b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx similarity index 96% rename from public/components/trace_analytics/components/common/search_bar.tsx rename to dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx index 9969119ca..3fe206047 100644 --- a/public/components/trace_analytics/components/common/search_bar.tsx +++ b/dashboards-observability/public/components/trace_analytics/components/common/search_bar.tsx @@ -32,6 +32,7 @@ import { EuiSpacer, EuiSuperDatePicker, } from '@elastic/eui'; +import { uiSettingsService } from '../../../../../common/utils'; import _ from 'lodash'; import React, { useState } from 'react'; import { Filters, FiltersProps } from './filters/filters'; @@ -46,6 +47,7 @@ export const renderDatePicker = ( { setStartTime(e.start); diff --git a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard.test.tsx.snap diff --git a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard_table.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard_table.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard_table.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/dashboard_table.test.tsx.snap diff --git a/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/latency_trend_cell.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/latency_trend_cell.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/latency_trend_cell.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/__snapshots__/latency_trend_cell.test.tsx.snap diff --git a/public/components/trace_analytics/components/dashboard/__tests__/dashboard.test.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard.test.tsx similarity index 100% rename from public/components/trace_analytics/components/dashboard/__tests__/dashboard.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard.test.tsx diff --git a/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 similarity index 100% rename from public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/dashboard_table.test.tsx diff --git a/public/components/trace_analytics/components/dashboard/__tests__/latency_trend_cell.test.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/latency_trend_cell.test.tsx similarity index 100% rename from public/components/trace_analytics/components/dashboard/__tests__/latency_trend_cell.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/dashboard/__tests__/latency_trend_cell.test.tsx diff --git a/public/components/trace_analytics/components/dashboard/dashboard.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx similarity index 100% rename from public/components/trace_analytics/components/dashboard/dashboard.tsx rename to dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard.tsx diff --git a/public/components/trace_analytics/components/dashboard/dashboard_table.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx similarity index 100% rename from public/components/trace_analytics/components/dashboard/dashboard_table.tsx rename to dashboards-observability/public/components/trace_analytics/components/dashboard/dashboard_table.tsx diff --git a/public/components/trace_analytics/components/dashboard/index.ts b/dashboards-observability/public/components/trace_analytics/components/dashboard/index.ts similarity index 100% rename from public/components/trace_analytics/components/dashboard/index.ts rename to dashboards-observability/public/components/trace_analytics/components/dashboard/index.ts diff --git a/public/components/trace_analytics/components/dashboard/latency_trend_cell.tsx b/dashboards-observability/public/components/trace_analytics/components/dashboard/latency_trend_cell.tsx similarity index 100% rename from public/components/trace_analytics/components/dashboard/latency_trend_cell.tsx rename to dashboards-observability/public/components/trace_analytics/components/dashboard/latency_trend_cell.tsx diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/services/__tests__/__snapshots__/service_view.test.tsx.snap diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/services/__tests__/__snapshots__/services.test.tsx.snap diff --git a/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/services/__tests__/__snapshots__/services_table.test.tsx.snap diff --git a/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx b/dashboards-observability/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx similarity index 100% rename from public/components/trace_analytics/components/services/__tests__/service_view.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/services/__tests__/service_view.test.tsx diff --git a/public/components/trace_analytics/components/services/__tests__/services.test.tsx b/dashboards-observability/public/components/trace_analytics/components/services/__tests__/services.test.tsx similarity index 100% rename from public/components/trace_analytics/components/services/__tests__/services.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/services/__tests__/services.test.tsx diff --git a/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx b/dashboards-observability/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx similarity index 100% rename from public/components/trace_analytics/components/services/__tests__/services_table.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/services/__tests__/services_table.test.tsx diff --git a/public/components/trace_analytics/components/services/index.ts b/dashboards-observability/public/components/trace_analytics/components/services/index.ts similarity index 100% rename from public/components/trace_analytics/components/services/index.ts rename to dashboards-observability/public/components/trace_analytics/components/services/index.ts diff --git a/public/components/trace_analytics/components/services/service_view.tsx b/dashboards-observability/public/components/trace_analytics/components/services/service_view.tsx similarity index 100% rename from public/components/trace_analytics/components/services/service_view.tsx rename to dashboards-observability/public/components/trace_analytics/components/services/service_view.tsx diff --git a/public/components/trace_analytics/components/services/services.tsx b/dashboards-observability/public/components/trace_analytics/components/services/services.tsx similarity index 100% rename from public/components/trace_analytics/components/services/services.tsx rename to dashboards-observability/public/components/trace_analytics/components/services/services.tsx diff --git a/public/components/trace_analytics/components/services/services_table.tsx b/dashboards-observability/public/components/trace_analytics/components/services/services_table.tsx similarity index 100% rename from public/components/trace_analytics/components/services/services_table.tsx rename to dashboards-observability/public/components/trace_analytics/components/services/services_table.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/flyout_list_item.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/flyout_list_item.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/flyout_list_item.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/flyout_list_item.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_flyout.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_flyout.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_flyout.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_flyout.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_table.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_table.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_table.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_table.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/__snapshots__/traces_table.test.tsx.snap diff --git a/public/components/trace_analytics/components/traces/__tests__/flyout_list_item.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/flyout_list_item.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/flyout_list_item.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/flyout_list_item.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/service_breakdown_panel.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/service_breakdown_panel.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/service_breakdown_panel.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/service_breakdown_panel.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/span_detail_flyout.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/span_detail_flyout.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/span_detail_flyout.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/span_detail_flyout.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/trace_view.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/trace_view.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/trace_view.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/trace_view.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/traces.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/traces.test.tsx diff --git a/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/__tests__/traces_table.test.tsx diff --git a/public/components/trace_analytics/components/traces/flyout_list_item.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/flyout_list_item.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/flyout_list_item.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/flyout_list_item.tsx diff --git a/public/components/trace_analytics/components/traces/index.ts b/dashboards-observability/public/components/trace_analytics/components/traces/index.ts similarity index 100% rename from public/components/trace_analytics/components/traces/index.ts rename to dashboards-observability/public/components/trace_analytics/components/traces/index.ts diff --git a/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/service_breakdown_panel.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx diff --git a/public/components/trace_analytics/components/traces/span_detail_flyout.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/span_detail_flyout.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/span_detail_flyout.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/span_detail_flyout.tsx diff --git a/public/components/trace_analytics/components/traces/span_detail_panel.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/span_detail_panel.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/span_detail_panel.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/span_detail_panel.tsx diff --git a/public/components/trace_analytics/components/traces/span_detail_table.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/span_detail_table.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/span_detail_table.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/span_detail_table.tsx diff --git a/public/components/trace_analytics/components/traces/trace_view.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/trace_view.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/trace_view.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/trace_view.tsx diff --git a/public/components/trace_analytics/components/traces/traces.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/traces.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/traces.tsx diff --git a/public/components/trace_analytics/components/traces/traces_table.tsx b/dashboards-observability/public/components/trace_analytics/components/traces/traces_table.tsx similarity index 100% rename from public/components/trace_analytics/components/traces/traces_table.tsx rename to dashboards-observability/public/components/trace_analytics/components/traces/traces_table.tsx diff --git a/public/components/trace_analytics/home.tsx b/dashboards-observability/public/components/trace_analytics/home.tsx similarity index 98% rename from public/components/trace_analytics/home.tsx rename to dashboards-observability/public/components/trace_analytics/home.tsx index 17d7fb807..57adea4cf 100644 --- a/public/components/trace_analytics/home.tsx +++ b/dashboards-observability/public/components/trace_analytics/home.tsx @@ -86,7 +86,7 @@ export const Home = (props: HomeProps) => { <> renderPageWithSidebar()} /> { + public setup(core: CoreSetup): ObservabilitySetup { + uiSettingsService.init(core.uiSettings); + + // redirect legacy notebooks URL to current URL under observability + if (window.location.pathname.includes('notebooks-dashboards')) { + window.location.assign(convertLegacyNotebooksUrl(window.location)); + } + + core.application.register({ + id: observabilityID, + title: observabilityTitle, + category: { + id: 'opensearch', + label: 'OpenSearch Plugins', + order: 2000, + }, + order: observabilityPluginOrder, + async mount(params: AppMountParameters) { + const { Observability } = await import('./components/index'); + const [coreStart, depsStart] = await core.getStartServices(); + const pplService = new PPLService(coreStart.http); + const dslService = new DSLService(coreStart.http); + const savedObjects = new SavedObjects(coreStart.http); + const timestampUtils = new TimestampUtils(dslService); + return Observability( + coreStart, + depsStart as AppPluginStartDependencies, + params, + pplService, + dslService, + savedObjects, + timestampUtils + ); + }, + }); + + // Return methods that should be available to other plugins + return {}; + } + public start(core: CoreStart): ObservabilityStart { + return {}; + } + public stop() {} +} diff --git a/public/services/requests/dsl.ts b/dashboards-observability/public/services/requests/dsl.ts similarity index 92% rename from public/services/requests/dsl.ts rename to dashboards-observability/public/services/requests/dsl.ts index 07bc43a2e..9cc300321 100644 --- a/public/services/requests/dsl.ts +++ b/dashboards-observability/public/services/requests/dsl.ts @@ -32,7 +32,7 @@ export default class DSLService { body: JSON.stringify(request) } ) - .catch(error => console.log(error)); + .catch(error => console.error(error)); } fetchIndices = async () => { @@ -48,14 +48,14 @@ export default class DSLService { } fetchFields = async( - request: any + index: string ) => { return this.http .get( `${DSL_BASE}${DSL_MAPPING}`, { query: { - index: request + index: index } } ) diff --git a/public/services/requests/ppl.ts b/dashboards-observability/public/services/requests/ppl.ts similarity index 100% rename from public/services/requests/ppl.ts rename to dashboards-observability/public/services/requests/ppl.ts diff --git a/public/services/saved_objects/event_analytics/saved_objects.ts b/dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts similarity index 51% rename from public/services/saved_objects/event_analytics/saved_objects.ts rename to dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts index a23a380c1..ed00d1e17 100644 --- a/public/services/saved_objects/event_analytics/saved_objects.ts +++ b/dashboards-observability/public/services/saved_objects/event_analytics/saved_objects.ts @@ -14,7 +14,6 @@ import { isEmpty, isArray } from 'lodash'; -import { htmlIdGenerator } from '@elastic/eui'; import { OBSERVABILITY_BASE, EVENT_ANALYTICS, @@ -42,10 +41,7 @@ interface ISavedObjectRequestParams { interface ISelectedPanelsParams { selectedCustomPanels: Array - name: string; - query: string; - type: string; - timeField: string; + savedVisualizationId: string } interface IBulkUpdateSavedVisualizationRquest { @@ -65,6 +61,7 @@ export default class SavedObjects { query, fields, dateRange, + timestamp, name = '', chartType = '', description = '' @@ -78,6 +75,10 @@ export default class SavedObjects { end: dateRange[1] || 'now', text: '' }, + selected_timestamp: { + 'name': timestamp || '', + 'type': 'timestamp' + }, selected_fields: { tokens: fields, text: '' @@ -105,119 +106,157 @@ export default class SavedObjects { return targetObj; } - async fetchSavedObjects(savedObjectRequestParams: ISavedObjectRequestParams) { + async fetchSavedObjects(params: ISavedObjectRequestParams) { // turn array into string. exmaple objectType ['savedQuery', 'savedVisualization'] => // 'savedQuery,savedVisualization' CONCAT_FIELDS.map((arrayField) => { this.stringifyList( - savedObjectRequestParams, + params, arrayField, ',' ); }); - const res = await this.http.get( + return await this.http.get( `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}`, { query: { - ...savedObjectRequestParams + ...params }, } - ).catch((error: any) => console.log(error)); - - return res; + ); } async fetchCustomPanels() { - return await this.http.get(`${CUSTOM_PANELS_API_PREFIX}/panels`).catch((error: any) => console.log(error)); + return await this.http.get(`${CUSTOM_PANELS_API_PREFIX}/panels`); } - async bulkUpdateCustomPanel (selectedPanelsParams: ISelectedPanelsParams) { + async bulkUpdateCustomPanel (params: ISelectedPanelsParams) { const finalParams = { panelId: '', - newVisualization: { - id: `panelViz_'${htmlIdGenerator()()}`, - title: selectedPanelsParams['name'], - query: selectedPanelsParams['query'], - type: selectedPanelsParams['type'], - timeField: selectedPanelsParams['timeField'] - } + savedVisualizationId: params.savedVisualizationId, }; - const responses = await Promise.all( - selectedPanelsParams['selectedCustomPanels'].map((panel) => { + return await Promise.all( + params['selectedCustomPanels'].map((panel) => { finalParams['panelId'] = panel['panel']['id']; - return this.http.post(`${CUSTOM_PANELS_API_PREFIX}/visualizations/event_explorer`, { + return this.http.post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { body: JSON.stringify(finalParams) }); }) - ).catch((error) => {}); - + ); }; - async bulkUpdateSavedVisualization(bulkUpdateSavedVisualizationRquest: IBulkUpdateSavedVisualizationRquest) { + async bulkUpdateSavedVisualization(params: IBulkUpdateSavedVisualizationRquest) { const finalParams = this.buildRequestBody({ - query: bulkUpdateSavedVisualizationRquest['query'], - fields: bulkUpdateSavedVisualizationRquest['fields'], - dateRange: bulkUpdateSavedVisualizationRquest['dateRange'], - chartType: bulkUpdateSavedVisualizationRquest['type'], - name: bulkUpdateSavedVisualizationRquest['name'] + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name }); - const responses = await Promise.all( - bulkUpdateSavedVisualizationRquest.savedObjectList.map((objectToUpdate) => { + return await Promise.all( + params.savedObjectList.map((objectToUpdate) => { finalParams['object_id'] = objectToUpdate['saved_object']['objectId']; - return this.http.put(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, { - body: JSON.stringify(finalParams) - }); + return this.http.put( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, + { + body: JSON.stringify(finalParams) + } + ); }) - ).catch((error) => {}); + ); } - async updateSavedVisualizationById(updateVisualizationRequest: any) { + async updateSavedVisualizationById(params: any) { const finalParams = this.buildRequestBody({ - query: updateVisualizationRequest['query'], - fields: updateVisualizationRequest['fields'], - dateRange: updateVisualizationRequest['dateRange'], + query: params.query, + fields: params.fields, + dateRange: params.dateRange, }); - finalParams['object_id'] = updateVisualizationRequest['objectId']; - - return await this.http.post(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, { - body: JSON.stringify(finalParams) - }).catch((error: any) => console.log(error)); + finalParams['object_id'] = params.objectId; + return await this.http.post( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, + { + body: JSON.stringify(finalParams) + } + ); } - async createSavedQuery(createQueryRequest: any) { + async createSavedQuery(params: any) { const finalParams = this.buildRequestBody({ - query: createQueryRequest['query'], - fields: createQueryRequest['fields'], - dateRange: createQueryRequest['dateRange'], - name: createQueryRequest['name'] + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + name: params.name, + timestamp: params.timestamp, }); - return await this.http.post(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, { - body: JSON.stringify(finalParams) - }).catch((error: any) => console.log(error)); + return await this.http.post( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_QUERY}`, + { + body: JSON.stringify(finalParams) + } + ); } - async createSavedVisualization(createVisualizationRequest: any) { + async createSavedVisualization(params: any) { const finalParams = this.buildRequestBody({ - query: createVisualizationRequest['query'], - fields: createVisualizationRequest['fields'], - dateRange: createVisualizationRequest['dateRange'], - chartType: createVisualizationRequest['type'], - name: createVisualizationRequest['name'] + query: params.query, + fields: params.fields, + dateRange: params.dateRange, + chartType: params.type, + name: params.name, + timestamp: params.timestamp }); - return await this.http.post(`${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, { - body: JSON.stringify(finalParams) - }).catch((error: any) => console.log(error)); + return await this.http.post( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}${SAVED_VISUALIZATION}`, + { + body: JSON.stringify(finalParams) + } + ); + } + + async createSavedTimestamp(params: any) { + const finalParams = { + index: params.index, + name: params.name, + type: params.type, + dsl_type: params.dsl_type, + }; + + return await this.http.post( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/timestamp`, + { + body: JSON.stringify(finalParams) + } + ); + } + + async updateTimestamp(params: any) { + const finalParams = { + objectId: params.index, + timestamp: { + name: params.name, + index: params.index, + type: params.type, + dsl_type: params.dsl_type + } + } + return await this.http.put( + `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/timestamp`, + { + body: JSON.stringify(finalParams) + } + ); } deleteSavedObjectsById(deleteObjectRequest: any) {} diff --git a/dashboards-observability/public/services/timestamp/timestamp.ts b/dashboards-observability/public/services/timestamp/timestamp.ts new file mode 100644 index 000000000..287485131 --- /dev/null +++ b/dashboards-observability/public/services/timestamp/timestamp.ts @@ -0,0 +1,54 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import DSLService from "../requests/dsl"; +import { + isEmpty, + isEqual, + map +} from 'lodash'; + +export default class TimestampUtils { + constructor(private dslService: DSLService) {} + + async getTimestamp(index: string) { + + const indexMappings = await this.getIndexMappings(index); + + if (indexMappings?.[index]?.mappings?.properties) { + const fieldMappings = indexMappings[index].mappings.properties; + const timestamps = { + default_timestamp: '', + available_timestamps: [] + }; + map(fieldMappings, (mapping, field) => { + if ( + mapping.type && + isEqual(mapping.type, 'date') && + isEmpty(timestamps.default_timestamp) + ) { + timestamps.default_timestamp = field; + } else if ( + mapping.type && + isEqual(mapping.type, 'date') + ) { + timestamps.available_timestamps.push(field); + } + }) + + return timestamps; + } + } + + async getIndexMappings(index: string) { + return await this.dslService.fetchFields(index); + } +} \ No newline at end of file diff --git a/public/types.ts b/dashboards-observability/public/types.ts similarity index 100% rename from public/types.ts rename to dashboards-observability/public/types.ts diff --git a/server/adaptors/custom_panels/custom_panel_adaptor.ts b/dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts similarity index 84% rename from server/adaptors/custom_panels/custom_panel_adaptor.ts rename to dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts index f5992c712..13052e93c 100644 --- a/server/adaptors/custom_panels/custom_panel_adaptor.ts +++ b/dashboards-observability/server/adaptors/custom_panels/custom_panel_adaptor.ts @@ -9,6 +9,7 @@ * GitHub history for details. */ +import { v4 as uuidv4 } from 'uuid'; import { PanelType, VisualizationType } from '../../../common/types/custom_panels'; import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; @@ -190,8 +191,8 @@ export class CustomPanelsAdaptor { } }; - // gets list of panels stored in index - viewSavedVisualiationList = async (client: ILegacyScopedClusterClient) => { + // gets all saved visualizations + getAllSavedVisualizations = async (client: ILegacyScopedClusterClient) => { try { const response = await client.callAsCurrentUser('observability.getObject', { objectType: 'savedVisualization', @@ -208,6 +209,28 @@ export class CustomPanelsAdaptor { } }; + // gets list of savedVisualizations by Id + getSavedVisualizationById = async ( + client: ILegacyScopedClusterClient, + savedVisualizationId: string + ) => { + try { + const response = await client.callAsCurrentUser('observability.getObjectById', { + objectId: savedVisualizationId, + }); + const visualization = response.observabilityObjectList[0]; + return { + id: visualization.objectId, + name: visualization.savedVisualization.name, + query: visualization.savedVisualization.query, + type: visualization.savedVisualization.type, + timeField: visualization.savedVisualization.selected_timestamp.name, + }; + } catch (error) { + throw new Error('Fetch Saved Visualizations By Id Error:' + error); + } + }; + //Get All Visualizations from a Panel //Add Visualization getVisualizations = async (client: ILegacyScopedClusterClient, panelId: string) => { @@ -241,13 +264,7 @@ export class CustomPanelsAdaptor { addVisualization = async ( client: ILegacyScopedClusterClient, panelId: string, - newVisualization: { - id: string; - title: string; - query: string; - type: string; - timeField: string; - }, + savedVisualizationId: string, oldVisualizationId?: string ) => { try { @@ -274,35 +291,11 @@ export class CustomPanelsAdaptor { } const newPanelVisualizations = [ ...visualizationsList, - { ...newVisualization, ...newDimensions }, - ]; - const updatePanelResponse = await this.updatePanel(client, panelId, { - visualizations: newPanelVisualizations, - }); - return newPanelVisualizations; - } catch (error) { - throw new Error('Add/Replace Visualization Error:' + error); - } - }; - - //Add Visualization in the Panel from Event Explorer - addVisualizationFromEvents = async ( - client: ILegacyScopedClusterClient, - panelId: string, - paramVisualization: { - id: string; - title: string; - query: string; - type: string; - timeField: string; - } - ) => { - try { - const allPanelVisualizations = await this.getVisualizations(client, panelId); - const newDimensions = this.getNewVizDimensions(allPanelVisualizations); - const newPanelVisualizations = [ - ...allPanelVisualizations, - { ...paramVisualization, ...newDimensions }, + { + id: 'panel_viz_' + uuidv4(), + savedVisualizationId: savedVisualizationId, + ...newDimensions, + }, ]; const updatePanelResponse = await this.updatePanel(client, panelId, { visualizations: newPanelVisualizations, @@ -313,26 +306,6 @@ export class CustomPanelsAdaptor { } }; - //Delete a Visualization in the Panel - deleteVisualization = async ( - client: ILegacyScopedClusterClient, - panelId: string, - visualizationId: string - ) => { - try { - const allPanelVisualizations = await this.getVisualizations(client, panelId); - const filteredPanelVisualizations = allPanelVisualizations.filter( - (panelVisualization: VisualizationType) => panelVisualization.id != visualizationId - ); - const updatePanelResponse = await this.updatePanel(client, panelId, { - visualizations: filteredPanelVisualizations, - }); - return filteredPanelVisualizations; - } catch (error) { - throw new Error('Delete Visualization Error:' + error); - } - }; - //Edits all Visualizations in the Panel editVisualization = async ( client: ILegacyScopedClusterClient, diff --git a/server/adaptors/notebooks/default_backend.ts b/dashboards-observability/server/adaptors/notebooks/default_backend.ts similarity index 100% rename from server/adaptors/notebooks/default_backend.ts rename to dashboards-observability/server/adaptors/notebooks/default_backend.ts diff --git a/server/adaptors/notebooks/index.ts b/dashboards-observability/server/adaptors/notebooks/index.ts similarity index 100% rename from server/adaptors/notebooks/index.ts rename to dashboards-observability/server/adaptors/notebooks/index.ts diff --git a/server/adaptors/notebooks/notebook_adaptor.ts b/dashboards-observability/server/adaptors/notebooks/notebook_adaptor.ts similarity index 100% rename from server/adaptors/notebooks/notebook_adaptor.ts rename to dashboards-observability/server/adaptors/notebooks/notebook_adaptor.ts diff --git a/server/adaptors/notebooks/zeppelin_backend.ts b/dashboards-observability/server/adaptors/notebooks/zeppelin_backend.ts similarity index 100% rename from server/adaptors/notebooks/zeppelin_backend.ts rename to dashboards-observability/server/adaptors/notebooks/zeppelin_backend.ts diff --git a/server/adaptors/opensearch_observability_plugin.ts b/dashboards-observability/server/adaptors/opensearch_observability_plugin.ts similarity index 100% rename from server/adaptors/opensearch_observability_plugin.ts rename to dashboards-observability/server/adaptors/opensearch_observability_plugin.ts diff --git a/server/adaptors/ppl_datasource.ts b/dashboards-observability/server/adaptors/ppl_datasource.ts similarity index 100% rename from server/adaptors/ppl_datasource.ts rename to dashboards-observability/server/adaptors/ppl_datasource.ts diff --git a/server/adaptors/ppl_plugin.ts b/dashboards-observability/server/adaptors/ppl_plugin.ts similarity index 100% rename from server/adaptors/ppl_plugin.ts rename to dashboards-observability/server/adaptors/ppl_plugin.ts diff --git a/server/common/helpers/notebooks/default_notebook_schema.ts b/dashboards-observability/server/common/helpers/notebooks/default_notebook_schema.ts similarity index 100% rename from server/common/helpers/notebooks/default_notebook_schema.ts rename to dashboards-observability/server/common/helpers/notebooks/default_notebook_schema.ts diff --git a/server/common/helpers/notebooks/query_helpers.ts b/dashboards-observability/server/common/helpers/notebooks/query_helpers.ts similarity index 100% rename from server/common/helpers/notebooks/query_helpers.ts rename to dashboards-observability/server/common/helpers/notebooks/query_helpers.ts diff --git a/server/common/helpers/notebooks/sample_notebooks.ts b/dashboards-observability/server/common/helpers/notebooks/sample_notebooks.ts similarity index 100% rename from server/common/helpers/notebooks/sample_notebooks.ts rename to dashboards-observability/server/common/helpers/notebooks/sample_notebooks.ts diff --git a/server/common/helpers/notebooks/wreck_requests.ts b/dashboards-observability/server/common/helpers/notebooks/wreck_requests.ts similarity index 100% rename from server/common/helpers/notebooks/wreck_requests.ts rename to dashboards-observability/server/common/helpers/notebooks/wreck_requests.ts diff --git a/server/common/types/index.ts b/dashboards-observability/server/common/types/index.ts similarity index 100% rename from server/common/types/index.ts rename to dashboards-observability/server/common/types/index.ts diff --git a/server/index.ts b/dashboards-observability/server/index.ts similarity index 100% rename from server/index.ts rename to dashboards-observability/server/index.ts diff --git a/server/plugin.ts b/dashboards-observability/server/plugin.ts similarity index 100% rename from server/plugin.ts rename to dashboards-observability/server/plugin.ts diff --git a/server/routes/custom_panels/panels_router.ts b/dashboards-observability/server/routes/custom_panels/panels_router.ts similarity index 100% rename from server/routes/custom_panels/panels_router.ts rename to dashboards-observability/server/routes/custom_panels/panels_router.ts diff --git a/server/routes/custom_panels/visualizations_router.ts b/dashboards-observability/server/routes/custom_panels/visualizations_router.ts similarity index 68% rename from server/routes/custom_panels/visualizations_router.ts rename to dashboards-observability/server/routes/custom_panels/visualizations_router.ts index c935b9d0c..584d3f30c 100644 --- a/server/routes/custom_panels/visualizations_router.ts +++ b/dashboards-observability/server/routes/custom_panels/visualizations_router.ts @@ -36,7 +36,7 @@ export function VisualizationsRouter(router: IRouter) { request ); try { - const visualizationList = await customPanelBackend.viewSavedVisualiationList( + const visualizationList = await customPanelBackend.getAllSavedVisualizations( opensearchNotebooksClient ); return response.ok({ @@ -54,20 +54,13 @@ export function VisualizationsRouter(router: IRouter) { } ); - // Add a new visualization to the panel - router.post( + // get all saved visualizations by Id + router.get( { - path: `${API_PREFIX}/visualizations`, + path: `${API_PREFIX}/visualizations/{savedVisualizationId}`, validate: { - body: schema.object({ - panelId: schema.string(), - newVisualization: schema.object({ - id: schema.string(), - title: schema.string(), - query: schema.string(), - type: schema.string(), - timeField: schema.string(), - }), + params: schema.object({ + savedVisualizationId: schema.string(), }), }, }, @@ -79,21 +72,18 @@ export function VisualizationsRouter(router: IRouter) { const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( request ); - try { - const newVisualizations = await customPanelBackend.addVisualization( + const savedVisualization = await customPanelBackend.getSavedVisualizationById( opensearchNotebooksClient, - request.body.panelId, - request.body.newVisualization + request.params.savedVisualizationId ); return response.ok({ body: { - message: 'Visualization Added', - visualizations: newVisualizations, + visualization: savedVisualization, }, }); } catch (error) { - console.error('Issue in adding visualization:', error); + console.error('Issue in fetching saved visualizations by ids:', error); return response.custom({ statusCode: error.statusCode || 500, body: error.message, @@ -102,22 +92,14 @@ export function VisualizationsRouter(router: IRouter) { } ); - // Add a new visualization to panel from event explorer - // NOTE: This is a separate endpoint for adding event explorer visualizations to Operational Panels - // Please use `id = 'panelViz_' + htmlIdGenerator()()` to create unique visualization Id + // Add a new visualization to the panel router.post( { - path: `${API_PREFIX}/visualizations/event_explorer`, + path: `${API_PREFIX}/visualizations`, validate: { body: schema.object({ panelId: schema.string(), - newVisualization: schema.object({ - id: schema.string(), - title: schema.string(), - query: schema.string(), - type: schema.string(), - timeField: schema.string(), - }), + savedVisualizationId: schema.string(), }), }, }, @@ -131,10 +113,10 @@ export function VisualizationsRouter(router: IRouter) { ); try { - const newVisualizations = await customPanelBackend.addVisualizationFromEvents( + const newVisualizations = await customPanelBackend.addVisualization( opensearchNotebooksClient, request.body.panelId, - request.body.newVisualization + request.body.savedVisualizationId ); return response.ok({ body: { @@ -159,14 +141,8 @@ export function VisualizationsRouter(router: IRouter) { validate: { body: schema.object({ panelId: schema.string(), + savedVisualizationId: schema.string(), oldVisualizationId: schema.string(), - newVisualization: schema.object({ - id: schema.string(), - title: schema.string(), - query: schema.string(), - type: schema.string(), - timeField: schema.string(), - }), }), }, }, @@ -183,7 +159,7 @@ export function VisualizationsRouter(router: IRouter) { const newVisualizations = await customPanelBackend.addVisualization( opensearchNotebooksClient, request.body.panelId, - request.body.newVisualization, + request.body.savedVisualizationId, request.body.oldVisualizationId ); return response.ok({ @@ -202,48 +178,6 @@ export function VisualizationsRouter(router: IRouter) { } ); - // Delete an existing visualization - router.delete( - { - path: `${API_PREFIX}/visualizations/{panelId}/{visualizationId}`, - validate: { - params: schema.object({ - panelId: schema.string(), - visualizationId: schema.string(), - }), - }, - }, - async ( - context, - request, - response - ): Promise> => { - const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( - request - ); - - try { - const newVisualizations = await customPanelBackend.deleteVisualization( - opensearchNotebooksClient, - request.params.panelId, - request.params.visualizationId - ); - return response.ok({ - body: { - message: 'Visualization Deleted', - visualizations: newVisualizations, - }, - }); - } catch (error) { - console.error('Issue in deleting visualization:', error); - return response.custom({ - statusCode: error.statusCode || 500, - body: error.message, - }); - } - } - ); - // changes the position of the mentioned visualizations // Also removes the visualiations not mentioned router.put( diff --git a/server/routes/dsl.ts b/dashboards-observability/server/routes/dsl.ts similarity index 100% rename from server/routes/dsl.ts rename to dashboards-observability/server/routes/dsl.ts diff --git a/server/routes/event_analytics/event_analytics_router.ts b/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts similarity index 76% rename from server/routes/event_analytics/event_analytics_router.ts rename to dashboards-observability/server/routes/event_analytics/event_analytics_router.ts index 54cbaf5c4..91cc6d9c6 100644 --- a/server/routes/event_analytics/event_analytics_router.ts +++ b/dashboards-observability/server/routes/event_analytics/event_analytics_router.ts @@ -65,6 +65,10 @@ export const registerEventAnalyticsRouter = ({ end: schema.string(), text: schema.string(), }), + selected_timestamp: schema.object({ + name: schema.string(), + type: schema.string() + }), selected_fields: schema.object({ tokens: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), text: schema.string(), @@ -104,6 +108,10 @@ export const registerEventAnalyticsRouter = ({ end: schema.string(), text: schema.string(), }), + selected_timestamp: schema.object({ + name: schema.string(), + type: schema.string() + }), selected_fields: schema.object({ tokens: schema.arrayOf(schema.object({}, { unknowns: 'allow' })), text: schema.string(), @@ -209,4 +217,67 @@ export const registerEventAnalyticsRouter = ({ result['message'] = savedRes['data']; return res.custom(result); }); + + router.post({ + path: `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/timestamp`, + validate: { + body: schema.object({ + name: schema.string(), + index: schema.string(), + type: schema.string(), + dsl_type: schema.string() + }) + } + }, + async ( + context, + req, + res + ) : Promise> => { + const savedRes = await savedObjectFacet.createSavedTimestamp(req); + const result: any = { + body: { + ...savedRes['data'] + } + }; + + if (savedRes['success']) return res.ok(result); + + result['statusCode'] = 500; + result['message'] = savedRes['data']; + return res.custom(result); + }); + + router.put({ + path: `${OBSERVABILITY_BASE}${EVENT_ANALYTICS}${SAVED_OBJECTS}/timestamp`, + validate: { + body: schema.object({ + objectId: schema.string(), + timestamp: schema.object({ + name: schema.string(), + index: schema.string(), + type: schema.string(), + dsl_type: schema.string() + }), + }) + } + }, + async ( + context, + req, + res + ) : Promise> => { + const savedRes = await savedObjectFacet.updateSavedTimestamp(req); + const result: any = { + body: { + ...savedRes['data'] + } + }; + + if (savedRes['success']) return res.ok(result); + + result['statusCode'] = 500; + result['message'] = savedRes['data']; + return res.custom(result); + }); } \ No newline at end of file diff --git a/server/routes/index.ts b/dashboards-observability/server/routes/index.ts similarity index 100% rename from server/routes/index.ts rename to dashboards-observability/server/routes/index.ts diff --git a/server/routes/notebooks/noteRouter.ts b/dashboards-observability/server/routes/notebooks/noteRouter.ts similarity index 100% rename from server/routes/notebooks/noteRouter.ts rename to dashboards-observability/server/routes/notebooks/noteRouter.ts diff --git a/server/routes/notebooks/paraRouter.ts b/dashboards-observability/server/routes/notebooks/paraRouter.ts similarity index 100% rename from server/routes/notebooks/paraRouter.ts rename to dashboards-observability/server/routes/notebooks/paraRouter.ts diff --git a/server/routes/notebooks/sqlRouter.ts b/dashboards-observability/server/routes/notebooks/sqlRouter.ts similarity index 100% rename from server/routes/notebooks/sqlRouter.ts rename to dashboards-observability/server/routes/notebooks/sqlRouter.ts diff --git a/server/routes/notebooks/vizRouter.ts b/dashboards-observability/server/routes/notebooks/vizRouter.ts similarity index 100% rename from server/routes/notebooks/vizRouter.ts rename to dashboards-observability/server/routes/notebooks/vizRouter.ts diff --git a/server/routes/ppl.ts b/dashboards-observability/server/routes/ppl.ts similarity index 100% rename from server/routes/ppl.ts rename to dashboards-observability/server/routes/ppl.ts diff --git a/server/routes/trace_analytics_dsl_router.ts b/dashboards-observability/server/routes/trace_analytics_dsl_router.ts similarity index 100% rename from server/routes/trace_analytics_dsl_router.ts rename to dashboards-observability/server/routes/trace_analytics_dsl_router.ts diff --git a/server/services/facets/dsl_facet.ts b/dashboards-observability/server/services/facets/dsl_facet.ts similarity index 100% rename from server/services/facets/dsl_facet.ts rename to dashboards-observability/server/services/facets/dsl_facet.ts diff --git a/server/services/facets/ppl_facet.ts b/dashboards-observability/server/services/facets/ppl_facet.ts similarity index 100% rename from server/services/facets/ppl_facet.ts rename to dashboards-observability/server/services/facets/ppl_facet.ts diff --git a/server/services/facets/saved_objects.ts b/dashboards-observability/server/services/facets/saved_objects.ts similarity index 72% rename from server/services/facets/saved_objects.ts rename to dashboards-observability/server/services/facets/saved_objects.ts index cd1976008..9e27a9ccb 100644 --- a/server/services/facets/saved_objects.ts +++ b/dashboards-observability/server/services/facets/saved_objects.ts @@ -53,11 +53,63 @@ export default class SavedObjectFacet { } } }; + const savedRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + res['success'] = true; + res['data'] = savedRes; + } catch (err: any) { + console.error('Event analytics create error: ', err); + res['data'] = err; + } + return res; + }; + + createTimestamp = async ( + request: any, + format: string, + ) => { + const res = { + success: false, + data: {} + }; + try { + const params = { + body: { + objectId: request.body.index, + 'timestamp': { + ...request.body + } + } + }; + const savedRes = await this.client.asScoped(request).callAsCurrentUser(format, params); + res['success'] = true; + res['data'] = savedRes; + } catch (err: any) { + console.error('Event analytics create timestamp error: ', err); + res['data'] = err; + } + return res; + } + + updateTimestamp = async ( + request: any, + format: string, + ) => { + const res = { + success: false, + data: {} + }; + try { + const params = { + objectId: request.body.objectId, + body: { + ...request.body + } + }; const savedQueryRes = await this.client.asScoped(request).callAsCurrentUser(format, params); res['success'] = true; res['data'] = savedQueryRes; } catch (err: any) { - console.error('Event analytics create error: ', err); + console.error('Event analytics update error: ', err); res['data'] = err; } return res; @@ -135,6 +187,14 @@ export default class SavedObjectFacet { return this.create(request, 'observability.createObject', 'savedVisualization'); }; + createSavedTimestamp = (request: any) => { + return this.createTimestamp(request, 'observability.createObject'); + }; + + updateSavedTimestamp = (request: any) => { + return this.updateTimestamp(request, 'observability.updateObjectById'); + }; + updateSavedQuery = (request: any) => { return this.update(request, 'observability.updateObjectById', 'savedQuery'); }; diff --git a/server/services/queryService.ts b/dashboards-observability/server/services/queryService.ts similarity index 100% rename from server/services/queryService.ts rename to dashboards-observability/server/services/queryService.ts diff --git a/server/services/utils/constants.ts b/dashboards-observability/server/services/utils/constants.ts similarity index 100% rename from server/services/utils/constants.ts rename to dashboards-observability/server/services/utils/constants.ts diff --git a/server/types.ts b/dashboards-observability/server/types.ts similarity index 100% rename from server/types.ts rename to dashboards-observability/server/types.ts diff --git a/test/__mocks__/coreMocks.ts b/dashboards-observability/test/__mocks__/coreMocks.ts similarity index 100% rename from test/__mocks__/coreMocks.ts rename to dashboards-observability/test/__mocks__/coreMocks.ts diff --git a/test/__mocks__/fileMock.js b/dashboards-observability/test/__mocks__/fileMock.js similarity index 100% rename from test/__mocks__/fileMock.js rename to dashboards-observability/test/__mocks__/fileMock.js diff --git a/test/__mocks__/httpClientMock.ts b/dashboards-observability/test/__mocks__/httpClientMock.ts similarity index 100% rename from test/__mocks__/httpClientMock.ts rename to dashboards-observability/test/__mocks__/httpClientMock.ts diff --git a/test/__mocks__/styleMock.js b/dashboards-observability/test/__mocks__/styleMock.js similarity index 100% rename from test/__mocks__/styleMock.js rename to dashboards-observability/test/__mocks__/styleMock.js diff --git a/test/constants.ts b/dashboards-observability/test/constants.ts similarity index 100% rename from test/constants.ts rename to dashboards-observability/test/constants.ts diff --git a/test/jest.config.js b/dashboards-observability/test/jest.config.js similarity index 100% rename from test/jest.config.js rename to dashboards-observability/test/jest.config.js diff --git a/test/setup.jest.ts b/dashboards-observability/test/setup.jest.ts similarity index 100% rename from test/setup.jest.ts rename to dashboards-observability/test/setup.jest.ts diff --git a/test/setupTests.ts b/dashboards-observability/test/setupTests.ts similarity index 100% rename from test/setupTests.ts rename to dashboards-observability/test/setupTests.ts diff --git a/tsconfig.json b/dashboards-observability/tsconfig.json similarity index 100% rename from tsconfig.json rename to dashboards-observability/tsconfig.json diff --git a/yarn.lock b/dashboards-observability/yarn.lock similarity index 99% rename from yarn.lock rename to dashboards-observability/yarn.lock index 472fa0ade..e105760b6 100644 --- a/yarn.lock +++ b/dashboards-observability/yarn.lock @@ -1562,9 +1562,9 @@ ignore@^4.0.6: integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== immer@^9.0.1: - version "9.0.5" - resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.5.tgz#a7154f34fe7064f15f00554cc94c66cc0bf453ec" - integrity sha512-2WuIehr2y4lmYz9gaQzetPR2ECniCifk4ORaQbU3g5EalLt+0IVTosEPJ5BoYl/75ky2mivzdRzV8wWgQGOSYQ== + version "9.0.6" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.6.tgz#7a96bf2674d06c8143e327cbf73539388ddf1a73" + integrity sha512-G95ivKpy+EvVAnAab4fVa4YGYn24J1SpEktnJX7JJ45Bd7xqME/SCplFzYFmTbrkwZbQ4xJK1xMTUYBkN6pWsQ== import-fresh@^3.0.0: version "3.3.0" @@ -2342,9 +2342,9 @@ pretty-bytes@^5.4.1: integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== prismjs@^1.22.0, prismjs@~1.24.0: - version "1.24.1" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.1.tgz#c4d7895c4d6500289482fa8936d9cdd192684036" - integrity sha512-mNPsedLuk90RVJioIky8ANZEwYm5w9LcvCXrxHlwf4fNVSn8jEipMybMkWUyyF0JhnC+C4VcOVSBuHRKs1L5Ow== + version "1.25.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756" + integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg== process-nextick-args@~2.0.0: version "2.0.1" diff --git a/opensearch-observability/.codecov.yml b/opensearch-observability/.codecov.yml new file mode 100644 index 000000000..7adfbc9d2 --- /dev/null +++ b/opensearch-observability/.codecov.yml @@ -0,0 +1,40 @@ +--- +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +## +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +## + +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + status: + project: + default: + target: 70% # the required coverage value + threshold: 1% # the leniency in hitting the target diff --git a/opensearch-observability/.editorconfig b/opensearch-observability/.editorconfig new file mode 100644 index 000000000..de2de324e --- /dev/null +++ b/opensearch-observability/.editorconfig @@ -0,0 +1,38 @@ +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +## +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +## + +# config for Pinterest ktlint. https://github.com/pinterest/ktlint + +root = true + +[*.{kt,kts}] +# possible values: number (e.g. 2), "unset" (makes ktlint ignore indentation completely) +indent_size=4 + +# we have detekt also checking for max line length. Disable the linter and use only one tool to check for max line length. +# See https://github.com/arturbosch/detekt +max_line_length=off diff --git a/opensearch-observability/.gitignore b/opensearch-observability/.gitignore new file mode 100644 index 000000000..de6e7bf2c --- /dev/null +++ b/opensearch-observability/.gitignore @@ -0,0 +1,323 @@ +.DS_Store +.project +.classpath + +*.iml +.gradle +out +local-test +.local-* + +npm-debug.log* +node_modules +/build/ +/public/app.css +.idea/ +.vscode/ +yarn-error.log +/coverage/ +.history/ +.eslintcache + +# OpenSearch Dashboards +.empty + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Kotlin ### +# Compiled class file + +# Log file + +# BlueJ files + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# AWS plugin +.idea/aws.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +local-test +.local-* diff --git a/opensearch-observability/build-tools/opensearchplugin-coverage.gradle b/opensearch-observability/build-tools/opensearchplugin-coverage.gradle new file mode 100644 index 000000000..d2d745a12 --- /dev/null +++ b/opensearch-observability/build-tools/opensearchplugin-coverage.gradle @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +/** + * OpenSearch Plugin build tools don't work with the Gradle Jacoco Plugin to report coverage out of the box. + * https://github.com/elastic/elasticsearch/issues/28867. + * + * This code sets up coverage reporting manually for OpenSearch plugin tests. This is complicated because: + * 1. The OpenSearch integTest Task doesn't implement Gradle's JavaForkOptions so we have to manually start the jacoco agent with the test JVM + * 2. The cluster nodes are stopped using 'kill -9' which means jacoco can't dump it's execution output to a file on VM shutdown + * 3. The Java Security Manager prevents JMX from writing execution output to the file. + * + * To workaround these we start the cluster with jmx enabled and then use Jacoco's JMX MBean to get the execution data before the + * cluster is stopped and dump it to a file. Luckily our current security policy seems to allow this. This will also probably + * break if there are multiple nodes in the integTestCluster. But for now... it sorta works. + */ + +// Get gradle to generate the required jvm agent arg for us using a dummy tasks of type Test. Unfortunately Elastic's +// testing tasks don't derive from Test so the jacoco plugin can't do this automatically. +def jacocoDir = "${buildDir}/jacoco" + +task dummyTest(type: Test) { + enabled = false + workingDir = file("/") // Force absolute path to jacoco agent jar + jacoco { + destinationFile = file("${jacocoDir}/test.exec") + destinationFile.parentFile.mkdirs() + jmx = true + } +} + +task dummyIntegTest(type: Test) { + enabled = false + workingDir = file("/") // Force absolute path to jacoco agent jar + jacoco { + destinationFile = file("${jacocoDir}/integTest.exec") + destinationFile.parentFile.mkdirs() + jmx = true + } +} + +integTest { + systemProperty 'jacoco.dir', "${jacocoDir}" +} + +jacocoTestReport { + dependsOn integTest, test + executionData dummyTest.jacoco.destinationFile, dummyIntegTest.jacoco.destinationFile + sourceDirectories.from = "src/main/kotlin" + classDirectories.from = sourceSets.main.output + reports { + html.enabled = true // human readable + xml.enabled = true // for coverlay + } +} + +allprojects{ + afterEvaluate { + jacocoTestReport.dependsOn integTest + + testClusters.integTest { + jvmArgs " ${dummyIntegTest.jacoco.getAsJvmArg()}".replace('javaagent:','javaagent:/') + systemProperty 'com.sun.management.jmxremote', "true" + systemProperty 'com.sun.management.jmxremote.authenticate', "false" + systemProperty 'com.sun.management.jmxremote.port', "7777" + systemProperty 'com.sun.management.jmxremote.ssl', "false" + systemProperty 'java.rmi.server.hostname', "127.0.0.1" + } + } +} diff --git a/opensearch-observability/build-tools/pkgbuild.gradle b/opensearch-observability/build-tools/pkgbuild.gradle new file mode 100644 index 000000000..50c762890 --- /dev/null +++ b/opensearch-observability/build-tools/pkgbuild.gradle @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +apply plugin: 'nebula.ospackage' + +// This is afterEvaluate because the bundlePlugin ZIP task is updated afterEvaluate and changes the ZIP name to match the plugin name +afterEvaluate { + ospackage { + packageName = "${name}" + release = isSnapshot ? "0.1" : '1' + version = "${project.version}" - "-SNAPSHOT" + + into '/usr/share/opensearch/plugins' + from(zipTree(bundlePlugin.archivePath)) { + into opensearchplugin.name + } + + user 'root' + permissionGroup 'root' + fileMode 0644 + dirMode 0755 + + requires('opensearch-oss', versions.opensearch, EQUAL) + packager = 'Amazon' + vendor = 'Amazon' + os = 'LINUX' + prefix '/usr' + + license 'ASL-2.0' + maintainer 'OpenSearch Team ' + url 'https://opensearch.org/' + summary ''' + OpenSearch Observability. + Reference documentation can be found at https://opensearch.org/docs/. + '''.stripIndent().replace('\n', ' ').trim() + } + + buildRpm { + arch = 'NOARCH' + archiveName "${packageName}-${version}.rpm" + dependsOn 'assemble' + } + + buildDeb { + arch = 'amd64' + archiveName "${packageName}-${version}.deb" + dependsOn 'assemble' + } +} diff --git a/opensearch-observability/build.gradle b/opensearch-observability/build.gradle new file mode 100644 index 000000000..5c974aaf5 --- /dev/null +++ b/opensearch-observability/build.gradle @@ -0,0 +1,306 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import org.opensearch.gradle.test.RestIntegTestTask +import java.util.concurrent.Callable + +buildscript { + ext { + opensearch_version = System.getProperty("opensearch.version", "1.1.0-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) + kotlin_version = System.getProperty("kotlin.version", "1.4.0") + } + + repositories { + mavenLocal() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + jcenter() + } + + dependencies { + classpath "org.opensearch.gradle:build-tools:${opensearch_version}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin_version}" + classpath "org.jetbrains.kotlin:kotlin-allopen:${kotlin_version}" + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.12.0" + classpath "org.jacoco:org.jacoco.agent:0.8.5" + } +} + +plugins { + id 'nebula.ospackage' version "8.3.0" + id "com.dorongold.task-tree" version "1.5" + id 'java-library' +} + +apply plugin: 'java' +apply plugin: 'jacoco' +apply plugin: 'idea' +apply plugin: 'opensearch.opensearchplugin' +apply plugin: 'opensearch.testclusters' +apply plugin: 'io.gitlab.arturbosch.detekt' +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'org.jetbrains.kotlin.plugin.allopen' + +def usingRemoteCluster = System.properties.containsKey('tests.rest.cluster') || System.properties.containsKey('tests.cluster') +def usingMultiNode = project.properties.containsKey('numNodes') + +check.dependsOn jacocoTestReport + +opensearchplugin { + name 'opensearch-observability' + description 'OpenSearch Plugin for OpenSearch Dashboards Observability' + classname "org.opensearch.observability.ObservabilityPlugin" +} + +allOpen { + annotation("org.opensearch.observability.util.OpenForTesting") +} + +configurations { + ktlint +} + +detekt { + config = files("detekt.yml") + buildUponDefaultConfig = true +} + +configurations.testCompile { + exclude module: "securemock" +} + +configurations.all { + if (it.state != Configuration.State.UNRESOLVED) return + resolutionStrategy { + force "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + force "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" + force "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.11.4" + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + jcenter() +} + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE.txt') + isSnapshot = "true" == System.getProperty("build.snapshot", "true") +} + +plugins.withId('java') { + sourceCompatibility = targetCompatibility = "1.8" +} + +plugins.withId('org.jetbrains.kotlin.jvm') { + compileKotlin.kotlinOptions.jvmTarget = compileTestKotlin.kotlinOptions.jvmTarget = "1.8" +} + +allprojects { + group = "org.opensearch" + + version = "${opensearch_version}" - "-SNAPSHOT" + ".0" + if (isSnapshot) { + version += "-SNAPSHOT" + } + + plugins.withId('java') { + sourceCompatibility = targetCompatibility = "1.8" + } +} + +dependencies { + compile "org.opensearch:opensearch:${opensearch_version}" + compile "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + compile "org.jetbrains.kotlin:kotlin-stdlib-common:${kotlin_version}" + compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" + compile "${group}:common-utils:${common_utils_version}" + compile group: 'com.google.guava', name: 'guava', version: '15.0' + testImplementation( + 'org.assertj:assertj-core:3.16.1', + 'org.junit.jupiter:junit-jupiter-api:5.6.2' + ) + testRuntime('org.junit.jupiter:junit-jupiter-engine:5.6.2') + testCompile "org.opensearch.test:framework:${opensearch_version}" + testCompile "org.jetbrains.kotlin:kotlin-test:${kotlin_version}" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" + testCompile "org.mockito:mockito-core:2.23.0" + testCompile "com.google.code.gson:gson:2.8.6" + + ktlint "com.pinterest:ktlint:0.41.0" +} + +javadoc.enabled = false // turn off javadoc as it barfs on Kotlin code +licenseHeaders.enabled = true +// no need to validate pom, as we do not upload to maven/sonatype +validateNebulaPom.enabled = false +dependencyLicenses.enabled = false +thirdPartyAudit.enabled = false +// Allow @Test to be used in test classes not inherited from LuceneTestCase. +// see https://github.com/elastic/elasticsearch/blob/master/buildSrc/src/main/resources/forbidden/es-test-signatures.txt +forbiddenApis.ignoreFailures = true +// Allow test cases to be named Tests without having to be inherited from LuceneTestCase. +// see https://github.com/elastic/elasticsearch/blob/323f312bbc829a63056a79ebe45adced5099f6e6/buildSrc/src/main/java/org/elasticsearch/gradle/precommit/TestingConventionsTasks.java +testingConventions.enabled = false +loggerUsageCheck.enabled = false + +test { + systemProperty 'tests.security.manager', 'false' + useJUnitPlatform() +} + +File repo = file("$buildDir/testclusters/repo") +def _numNodes = findProperty('numNodes') as Integer ?: 1 + +def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile +es_tmp_dir.mkdirs() + +// As of ES 7.7 the sample-extension-plugin is being added to the list of plugins for the testCluster during build before +// the job-scheduler plugin (not a dependency for opensearch-observability) is causing build failures. +// The job-scheduler zip is added explicitly above but the sample-extension-plugin is added implicitly at some time during evaluation. +// Will need to do a deep dive to find out exactly what task adds the sample-extension-plugin and add job-scheduler there but a temporary hack is to +// reorder the plugins list after evaluation but prior to task execution when the plugins are installed. +afterEvaluate { + testClusters.integTest.nodes.each { node -> + def plugins = node.plugins + def firstPlugin = plugins.get(0) + plugins.remove(0) + plugins.add(firstPlugin) + } +} + +tasks.withType(licenseHeaders.class) { + additionalLicense 'AL ', 'Apache', 'Licensed under the Apache License, Version 2.0 (the "License")' +} + +task integTest(type: RestIntegTestTask) { + description = "Run tests against a cluster that has security enabled" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath +} +tasks.named("check").configure { dependsOn(integTest) } + +integTest { + systemProperty 'tests.security.manager', 'false' + systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for + // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. + doFirst { + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can + // use longer timeouts for requests. + def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null + systemProperty 'cluster.debug', isDebuggingCluster + // Set number of nodes system property to be used in tests + systemProperty 'cluster.number_of_nodes', "${_numNodes}" + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + + // The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable + if (System.getProperty("test.debug") != null) { + jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000' + } + + // https://github.com/opensearch-project/index-management/pull/93 + if (System.getProperty("tests.clustername") != null) { + exclude 'org/opensearch/observability/ObservabilityPluginIT.class' + } +} + + +Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin"); +integTest.dependsOn(bundle) +integTest.getClusters().forEach { c -> c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) } + +testClusters.integTest { + testDistribution = "INTEG_TEST" + // Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1 + if (_numNodes > 1) numberOfNodes = _numNodes + // When running integration tests it doesn't forward the --debug-jvm to the cluster anymore + // i.e. we have to use a custom property to flag when we want to debug opensearch JVM + // since we also support multi node integration tests we increase debugPort per node + if (System.getProperty("cluster.debug") != null) { + def debugPort = 5005 + nodes.forEach { node -> + node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}") + debugPort += 1 + } + } + setting 'path.repo', repo.absolutePath +} + +run { + doFirst { + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + useCluster testClusters.integTest +} + +task ktlint(type: JavaExec, group: "verification") { + description = "Check Kotlin code style." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "src/**/*.kt" + // to generate report in checkstyle format prepend following args: + // "--reporter=plain", "--reporter=checkstyle,output=${buildDir}/ktlint.xml" + // see https://github.com/pinterest/ktlint#usage for more +} + +check.dependsOn ktlint + +task ktlintFormat(type: JavaExec, group: "formatting") { + description = "Fix Kotlin code style deviations." + main = "com.pinterest.ktlint.Main" + classpath = configurations.ktlint + args "-F", "src/**/*.kt" +} + +compileKotlin { kotlinOptions.freeCompilerArgs = ['-Xjsr305=strict'] } + +// Only apply jacoco test coverage if we are running a local single node cluster +if (!usingRemoteCluster && !usingMultiNode) { + apply from: 'build-tools/opensearchplugin-coverage.gradle' +} + +apply from: 'build-tools/pkgbuild.gradle' diff --git a/opensearch-observability/config/checkstyle/checkstyle.xml b/opensearch-observability/config/checkstyle/checkstyle.xml new file mode 100644 index 000000000..286aef81d --- /dev/null +++ b/opensearch-observability/config/checkstyle/checkstyle.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opensearch-observability/config/checkstyle/google_checks.xml b/opensearch-observability/config/checkstyle/google_checks.xml new file mode 100644 index 000000000..662740ff6 --- /dev/null +++ b/opensearch-observability/config/checkstyle/google_checks.xml @@ -0,0 +1,291 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/opensearch-observability/config/checkstyle/suppressions.xml b/opensearch-observability/config/checkstyle/suppressions.xml new file mode 100644 index 000000000..5926732a0 --- /dev/null +++ b/opensearch-observability/config/checkstyle/suppressions.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/opensearch-observability/detekt.yml b/opensearch-observability/detekt.yml new file mode 100644 index 000000000..dbcca8ab6 --- /dev/null +++ b/opensearch-observability/detekt.yml @@ -0,0 +1,39 @@ +--- +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +## +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +## + +style: + ForbiddenComment: + active: false + MaxLineLength: + maxLineLength: 150 + ThrowsCount: + active: true + max: 10 + ReturnCount: + active: true + max: 10 diff --git a/opensearch-observability/gradle/wrapper/gradle-wrapper.jar b/opensearch-observability/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..62d4c0535 Binary files /dev/null and b/opensearch-observability/gradle/wrapper/gradle-wrapper.jar differ diff --git a/opensearch-observability/gradle/wrapper/gradle-wrapper.properties b/opensearch-observability/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..db803810d --- /dev/null +++ b/opensearch-observability/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,33 @@ +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +# +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +# + +#Wed Jul 29 13:30:55 PDT 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/opensearch-observability/gradlew b/opensearch-observability/gradlew new file mode 100755 index 000000000..2fe81a7d9 --- /dev/null +++ b/opensearch-observability/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/opensearch-observability/gradlew.bat b/opensearch-observability/gradlew.bat new file mode 100644 index 000000000..24467a141 --- /dev/null +++ b/opensearch-observability/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/opensearch-observability/src/main/config/observability.yml b/opensearch-observability/src/main/config/observability.yml new file mode 100644 index 000000000..18acb0654 --- /dev/null +++ b/opensearch-observability/src/main/config/observability.yml @@ -0,0 +1,50 @@ +--- +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +## +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +## + +# configuration file for the observability plugin +opensearch.notebooks: + general: + operationTimeoutMs: 60000 # 60 seconds, Minimum 100ms + defaultItemsQueryCount: 100 # default number of items to query + polling: + jobLockDurationSeconds: 300 # 5 Minutes, Minimum 10 seconds + minPollingDurationSeconds: 300 # 5 Minutes, Minimum 60 seconds + maxPollingDurationSeconds: 900 # 15 Minutes, Minimum 5 Minutes + maxLockRetries: 1 # Max number of retries to retry locking + access: + adminAccess: "AllNotebooks" + # adminAccess values: + ## Standard -> Admin user access follows standard user + ## AllNotebooks -> Admin user with "all_access" role can see all observability objects of all users. + filterBy: "NoFilter" # Applied when tenant != __user__ + # filterBy values: + ## NoFilter -> everyone see each other's observability objects + ## User -> observability objects are visible to only themselves + ## Roles -> observability objects are visible to users having any one of the role of creator + ## BackendRoles -> observability objects are visible to users having any one of the backend role of creator + ignoreRoles: ["own_index", "kibana_user", "notebooks_full_access", "notebooks_read_access"] diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/ObservabilityPlugin.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/ObservabilityPlugin.kt new file mode 100644 index 000000000..0ed142035 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/ObservabilityPlugin.kt @@ -0,0 +1,141 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ +package org.opensearch.observability + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionResponse +import org.opensearch.client.Client +import org.opensearch.cluster.metadata.IndexNameExpressionResolver +import org.opensearch.cluster.node.DiscoveryNodes +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.io.stream.NamedWriteableRegistry +import org.opensearch.common.settings.ClusterSettings +import org.opensearch.common.settings.IndexScopedSettings +import org.opensearch.common.settings.Setting +import org.opensearch.common.settings.Settings +import org.opensearch.common.settings.SettingsFilter +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.env.Environment +import org.opensearch.env.NodeEnvironment +import org.opensearch.observability.action.CreateObservabilityObjectAction +import org.opensearch.observability.action.DeleteObservabilityObjectAction +import org.opensearch.observability.action.GetObservabilityObjectAction +import org.opensearch.observability.action.UpdateObservabilityObjectAction +import org.opensearch.observability.index.ObservabilityIndex +import org.opensearch.observability.resthandler.ObservabilityRestHandler +import org.opensearch.observability.settings.PluginSettings +import org.opensearch.plugins.ActionPlugin +import org.opensearch.plugins.Plugin +import org.opensearch.repositories.RepositoriesService +import org.opensearch.rest.RestController +import org.opensearch.rest.RestHandler +import org.opensearch.script.ScriptService +import org.opensearch.threadpool.ThreadPool +import org.opensearch.watcher.ResourceWatcherService +import java.util.function.Supplier + +/** + * Entry point of the OpenSearch Observability plugin. + * This class initializes the rest handlers. + */ +class ObservabilityPlugin : Plugin(), ActionPlugin { + + companion object { + const val PLUGIN_NAME = "opensearch-observability" + const val LOG_PREFIX = "observability" + const val BASE_OBSERVABILITY_URI = "/_plugins/_observability" + } + + /** + * {@inheritDoc} + */ + override fun getSettings(): List> { + return PluginSettings.getAllSettings() + } + + /** + * {@inheritDoc} + */ + override fun createComponents( + client: Client, + clusterService: ClusterService, + threadPool: ThreadPool, + resourceWatcherService: ResourceWatcherService, + scriptService: ScriptService, + xContentRegistry: NamedXContentRegistry, + environment: Environment, + nodeEnvironment: NodeEnvironment, + namedWriteableRegistry: NamedWriteableRegistry, + indexNameExpressionResolver: IndexNameExpressionResolver, + repositoriesServiceSupplier: Supplier + ): Collection { + PluginSettings.addSettingsUpdateConsumer(clusterService) + ObservabilityIndex.initialize(client, clusterService) + return emptyList() + } + + /** + * {@inheritDoc} + */ + override fun getRestHandlers( + settings: Settings, + restController: RestController, + clusterSettings: ClusterSettings, + indexScopedSettings: IndexScopedSettings, + settingsFilter: SettingsFilter, + indexNameExpressionResolver: IndexNameExpressionResolver, + nodesInCluster: Supplier + ): List { + return listOf( + ObservabilityRestHandler() + ) + } + + /** + * {@inheritDoc} + */ + override fun getActions(): List> { + return listOf( + ActionPlugin.ActionHandler( + CreateObservabilityObjectAction.ACTION_TYPE, + CreateObservabilityObjectAction::class.java + ), + ActionPlugin.ActionHandler( + DeleteObservabilityObjectAction.ACTION_TYPE, + DeleteObservabilityObjectAction::class.java + ), + ActionPlugin.ActionHandler( + GetObservabilityObjectAction.ACTION_TYPE, + GetObservabilityObjectAction::class.java + ), + ActionPlugin.ActionHandler( + UpdateObservabilityObjectAction.ACTION_TYPE, + UpdateObservabilityObjectAction::class.java + ) + ) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/CreateObservabilityObjectAction.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/CreateObservabilityObjectAction.kt new file mode 100644 index 000000000..acb1c15a6 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/CreateObservabilityObjectAction.kt @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.action + +import org.opensearch.action.ActionType +import org.opensearch.action.support.ActionFilters +import org.opensearch.client.Client +import org.opensearch.common.inject.Inject +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User +import org.opensearch.observability.model.CreateObservabilityObjectRequest +import org.opensearch.observability.model.CreateObservabilityObjectResponse +import org.opensearch.transport.TransportService + +/** + * Create ObservabilityObject transport action + */ +internal class CreateObservabilityObjectAction @Inject constructor( + transportService: TransportService, + client: Client, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry +) : PluginBaseAction( + NAME, + transportService, + client, + actionFilters, + ::CreateObservabilityObjectRequest +) { + companion object { + private const val NAME = "cluster:admin/opensearch/observability/create" + internal val ACTION_TYPE = ActionType(NAME, ::CreateObservabilityObjectResponse) + } + + /** + * {@inheritDoc} + */ + override fun executeRequest( + request: CreateObservabilityObjectRequest, + user: User? + ): CreateObservabilityObjectResponse { + return ObservabilityActions.create(request, user) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/DeleteObservabilityObjectAction.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/DeleteObservabilityObjectAction.kt new file mode 100644 index 000000000..2294ec567 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/DeleteObservabilityObjectAction.kt @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.action + +import org.opensearch.action.ActionType +import org.opensearch.action.support.ActionFilters +import org.opensearch.client.Client +import org.opensearch.common.inject.Inject +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User +import org.opensearch.observability.model.DeleteObservabilityObjectRequest +import org.opensearch.observability.model.DeleteObservabilityObjectResponse +import org.opensearch.transport.TransportService + +/** + * Delete ObservabilityObject transport action + */ +internal class DeleteObservabilityObjectAction @Inject constructor( + transportService: TransportService, + client: Client, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry +) : PluginBaseAction( + NAME, + transportService, + client, + actionFilters, + ::DeleteObservabilityObjectRequest +) { + companion object { + private const val NAME = "cluster:admin/opensearch/observability/delete" + internal val ACTION_TYPE = ActionType(NAME, ::DeleteObservabilityObjectResponse) + } + + /** + * {@inheritDoc} + */ + override fun executeRequest(request: DeleteObservabilityObjectRequest, user: User?): DeleteObservabilityObjectResponse { + return ObservabilityActions.delete(request, user) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/GetObservabilityObjectAction.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/GetObservabilityObjectAction.kt new file mode 100644 index 000000000..f549e8bf7 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/GetObservabilityObjectAction.kt @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.action + +import org.opensearch.action.ActionType +import org.opensearch.action.support.ActionFilters +import org.opensearch.client.Client +import org.opensearch.common.inject.Inject +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User +import org.opensearch.observability.model.GetObservabilityObjectRequest +import org.opensearch.observability.model.GetObservabilityObjectResponse +import org.opensearch.transport.TransportService + +/** + * Get ObservabilityObject transport action + */ +internal class GetObservabilityObjectAction @Inject constructor( + transportService: TransportService, + client: Client, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry +) : PluginBaseAction( + NAME, + transportService, + client, + actionFilters, + ::GetObservabilityObjectRequest +) { + companion object { + private const val NAME = "cluster:admin/opensearch/observability/get" + internal val ACTION_TYPE = ActionType(NAME, ::GetObservabilityObjectResponse) + } + + /** + * {@inheritDoc} + */ + override fun executeRequest(request: GetObservabilityObjectRequest, user: User?): GetObservabilityObjectResponse { + return ObservabilityActions.get(request, user) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/ObservabilityActions.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/ObservabilityActions.kt new file mode 100644 index 000000000..c8bda7699 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/ObservabilityActions.kt @@ -0,0 +1,304 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.action + +import org.opensearch.OpenSearchStatusException +import org.opensearch.commons.authuser.User +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.index.ObservabilityIndex +import org.opensearch.observability.model.CreateObservabilityObjectRequest +import org.opensearch.observability.model.CreateObservabilityObjectResponse +import org.opensearch.observability.model.DeleteObservabilityObjectRequest +import org.opensearch.observability.model.DeleteObservabilityObjectResponse +import org.opensearch.observability.model.GetObservabilityObjectRequest +import org.opensearch.observability.model.GetObservabilityObjectResponse +import org.opensearch.observability.model.ObservabilityObjectDoc +import org.opensearch.observability.model.ObservabilityObjectSearchResult +import org.opensearch.observability.model.UpdateObservabilityObjectRequest +import org.opensearch.observability.model.UpdateObservabilityObjectResponse +import org.opensearch.observability.security.UserAccessManager +import org.opensearch.observability.util.logger +import org.opensearch.rest.RestStatus +import java.time.Instant + +/** + * ObservabilityObject index operation actions. + */ +internal object ObservabilityActions { + private val log by logger(ObservabilityActions::class.java) + + /** + * Create new ObservabilityObject + * @param request [CreateObservabilityObjectRequest] object + * @return [CreateObservabilityObjectResponse] + */ + fun create(request: CreateObservabilityObjectRequest, user: User?): CreateObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-create") + UserAccessManager.validateUser(user) + val currentTime = Instant.now() + val objectDoc = ObservabilityObjectDoc( + "ignore", + currentTime, + currentTime, + UserAccessManager.getUserTenant(user), + UserAccessManager.getAllAccessInfo(user), + request.type, + request.objectData + ) + val docId = ObservabilityIndex.createObservabilityObject(objectDoc, request.objectId) + docId ?: throw OpenSearchStatusException( + "ObservabilityObject Creation failed", + RestStatus.INTERNAL_SERVER_ERROR + ) + return CreateObservabilityObjectResponse(docId) + } + + /** + * Update ObservabilityObject + * @param request [UpdateObservabilityObjectRequest] object + * @return [UpdateObservabilityObjectResponse] + */ + fun update(request: UpdateObservabilityObjectRequest, user: User?): UpdateObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-update ${request.objectId}") + UserAccessManager.validateUser(user) + val observabilityObject = ObservabilityIndex.getObservabilityObject(request.objectId) + observabilityObject + ?: throw OpenSearchStatusException( + "ObservabilityObject ${request.objectId} not found", + RestStatus.NOT_FOUND + ) + val currentDoc = observabilityObject.observabilityObjectDoc + if (!UserAccessManager.doesUserHasAccess(user, currentDoc.tenant, currentDoc.access)) { + throw OpenSearchStatusException( + "Permission denied for ObservabilityObject ${request.objectId}", + RestStatus.FORBIDDEN + ) + } + val currentTime = Instant.now() + val objectDoc = ObservabilityObjectDoc( + request.objectId, + currentTime, + currentDoc.createdTime, + UserAccessManager.getUserTenant(user), + UserAccessManager.getAllAccessInfo(user), + request.type, + request.objectData + ) + if (!ObservabilityIndex.updateObservabilityObject(request.objectId, objectDoc)) { + throw OpenSearchStatusException("ObservabilityObject Update failed", RestStatus.INTERNAL_SERVER_ERROR) + } + return UpdateObservabilityObjectResponse(request.objectId) + } + + /** + * Get ObservabilityObject info + * @param request [GetObservabilityObjectRequest] object + * @return [GetObservabilityObjectResponse] + */ + fun get(request: GetObservabilityObjectRequest, user: User?): GetObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-get ${request.objectIds}") + UserAccessManager.validateUser(user) + return when (request.objectIds.size) { + 0 -> getAll(request, user) + 1 -> info(request.objectIds.first(), user) + else -> info(request.objectIds, user) + } + } + + /** + * Get ObservabilityObject info + * @param objectId object id + * @param user the user info object + * @return [GetObservabilityObjectResponse] + */ + private fun info(objectId: String, user: User?): GetObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-info $objectId") + val observabilityObjectDocInfo = ObservabilityIndex.getObservabilityObject(objectId) + observabilityObjectDocInfo + ?: run { + throw OpenSearchStatusException("ObservabilityObject $objectId not found", RestStatus.NOT_FOUND) + } + val currentDoc = observabilityObjectDocInfo.observabilityObjectDoc + if (!UserAccessManager.doesUserHasAccess(user, currentDoc.tenant, currentDoc.access)) { + throw OpenSearchStatusException("Permission denied for ObservabilityObject $objectId", RestStatus.FORBIDDEN) + } + val docInfo = ObservabilityObjectDoc( + objectId, + currentDoc.updatedTime, + currentDoc.createdTime, + currentDoc.tenant, + currentDoc.access, + currentDoc.type, + currentDoc.objectData + ) + return GetObservabilityObjectResponse( + ObservabilityObjectSearchResult(docInfo), + UserAccessManager.hasAllInfoAccess(user) + ) + } + + /** + * Get ObservabilityObject info + * @param objectIds object id set + * @param user the user info object + * @return [GetObservabilityObjectResponse] + */ + private fun info(objectIds: Set, user: User?): GetObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-info $objectIds") + val objectDocs = ObservabilityIndex.getObservabilityObjects(objectIds) + if (objectDocs.size != objectIds.size) { + val mutableSet = objectIds.toMutableSet() + objectDocs.forEach { mutableSet.remove(it.id) } + throw OpenSearchStatusException( + "ObservabilityObject $mutableSet not found", + RestStatus.NOT_FOUND + ) + } + objectDocs.forEach { + val currentDoc = it.observabilityObjectDoc + if (!UserAccessManager.doesUserHasAccess(user, currentDoc.tenant, currentDoc.access)) { + throw OpenSearchStatusException( + "Permission denied for ObservabilityObject ${it.id}", + RestStatus.FORBIDDEN + ) + } + } + val configSearchResult = objectDocs.map { + ObservabilityObjectDoc( + it.id!!, + it.observabilityObjectDoc.updatedTime, + it.observabilityObjectDoc.createdTime, + it.observabilityObjectDoc.tenant, + it.observabilityObjectDoc.access, + it.observabilityObjectDoc.type, + it.observabilityObjectDoc.objectData + ) + } + return GetObservabilityObjectResponse( + ObservabilityObjectSearchResult(configSearchResult), + UserAccessManager.hasAllInfoAccess(user) + ) + } + + /** + * Get all ObservabilityObject matching the criteria + * @param request [GetObservabilityObjectRequest] object + * @param user the user info object + * @return [GetObservabilityObjectResponse] + */ + private fun getAll(request: GetObservabilityObjectRequest, user: User?): GetObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-getAll") + val searchResult = ObservabilityIndex.getAllObservabilityObjects( + UserAccessManager.getUserTenant(user), + UserAccessManager.getSearchAccessInfo(user), + request + ) + return GetObservabilityObjectResponse(searchResult, UserAccessManager.hasAllInfoAccess(user)) + } + + /** + * Delete ObservabilityObject + * @param request [DeleteObservabilityObjectRequest] object + * @param user the user info object + * @return [DeleteObservabilityObjectResponse] + */ + fun delete(request: DeleteObservabilityObjectRequest, user: User?): DeleteObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-delete ${request.objectIds}") + return if (request.objectIds.size == 1) { + delete(request.objectIds.first(), user) + } else { + delete(request.objectIds, user) + } + } + + /** + * Delete by object id + * + * @param objectId + * @param user + * @return [DeleteObservabilityObjectResponse] + */ + private fun delete(objectId: String, user: User?): DeleteObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-delete $objectId") + UserAccessManager.validateUser(user) + val observabilityObjectDocInfo = ObservabilityIndex.getObservabilityObject(objectId) + observabilityObjectDocInfo + ?: run { + throw OpenSearchStatusException( + "ObservabilityObject $objectId not found", + RestStatus.NOT_FOUND + ) + } + + val currentDoc = observabilityObjectDocInfo.observabilityObjectDoc + if (!UserAccessManager.doesUserHasAccess(user, currentDoc.tenant, currentDoc.access)) { + throw OpenSearchStatusException( + "Permission denied for ObservabilityObject $objectId", + RestStatus.FORBIDDEN + ) + } + if (!ObservabilityIndex.deleteObservabilityObject(objectId)) { + throw OpenSearchStatusException( + "ObservabilityObject $objectId delete failed", + RestStatus.REQUEST_TIMEOUT + ) + } + return DeleteObservabilityObjectResponse(mapOf(Pair(objectId, RestStatus.OK))) + } + + /** + * Delete ObservabilityObject + * @param objectIds ObservabilityObject object ids + * @param user the user info object + * @return [DeleteObservabilityObjectResponse] + */ + private fun delete(objectIds: Set, user: User?): DeleteObservabilityObjectResponse { + log.info("$LOG_PREFIX:ObservabilityObject-delete $objectIds") + UserAccessManager.validateUser(user) + val configDocs = ObservabilityIndex.getObservabilityObjects(objectIds) + if (configDocs.size != objectIds.size) { + val mutableSet = objectIds.toMutableSet() + configDocs.forEach { mutableSet.remove(it.id) } + throw OpenSearchStatusException( + "ObservabilityObject $mutableSet not found", + RestStatus.NOT_FOUND + ) + } + configDocs.forEach { + val currentDoc = it.observabilityObjectDoc + if (!UserAccessManager.doesUserHasAccess(user, currentDoc.tenant, currentDoc.access)) { + throw OpenSearchStatusException( + "Permission denied for ObservabilityObject ${it.id}", + RestStatus.FORBIDDEN + ) + } + } + val deleteStatus = ObservabilityIndex.deleteObservabilityObjects(objectIds) + return DeleteObservabilityObjectResponse(deleteStatus) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/PluginBaseAction.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/PluginBaseAction.kt new file mode 100644 index 000000000..25e302260 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/PluginBaseAction.kt @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.action + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.opensearch.OpenSearchSecurityException +import org.opensearch.OpenSearchStatusException +import org.opensearch.action.ActionListener +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionResponse +import org.opensearch.action.support.ActionFilters +import org.opensearch.action.support.HandledTransportAction +import org.opensearch.client.Client +import org.opensearch.common.io.stream.Writeable +import org.opensearch.commons.ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT +import org.opensearch.commons.authuser.User +import org.opensearch.index.IndexNotFoundException +import org.opensearch.index.engine.VersionConflictEngineException +import org.opensearch.indices.InvalidIndexNameException +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.logger +import org.opensearch.rest.RestStatus +import org.opensearch.tasks.Task +import org.opensearch.transport.TransportService +import java.io.IOException + +abstract class PluginBaseAction( + name: String, + transportService: TransportService, + val client: Client, + actionFilters: ActionFilters, + requestReader: Writeable.Reader +) : HandledTransportAction(name, transportService, actionFilters, requestReader) { + companion object { + private val log by logger(PluginBaseAction::class.java) + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + } + + /** + * {@inheritDoc} + */ + @Suppress("TooGenericExceptionCaught") + override fun doExecute( + task: Task?, + request: Request, + listener: ActionListener + ) { + val userStr: String? = client.threadPool().threadContext.getTransient(OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) + val user: User? = User.parse(userStr) + scope.launch { + try { + listener.onResponse(executeRequest(request, user)) + } catch (exception: OpenSearchStatusException) { + log.warn("$LOG_PREFIX:OpenSearchStatusException: message:${exception.message}") + listener.onFailure(exception) + } catch (exception: OpenSearchSecurityException) { + log.warn("$LOG_PREFIX:OpenSearchSecurityException:", exception) + listener.onFailure( + OpenSearchStatusException( + "Permissions denied: ${exception.message} - Contact administrator", + RestStatus.FORBIDDEN + ) + ) + } catch (exception: VersionConflictEngineException) { + log.warn("$LOG_PREFIX:VersionConflictEngineException:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.CONFLICT)) + } catch (exception: IndexNotFoundException) { + log.warn("$LOG_PREFIX:IndexNotFoundException:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.NOT_FOUND)) + } catch (exception: InvalidIndexNameException) { + log.warn("$LOG_PREFIX:InvalidIndexNameException:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.BAD_REQUEST)) + } catch (exception: IllegalArgumentException) { + log.warn("$LOG_PREFIX:IllegalArgumentException:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.BAD_REQUEST)) + } catch (exception: IllegalStateException) { + log.warn("$LOG_PREFIX:IllegalStateException:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.SERVICE_UNAVAILABLE)) + } catch (exception: IOException) { + log.error("$LOG_PREFIX:Uncaught IOException:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.FAILED_DEPENDENCY)) + } catch (exception: Exception) { + log.error("$LOG_PREFIX:Uncaught Exception:", exception) + listener.onFailure(OpenSearchStatusException(exception.message, RestStatus.INTERNAL_SERVER_ERROR)) + } + } + } + + /** + * Execute the transport request + * @param request the request to execute + * @return the response to return. + */ + abstract fun executeRequest(request: Request, user: User?): Response +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/UpdateObservabilityObjectAction.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/UpdateObservabilityObjectAction.kt new file mode 100644 index 000000000..315758fba --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/action/UpdateObservabilityObjectAction.kt @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.action + +import org.opensearch.action.ActionType +import org.opensearch.action.support.ActionFilters +import org.opensearch.client.Client +import org.opensearch.common.inject.Inject +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.commons.authuser.User +import org.opensearch.observability.model.UpdateObservabilityObjectRequest +import org.opensearch.observability.model.UpdateObservabilityObjectResponse +import org.opensearch.transport.TransportService + +/** + * Update ObservabilityObject transport action + */ +internal class UpdateObservabilityObjectAction @Inject constructor( + transportService: TransportService, + client: Client, + actionFilters: ActionFilters, + val xContentRegistry: NamedXContentRegistry +) : PluginBaseAction( + NAME, + transportService, + client, + actionFilters, + ::UpdateObservabilityObjectRequest +) { + companion object { + private const val NAME = "cluster:admin/opensearch/observability/update" + internal val ACTION_TYPE = ActionType(NAME, ::UpdateObservabilityObjectResponse) + } + + /** + * {@inheritDoc} + */ + override fun executeRequest(request: UpdateObservabilityObjectRequest, user: User?): UpdateObservabilityObjectResponse { + return ObservabilityActions.update(request, user) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/index/ObservabilityIndex.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/index/ObservabilityIndex.kt new file mode 100644 index 000000000..683781fe3 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/index/ObservabilityIndex.kt @@ -0,0 +1,369 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.index + +import org.opensearch.ResourceAlreadyExistsException +import org.opensearch.ResourceNotFoundException +import org.opensearch.action.DocWriteResponse +import org.opensearch.action.admin.indices.create.CreateIndexRequest +import org.opensearch.action.bulk.BulkRequest +import org.opensearch.action.delete.DeleteRequest +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse +import org.opensearch.action.get.MultiGetRequest +import org.opensearch.action.index.IndexRequest +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.update.UpdateRequest +import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.unit.TimeValue +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.common.xcontent.XContentType +import org.opensearch.index.query.QueryBuilders +import org.opensearch.index.reindex.ReindexAction +import org.opensearch.index.reindex.ReindexRequestBuilder +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.model.GetObservabilityObjectRequest +import org.opensearch.observability.model.ObservabilityObjectDoc +import org.opensearch.observability.model.ObservabilityObjectDocInfo +import org.opensearch.observability.model.ObservabilityObjectSearchResult +import org.opensearch.observability.model.RestTag.ACCESS_LIST_FIELD +import org.opensearch.observability.model.RestTag.TENANT_FIELD +import org.opensearch.observability.model.SearchResults +import org.opensearch.observability.settings.PluginSettings +import org.opensearch.observability.util.SecureIndexClient +import org.opensearch.observability.util.logger +import org.opensearch.rest.RestStatus +import org.opensearch.search.SearchHit +import org.opensearch.search.builder.SearchSourceBuilder +import java.util.concurrent.TimeUnit + +/** + * Class for doing OpenSearch index operation to maintain observability objects in cluster. + */ +@Suppress("TooManyFunctions") +internal object ObservabilityIndex { + private val log by logger(ObservabilityIndex::class.java) + private const val INDEX_NAME = ".opensearch-observability" + private const val NOTEBOOKS_INDEX_NAME = ".opensearch-notebooks" + private const val OBSERVABILITY_MAPPING_FILE_NAME = "observability-mapping.yml" + private const val OBSERVABILITY_SETTINGS_FILE_NAME = "observability-settings.yml" + private const val MAPPING_TYPE = "_doc" + + private lateinit var client: Client + private lateinit var clusterService: ClusterService + + private val searchHitParser = object : SearchResults.SearchHitParser { + override fun parse(searchHit: SearchHit): ObservabilityObjectDoc { + val parser = XContentType.JSON.xContent().createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + searchHit.sourceAsString + ) + parser.nextToken() + return ObservabilityObjectDoc.parse(parser, searchHit.id) + } + } + + /** + * Initialize the class + * @param client The OpenSearch client + * @param clusterService The OpenSearch cluster service + */ + fun initialize(client: Client, clusterService: ClusterService) { + this.client = SecureIndexClient(client) + this.clusterService = clusterService + } + + /** + * Create index using the mapping and settings defined in resource + */ + @Suppress("TooGenericExceptionCaught") + private fun createIndex() { + if (!isIndexExists(INDEX_NAME)) { + val classLoader = ObservabilityIndex::class.java.classLoader + val indexMappingSource = classLoader.getResource(OBSERVABILITY_MAPPING_FILE_NAME)?.readText()!! + val indexSettingsSource = classLoader.getResource(OBSERVABILITY_SETTINGS_FILE_NAME)?.readText()!! + val request = CreateIndexRequest(INDEX_NAME) + .mapping(MAPPING_TYPE, indexMappingSource, XContentType.YAML) + .settings(indexSettingsSource, XContentType.YAML) + try { + val actionFuture = client.admin().indices().create(request) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + if (response.isAcknowledged) { + log.info("$LOG_PREFIX:Index $INDEX_NAME creation Acknowledged") + reindexNotebooks() + } else { + throw IllegalStateException("$LOG_PREFIX:Index $INDEX_NAME creation not Acknowledged") + } + } catch (exception: Exception) { + if (exception !is ResourceAlreadyExistsException && exception.cause !is ResourceAlreadyExistsException) { + throw exception + } + } + } + } + + /** + * Reindex .opensearch-notebooks to .opensearch-observability index + */ + @Suppress("TooGenericExceptionCaught") + private fun reindexNotebooks() { + if (isIndexExists(NOTEBOOKS_INDEX_NAME)) { + try { + log.info("$LOG_PREFIX:Index - reindex $NOTEBOOKS_INDEX_NAME to $INDEX_NAME") + val reindexResponse = ReindexRequestBuilder(client, ReindexAction.INSTANCE) + .source(NOTEBOOKS_INDEX_NAME) + .destination(INDEX_NAME) + .abortOnVersionConflict(false) + .refresh(true) + .get() + if (reindexResponse.isTimedOut) { + throw IllegalStateException("$LOG_PREFIX:Index - reindex $NOTEBOOKS_INDEX_NAME timed out") + } else if (reindexResponse.searchFailures.isNotEmpty()) { + throw IllegalStateException("$LOG_PREFIX:Index - reindex $NOTEBOOKS_INDEX_NAME failed with searchFailures") + } else if (reindexResponse.bulkFailures.isNotEmpty()) { + throw IllegalStateException("$LOG_PREFIX:Index - reindex $NOTEBOOKS_INDEX_NAME failed with bulkFailures") + } else if (reindexResponse.total != reindexResponse.created + reindexResponse.updated) { + throw IllegalStateException( + "$LOG_PREFIX:Index - reindex number of docs created:${reindexResponse.created} + " + + "updated:${reindexResponse.updated} does not equal requested:${reindexResponse.total}" + ) + } + log.info( + "$LOG_PREFIX:Index - reindex ${reindexResponse.created} docs created " + + "and ${reindexResponse.updated} docs updated in $INDEX_NAME" + ) + } catch (exception: Exception) { + if (exception !is ResourceNotFoundException && exception.cause !is ResourceNotFoundException) { + throw exception + } + } + } + } + + /** + * Check if the index is created and available. + * @param index + * @return true if index is available, false otherwise + */ + private fun isIndexExists(index: String): Boolean { + val clusterState = clusterService.state() + return clusterState.routingTable.hasIndex(index) + } + + /** + * Create observability object + * + * @param observabilityObjectDoc + * @param id + * @return object id if successful, otherwise null + */ + fun createObservabilityObject(observabilityObjectDoc: ObservabilityObjectDoc, id: String? = null): String? { + createIndex() + val xContent = observabilityObjectDoc.toXContent() + val indexRequest = IndexRequest(INDEX_NAME) + .source(xContent) + .create(true) + if (id != null) { + indexRequest.id(id) + } + val actionFuture = client.index(indexRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + return if (response.result != DocWriteResponse.Result.CREATED) { + log.warn("$LOG_PREFIX:createObservabilityObject - response:$response") + null + } else { + response.id + } + } + + /** + * Get observability object + * + * @param id + * @return [ObservabilityObjectDocInfo] + */ + fun getObservabilityObject(id: String): ObservabilityObjectDocInfo? { + createIndex() + val getRequest = GetRequest(INDEX_NAME).id(id) + val actionFuture = client.get(getRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + return parseObservabilityObjectDoc(id, response) + } + + /** + * Get multiple observability objects + * + * @param ids + * @return list of [ObservabilityObjectDocInfo] + */ + fun getObservabilityObjects(ids: Set): List { + createIndex() + val getRequest = MultiGetRequest() + ids.forEach { getRequest.add(INDEX_NAME, it) } + val actionFuture = client.multiGet(getRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + return response.responses.mapNotNull { parseObservabilityObjectDoc(it.id, it.response) } + } + + /** + * Parse observability object doc + * + * @param id + * @param response + * @return [ObservabilityObjectDocInfo] + */ + private fun parseObservabilityObjectDoc(id: String, response: GetResponse): ObservabilityObjectDocInfo? { + return if (response.sourceAsString == null) { + log.warn("$LOG_PREFIX:getObservabilityObject - $id not found; response:$response") + null + } else { + val parser = XContentType.JSON.xContent().createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + response.sourceAsString + ) + parser.nextToken() + val doc = ObservabilityObjectDoc.parse(parser, id) + ObservabilityObjectDocInfo(id, response.version, response.seqNo, response.primaryTerm, doc) + } + } + + /** + * Get all observability objects + * + * @param tenant + * @param access + * @param request + * @return [ObservabilityObjectSearchResult] + */ + fun getAllObservabilityObjects( + tenant: String, + access: List, + request: GetObservabilityObjectRequest + ): ObservabilityObjectSearchResult { + createIndex() + val queryHelper = ObservabilityQueryHelper(request.types) + val sourceBuilder = SearchSourceBuilder() + .timeout(TimeValue(PluginSettings.operationTimeoutMs, TimeUnit.MILLISECONDS)) + .size(request.maxItems) + .from(request.fromIndex) + queryHelper.addSortField(sourceBuilder, request.sortField, request.sortOrder) + + val query = QueryBuilders.boolQuery() + query.filter(QueryBuilders.termsQuery(TENANT_FIELD, tenant)) + if (access.isNotEmpty()) { + query.filter(QueryBuilders.termsQuery(ACCESS_LIST_FIELD, access)) + } + queryHelper.addTypeFilters(query) + queryHelper.addQueryFilters(query, request.filterParams) + sourceBuilder.query(query) + val searchRequest = SearchRequest() + .indices(INDEX_NAME) + .source(sourceBuilder) + val actionFuture = client.search(searchRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + val result = ObservabilityObjectSearchResult(request.fromIndex.toLong(), response, searchHitParser) + log.info( + "$LOG_PREFIX:getAllObservabilityObjects types:${request.types} from:${request.fromIndex}, maxItems:${request.maxItems}," + + " sortField:${request.sortField}, sortOrder=${request.sortOrder}, filters=${request.filterParams}" + + " retCount:${result.objectList.size}, totalCount:${result.totalHits}" + ) + return result + } + + /** + * Update observability object + * + * @param id + * @param observabilityObjectDoc + * @return true if successful, otherwise false + */ + fun updateObservabilityObject(id: String, observabilityObjectDoc: ObservabilityObjectDoc): Boolean { + createIndex() + val updateRequest = UpdateRequest() + .index(INDEX_NAME) + .id(id) + .doc(observabilityObjectDoc.toXContent()) + .fetchSource(true) + val actionFuture = client.update(updateRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + if (response.result != DocWriteResponse.Result.UPDATED) { + log.warn("$LOG_PREFIX:updateObservabilityObject failed for $id; response:$response") + } + return response.result == DocWriteResponse.Result.UPDATED + } + + /** + * Delete observability object + * + * @param id + * @return true if successful, otherwise false + */ + fun deleteObservabilityObject(id: String): Boolean { + createIndex() + val deleteRequest = DeleteRequest() + .index(INDEX_NAME) + .id(id) + val actionFuture = client.delete(deleteRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + if (response.result != DocWriteResponse.Result.DELETED) { + log.warn("$LOG_PREFIX:deleteObservabilityObject failed for $id; response:$response") + } + return response.result == DocWriteResponse.Result.DELETED + } + + /** + * Delete multiple observability objects + * + * @param ids + * @return map of id to delete status + */ + fun deleteObservabilityObjects(ids: Set): Map { + createIndex() + val bulkRequest = BulkRequest() + ids.forEach { + val deleteRequest = DeleteRequest() + .index(INDEX_NAME) + .id(it) + bulkRequest.add(deleteRequest) + } + val actionFuture = client.bulk(bulkRequest) + val response = actionFuture.actionGet(PluginSettings.operationTimeoutMs) + val mutableMap = mutableMapOf() + response.forEach { + mutableMap[it.id] = it.status() + if (it.isFailed) { + log.warn("$LOG_PREFIX:deleteObservabilityObjects failed for ${it.id}; response:${it.failureMessage}") + } + } + return mutableMap + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/index/ObservabilityQueryHelper.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/index/ObservabilityQueryHelper.kt new file mode 100644 index 000000000..748572a95 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/index/ObservabilityQueryHelper.kt @@ -0,0 +1,137 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.index + +import org.opensearch.OpenSearchStatusException +import org.opensearch.index.query.BoolQueryBuilder +import org.opensearch.index.query.Operator +import org.opensearch.index.query.QueryBuilder +import org.opensearch.index.query.QueryBuilders +import org.opensearch.observability.model.ObservabilityObjectType +import org.opensearch.observability.model.RestTag.CREATED_TIME_FIELD +import org.opensearch.observability.model.RestTag.NAME_FIELD +import org.opensearch.observability.model.RestTag.QUERY_FIELD +import org.opensearch.observability.model.RestTag.UPDATED_TIME_FIELD +import org.opensearch.rest.RestStatus +import org.opensearch.search.builder.SearchSourceBuilder +import org.opensearch.search.sort.SortOrder +import java.util.EnumSet + +/** + * Helper class for Get operations. + */ +internal class ObservabilityQueryHelper(private val types: EnumSet) { + companion object { + private val METADATA_RANGE_FIELDS = setOf( + UPDATED_TIME_FIELD, + CREATED_TIME_FIELD + ) + + // keyword and text fields are under observability object and should be prepended with type + private val KEYWORD_FIELDS: Set = setOf() + private val TEXT_FIELDS = setOf( + NAME_FIELD + ) + + private val METADATA_FIELDS = METADATA_RANGE_FIELDS + private val OBSERVABILITY_OBJECT_FIELDS = KEYWORD_FIELDS.union(TEXT_FIELDS) + private val ALL_FIELDS = METADATA_FIELDS.union(OBSERVABILITY_OBJECT_FIELDS) + + val FILTER_PARAMS = ALL_FIELDS.union(setOf(QUERY_FIELD)) + } + + private val prefixes = if (types.size == 0) { + ObservabilityObjectType.getAll() + } else { + types + } + + fun addSortField(sourceBuilder: SearchSourceBuilder, sortField: String?, sortOrder: SortOrder?) { + val order = sortOrder ?: SortOrder.ASC + if (sortField == null) { + sourceBuilder.sort(UPDATED_TIME_FIELD, order) + } else { + val fields = mutableListOf() + when { + METADATA_RANGE_FIELDS.contains(sortField) -> fields.add(sortField) + KEYWORD_FIELDS.contains(sortField) -> fields.addAll(types.map { "${it.tag}.$sortField" }) + TEXT_FIELDS.contains(sortField) -> fields.addAll(types.map { "${it.tag}.$sortField.keyword" }) + else -> throw OpenSearchStatusException("Field $sortField not acceptable", RestStatus.NOT_ACCEPTABLE) + } + fields.forEach { sourceBuilder.sort(it, order) } + } + } + + fun addTypeFilters(query: BoolQueryBuilder) { + if (types.size > 0) { + types.forEach { + query.should().add(QueryBuilders.existsQuery(it.tag)) + } + query.minimumShouldMatch(1) + } + } + + fun addQueryFilters(query: BoolQueryBuilder, filterParams: Map) { + filterParams.forEach { + when { + QUERY_FIELD == it.key -> query.filter(getQueryAllBuilder(it.value)) // all text search + METADATA_RANGE_FIELDS.contains(it.key) -> query.filter(getRangeQueryBuilder(it.key, it.value)) + KEYWORD_FIELDS.contains(it.key) -> addTermsQueryBuilder(query, it.key, it.value) + TEXT_FIELDS.contains(it.key) -> addMatchQueryBuilder(query, it.key, it.value) + else -> throw OpenSearchStatusException("Query on ${it.key} not acceptable", RestStatus.NOT_ACCEPTABLE) + } + } + } + + private fun getQueryAllBuilder(queryValue: String): QueryBuilder { + val allQuery = QueryBuilders.queryStringQuery(queryValue) + // Searching on metadata field is not supported. skip adding METADATA_FIELDS + OBSERVABILITY_OBJECT_FIELDS.forEach { + prefixes.forEach { type -> allQuery.field("$type.$it") } + } + return allQuery + } + + private fun getRangeQueryBuilder(queryKey: String, queryValue: String): QueryBuilder { + val range = queryValue.split("..") + return when (range.size) { + 1 -> QueryBuilders.termQuery(queryKey, queryValue) + 2 -> { + val rangeQuery = QueryBuilders.rangeQuery(queryKey) + rangeQuery.from(range[0]) + rangeQuery.to(range[1]) + rangeQuery + } + else -> { + throw OpenSearchStatusException( + "Invalid Range format $queryValue, allowed format 'exact' or 'from..to'", + RestStatus.NOT_ACCEPTABLE + ) + } + } + } + + private fun addTermQueryBuilder(query: BoolQueryBuilder, queryKey: String, queryValue: String) { + prefixes.forEach { query.filter(QueryBuilders.termQuery("${it.tag}.$queryKey", queryValue)) } + } + + private fun addTermsQueryBuilder(query: BoolQueryBuilder, queryKey: String, queryValue: String) { + prefixes.forEach { query.filter(QueryBuilders.termsQuery("${it.tag}.$queryKey", queryValue.split(","))) } + } + + private fun addMatchQueryBuilder(query: BoolQueryBuilder, queryKey: String, queryValue: String) { + prefixes.forEach { + query.should().add(QueryBuilders.matchQuery("${it.tag}.$queryKey", queryValue).operator(Operator.AND)) + } + query.minimumShouldMatch(1) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseModel.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseModel.kt new file mode 100644 index 000000000..bc937685f --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseModel.kt @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.xcontent.ToXContent + +/** + * interface for representing objects. + */ +interface BaseModel : Writeable, ToXContent diff --git a/public/components/application_analytics/home.tsx b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseObjectData.kt similarity index 70% rename from public/components/application_analytics/home.tsx rename to opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseObjectData.kt index 8c9b5770d..8cffefab2 100644 --- a/public/components/application_analytics/home.tsx +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseObjectData.kt @@ -9,8 +9,9 @@ * GitHub history for details. */ -import React from 'react'; +package org.opensearch.observability.model -export const Home = () => { - return

Application analytics home

; -}; \ No newline at end of file +/** + * Marker interface for Channel Data + */ +interface BaseObjectData : BaseModel diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseResponse.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseResponse.kt new file mode 100644 index 000000000..6bbe09a4d --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/BaseResponse.kt @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.model + +import org.opensearch.action.ActionResponse +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.ToXContentObject +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentFactory +import org.opensearch.rest.RestStatus +import java.io.IOException + +/** + * Base response which give REST status. + */ +internal abstract class BaseResponse : ActionResponse, ToXContentObject { + + /** + * constructor for creating the class + */ + constructor() + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) + + /** + * {@inheritDoc} + */ + fun toXContent(params: ToXContent.Params = ToXContent.EMPTY_PARAMS): XContentBuilder { + return toXContent(XContentFactory.jsonBuilder(), params) + } + + /** + * get rest status for the response. Useful override for multi-status response. + * @return RestStatus for the response + */ + open fun getStatus(): RestStatus { + return RestStatus.OK + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/CreateObservabilityObjectRequest.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/CreateObservabilityObjectRequest.kt new file mode 100644 index 000000000..8ceaaea52 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/CreateObservabilityObjectRequest.kt @@ -0,0 +1,142 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +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.ToXContentObject +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.fieldIfNotNull +import org.opensearch.commons.utils.logger +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import java.io.IOException + +/** + * Action request for creating new configuration. + */ +internal class CreateObservabilityObjectRequest : ActionRequest, ToXContentObject { + val objectId: String? + val type: ObservabilityObjectType + val objectData: BaseObjectData? + + companion object { + private val log by logger(CreateObservabilityObjectRequest::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { CreateObservabilityObjectRequest(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + * @param id optional id to use if missed in XContent + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser, id: String? = null): CreateObservabilityObjectRequest { + var objectId: String? = id + var type: ObservabilityObjectType? = null + var baseObjectData: BaseObjectData? = null + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_FIELD -> objectId = parser.text() + else -> { + val objectTypeForTag = ObservabilityObjectType.fromTagOrDefault(fieldName) + if (objectTypeForTag != ObservabilityObjectType.NONE && baseObjectData == null) { + baseObjectData = + ObservabilityObjectDataProperties.createObjectData(objectTypeForTag, parser) + type = objectTypeForTag + } else { + parser.skipChildren() + log.info("Unexpected field: $fieldName, while parsing CreateObservabilityObjectRequest") + } + } + } + } + type ?: throw IllegalArgumentException("Object data field absent") + baseObjectData ?: throw IllegalArgumentException("Object data field absent") + return CreateObservabilityObjectRequest(objectId, type, baseObjectData) + } + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + return builder.startObject() + .fieldIfNotNull(OBJECT_ID_FIELD, objectId) + .field(type.tag, objectData) + .endObject() + } + + /** + * constructor for creating the class + * @param objectId optional id to use for ObservabilityObject + * @param type type of ObservabilityObject + * @param objectData the ObservabilityObject + */ + constructor(objectId: String? = null, type: ObservabilityObjectType, objectData: BaseObjectData) { + this.objectId = objectId + this.type = type + this.objectData = objectData + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + objectId = input.readOptionalString() + type = input.readEnum(ObservabilityObjectType::class.java) + objectData = input.readOptionalWriteable( + ObservabilityObjectDataProperties.getReaderForObjectType( + input.readEnum( + ObservabilityObjectType::class.java + ) + ) + ) + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + super.writeTo(output) + output.writeOptionalString(objectId) + output.writeEnum(type) + output.writeOptionalWriteable(objectData) + } + + /** + * {@inheritDoc} + */ + override fun validate(): ActionRequestValidationException? { + return null + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/CreateObservabilityObjectResponse.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/CreateObservabilityObjectResponse.kt new file mode 100644 index 000000000..528c6cf95 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/CreateObservabilityObjectResponse.kt @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.logger +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import java.io.IOException + +/** + * Action Response for creating new configuration. + */ +internal class CreateObservabilityObjectResponse : BaseResponse { + val objectId: String + + companion object { + private val log by logger(CreateObservabilityObjectResponse::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { CreateObservabilityObjectResponse(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser): CreateObservabilityObjectResponse { + var objectId: String? = null + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_FIELD -> objectId = parser.text() + else -> { + parser.skipChildren() + log.info("Unexpected field: $fieldName, while parsing CreateObservabilityObjectResponse") + } + } + } + objectId ?: throw IllegalArgumentException("$OBJECT_ID_FIELD field absent") + return CreateObservabilityObjectResponse(objectId) + } + } + + /** + * constructor for creating the class + * @param id the id of the created ObservabilityObject + */ + constructor(id: String) { + this.objectId = id + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + objectId = input.readString() + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + output.writeString(objectId) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + return builder.startObject() + .field(OBJECT_ID_FIELD, objectId) + .endObject() + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/DeleteObservabilityObjectRequest.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/DeleteObservabilityObjectRequest.kt new file mode 100644 index 000000000..8dfa6c2b3 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/DeleteObservabilityObjectRequest.kt @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.action.ValidateActions +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.ToXContentObject +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.logger +import org.opensearch.commons.utils.stringList +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_ID_LIST_FIELD +import java.io.IOException + +/** + * Action request for creating new configuration. + */ +internal class DeleteObservabilityObjectRequest : ActionRequest, ToXContentObject { + val objectIds: Set + + companion object { + private val log by logger(DeleteObservabilityObjectRequest::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { DeleteObservabilityObjectRequest(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + * @param id optional id to use if missed in XContent + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser): DeleteObservabilityObjectRequest { + var objectIds: Set? = null + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_LIST_FIELD -> objectIds = parser.stringList().toSet() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Skipping Unknown field $fieldName") + } + } + } + objectIds ?: throw IllegalArgumentException("$OBJECT_ID_FIELD field absent") + return DeleteObservabilityObjectRequest(objectIds) + } + } + + /** + * constructor for creating the class + * @param objectIds the id of the observability object + */ + constructor(objectIds: Set) { + this.objectIds = objectIds + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + objectIds = input.readStringList().toSet() + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + super.writeTo(output) + output.writeStringCollection(objectIds) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + return builder.startObject() + .field(OBJECT_ID_LIST_FIELD, objectIds) + .endObject() + } + + /** + * {@inheritDoc} + */ + override fun validate(): ActionRequestValidationException? { + var validationException: ActionRequestValidationException? = null + if (objectIds.isNullOrEmpty()) { + validationException = ValidateActions.addValidationError("objectIds is null or empty", validationException) + } + return validationException + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/DeleteObservabilityObjectResponse.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/DeleteObservabilityObjectResponse.kt new file mode 100644 index 000000000..f5f699fb5 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/DeleteObservabilityObjectResponse.kt @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.STRING_READER +import org.opensearch.commons.utils.STRING_WRITER +import org.opensearch.commons.utils.enumReader +import org.opensearch.commons.utils.enumWriter +import org.opensearch.commons.utils.logger +import org.opensearch.observability.model.RestTag.DELETE_RESPONSE_LIST_TAG +import org.opensearch.rest.RestStatus +import java.io.IOException + +/** + * Action Response for creating new configuration. + */ +internal class DeleteObservabilityObjectResponse : BaseResponse { + val objectIdToStatus: Map + + companion object { + private val log by logger(DeleteObservabilityObjectResponse::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { DeleteObservabilityObjectResponse(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser): DeleteObservabilityObjectResponse { + var objectIdToStatus: Map? = null + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + DELETE_RESPONSE_LIST_TAG -> objectIdToStatus = convertMapStrings(parser.mapStrings()) + else -> { + parser.skipChildren() + log.info("Unexpected field: $fieldName, while parsing DeleteObservabilityObjectResponse") + } + } + } + objectIdToStatus ?: throw IllegalArgumentException("$DELETE_RESPONSE_LIST_TAG field absent") + return DeleteObservabilityObjectResponse(objectIdToStatus) + } + + private fun convertMapStrings(inputMap: Map): Map { + return inputMap.mapValues { RestStatus.valueOf(it.value) } + } + } + + /** + * constructor for creating the class + * @param objectIdToStatus the ids of the deleted observability object with status + */ + constructor(objectIdToStatus: Map) { + this.objectIdToStatus = objectIdToStatus + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + objectIdToStatus = input.readMap(STRING_READER, enumReader(RestStatus::class.java)) + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + output.writeMap(objectIdToStatus, STRING_WRITER, enumWriter(RestStatus::class.java)) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + return builder.startObject() + .field(DELETE_RESPONSE_LIST_TAG, objectIdToStatus) + .endObject() + } + + override fun getStatus(): RestStatus { + val distinctStatus = objectIdToStatus.values.distinct() + return when { + distinctStatus.size > 1 -> RestStatus.MULTI_STATUS + distinctStatus.size == 1 -> distinctStatus[0] + else -> RestStatus.NOT_MODIFIED + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/GetObservabilityObjectRequest.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/GetObservabilityObjectRequest.kt new file mode 100644 index 000000000..d026a94d5 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/GetObservabilityObjectRequest.kt @@ -0,0 +1,198 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.action.ValidateActions +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.ToXContentObject +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.STRING_READER +import org.opensearch.commons.utils.STRING_WRITER +import org.opensearch.commons.utils.enumReader +import org.opensearch.commons.utils.enumSet +import org.opensearch.commons.utils.fieldIfNotNull +import org.opensearch.commons.utils.logger +import org.opensearch.commons.utils.stringList +import org.opensearch.observability.model.RestTag.FILTER_PARAM_LIST_FIELD +import org.opensearch.observability.model.RestTag.FROM_INDEX_FIELD +import org.opensearch.observability.model.RestTag.MAX_ITEMS_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_ID_LIST_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_TYPE_FIELD +import org.opensearch.observability.model.RestTag.SORT_FIELD_FIELD +import org.opensearch.observability.model.RestTag.SORT_ORDER_FIELD +import org.opensearch.observability.settings.PluginSettings +import org.opensearch.search.sort.SortOrder +import java.io.IOException +import java.util.EnumSet + +/** + * Action Request for getting ObservabilityObject. + */ +class GetObservabilityObjectRequest : ActionRequest, ToXContentObject { + val objectIds: Set + val types: EnumSet + val fromIndex: Int + val maxItems: Int + val sortField: String? + val sortOrder: SortOrder? + val filterParams: Map + + companion object { + private val log by logger(GetObservabilityObjectRequest::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { GetObservabilityObjectRequest(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser): GetObservabilityObjectRequest { + var objectIdList: Set = setOf() + var types: EnumSet = EnumSet.noneOf(ObservabilityObjectType::class.java) + var fromIndex = 0 + var maxItems = PluginSettings.defaultItemsQueryCount + var sortField: String? = null + var sortOrder: SortOrder? = null + var filterParams: Map = mapOf() + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_LIST_FIELD -> objectIdList = parser.stringList().toSet() + OBJECT_TYPE_FIELD -> types = parser.enumSet(ObservabilityObjectType.enumParser) + FROM_INDEX_FIELD -> fromIndex = parser.intValue() + MAX_ITEMS_FIELD -> maxItems = parser.intValue() + SORT_FIELD_FIELD -> sortField = parser.text() + SORT_ORDER_FIELD -> sortOrder = SortOrder.fromString(parser.text()) + FILTER_PARAM_LIST_FIELD -> filterParams = parser.mapStrings() + else -> { + parser.skipChildren() + log.info("Unexpected field: $fieldName, while parsing GetObservabilityObjectRequest") + } + } + } + return GetObservabilityObjectRequest( + objectIdList, + types, + fromIndex, + maxItems, + sortField, + sortOrder, + filterParams + ) + } + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + return builder!!.startObject() + .field(OBJECT_ID_LIST_FIELD, objectIds) + .field(OBJECT_TYPE_FIELD, types) + .field(FROM_INDEX_FIELD, fromIndex) + .field(MAX_ITEMS_FIELD, maxItems) + .fieldIfNotNull(SORT_FIELD_FIELD, sortField) + .fieldIfNotNull(SORT_ORDER_FIELD, sortOrder) + .field(FILTER_PARAM_LIST_FIELD, filterParams) + .endObject() + } + + /** + * constructor for creating the class + * @param objectIds the ids of the observability objects (other parameters are not relevant if ids are present) + * @param fromIndex the starting index for paginated response + * @param maxItems the maximum number of items to return for paginated response + * @param sortField the sort field if response has many items + * @param sortOrder the sort order if response has many items + * @param filterParams the filter parameters + */ + @Suppress("LongParameterList") + constructor( + objectIds: Set = setOf(), + types: EnumSet = EnumSet.noneOf(ObservabilityObjectType::class.java), + fromIndex: Int = 0, + maxItems: Int = PluginSettings.defaultItemsQueryCount, + sortField: String? = null, + sortOrder: SortOrder? = null, + filterParams: Map = mapOf() + ) { + this.objectIds = objectIds + this.types = types + this.fromIndex = fromIndex + this.maxItems = maxItems + this.sortField = sortField + this.sortOrder = sortOrder + this.filterParams = filterParams + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + objectIds = input.readStringList().toSet() + types = input.readEnumSet(ObservabilityObjectType::class.java) + fromIndex = input.readInt() + maxItems = input.readInt() + sortField = input.readOptionalString() + sortOrder = input.readOptionalWriteable(enumReader(SortOrder::class.java)) + filterParams = input.readMap(STRING_READER, STRING_READER) + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + super.writeTo(output) + output.writeStringCollection(objectIds) + output.writeEnumSet(types) + output.writeInt(fromIndex) + output.writeInt(maxItems) + output.writeOptionalString(sortField) + output.writeOptionalWriteable(sortOrder) + output.writeMap(filterParams, STRING_WRITER, STRING_WRITER) + } + + /** + * {@inheritDoc} + */ + override fun validate(): ActionRequestValidationException? { + var validationException: ActionRequestValidationException? = null + if (fromIndex < 0) { + validationException = ValidateActions.addValidationError("fromIndex is -ve", validationException) + } + if (maxItems <= 0) { + validationException = ValidateActions.addValidationError("maxItems is not +ve", validationException) + } + return validationException + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/GetObservabilityObjectResponse.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/GetObservabilityObjectResponse.kt new file mode 100644 index 000000000..0ed1247e3 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/GetObservabilityObjectResponse.kt @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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.XContentParser +import java.io.IOException + +/** + * Action Response for getting ObservabilityObject. + */ +internal class GetObservabilityObjectResponse : BaseResponse { + val searchResult: ObservabilityObjectSearchResult + private val filterSensitiveInfo: Boolean + + companion object { + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { GetObservabilityObjectResponse(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser): GetObservabilityObjectResponse { + return GetObservabilityObjectResponse(ObservabilityObjectSearchResult(parser), false) + } + } + + /** + * constructor for creating the class + * @param searchResult the ObservabilityObject list + */ + constructor(searchResult: ObservabilityObjectSearchResult, filterSensitiveInfo: Boolean) { + this.searchResult = searchResult + this.filterSensitiveInfo = filterSensitiveInfo + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + searchResult = ObservabilityObjectSearchResult(input) + filterSensitiveInfo = input.readBoolean() + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + searchResult.writeTo(output) + output.writeBoolean(filterSensitiveInfo) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + val xContentParams = if (filterSensitiveInfo) { + RestTag.FILTERED_REST_OUTPUT_PARAMS + } else { + RestTag.REST_OUTPUT_PARAMS + } + return searchResult.toXContent(builder, xContentParams) + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Notebook.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Notebook.kt new file mode 100644 index 000000000..0418850fc --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Notebook.kt @@ -0,0 +1,464 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +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.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Notebook main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "dateCreated" : "2020-12-11T20:51:15.509Z",
+ *   "name" : "test",
+ *   "dateModified" : "2020-12-11T21:04:55.336Z",
+ *   "backend" : "Default",
+ *   "paragraphs" : [
+ *     {
+ *       "output" : [
+ *         {
+ *           "result" : "# This is a markdown paragraph",
+ *           "outputType" : "MARKDOWN",
+ *           "execution_time" : "0s"
+ *         }
+ *       ],
+ *       "input" : {
+ *         "inputText" : "# This is a markdown paragraph",
+ *         "inputType" : "MARKDOWN"
+ *       },
+ *       "dateCreated" : "2020-12-11T21:04:39.997Z",
+ *       "dateModified" : "2020-12-11T21:04:48.207Z",
+ *       "id" : "paragraph_61e96a10-af19-4c7d-ae4e-d2e449c65dff"
+ *     }
+ *   ]
+ * }
+ * }
+ */ + +internal data class Notebook( + val name: String?, + val dateCreated: String?, + val dateModified: String?, + val backend: String?, + val paragraphs: List? +) : BaseObjectData { + + internal companion object { + private val log by logger(Notebook::class.java) + private const val NAME_TAG = "name" + private const val DATE_CREATED_TAG = "dateCreated" + private const val DATE_MODIFIED_TAG = "dateModified" + private const val BACKEND_TAG = "backend" + private const val PARAGRAPHS_TAG = "paragraphs" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Notebook(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(Paragraph.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create Notebook object + * @param parser data referenced at parser + * @return created Notebook object + */ + fun parse(parser: XContentParser): Notebook { + var name: String? = null + var dateCreated: String? = null + var dateModified: String? = null + var backend: String? = null + var paragraphs: List? = 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) { + NAME_TAG -> name = parser.text() + DATE_CREATED_TAG -> dateCreated = parser.text() + DATE_MODIFIED_TAG -> dateModified = parser.text() + BACKEND_TAG -> backend = parser.text() + PARAGRAPHS_TAG -> paragraphs = parseItemList(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Notebook Skipping Unknown field $fieldName") + } + } + } + return Notebook(name, dateCreated, dateModified, backend, paragraphs) + } + } + + /** + * 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(), + dateCreated = input.readString(), + dateModified = input.readString(), + backend = input.readString(), + paragraphs = input.readList(Paragraph.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(dateCreated) + output.writeString(dateModified) + output.writeString(backend) + output.writeCollection(paragraphs) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + val xContentParams = params ?: RestTag.REST_OUTPUT_PARAMS + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(DATE_CREATED_TAG, dateCreated) + .fieldIfNotNull(DATE_MODIFIED_TAG, dateModified) + .fieldIfNotNull(BACKEND_TAG, backend) + if (paragraphs != null) { + builder.startArray(PARAGRAPHS_TAG) + paragraphs.forEach { it.toXContent(builder, xContentParams) } + builder.endArray() + } + return builder.endObject() + } + + /** + * Notebook source data class + */ + internal data class Paragraph( + val output: List, + val input: Input?, + val dateCreated: String, + val dateModified: String, + val id: String + ) : BaseModel { + internal companion object { + private const val OUTPUT_TAG = "output" + private const val INPUT_TAG = "input" + private const val DATE_CREATED_TAG = "dateCreated" + private const val DATE_MODIFIED_TAG = "dateModified" + private const val ID_TAG = "id" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Paragraph(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(Output.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create Source object + * @param parser data referenced at parser + * @return created Source object + */ + fun parse(parser: XContentParser): Paragraph { + var output: List? = null + var input: Input? = null + var dateCreated: String? = null + var dateModified: String? = null + var id: 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) { + OUTPUT_TAG -> output = parseItemList(parser) + INPUT_TAG -> input = Input.parse(parser) + DATE_CREATED_TAG -> dateCreated = parser.text() + DATE_MODIFIED_TAG -> dateModified = parser.text() + ID_TAG -> id = parser.text() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Source Skipping Unknown field $fieldName") + } + } + } + output ?: throw IllegalArgumentException("$OUTPUT_TAG field absent") + input ?: throw IllegalArgumentException("$INPUT_TAG field absent") + dateCreated ?: throw IllegalArgumentException("$DATE_CREATED_TAG field absent") + dateModified ?: throw IllegalArgumentException("$DATE_MODIFIED_TAG field absent") + id ?: throw IllegalArgumentException("$ID_TAG field absent") + return Paragraph(output, input, dateCreated, dateModified, id) + } + } + + constructor(streamInput: StreamInput) : this( + output = streamInput.readList(Output.reader), + input = streamInput.readOptionalWriteable(Input.reader), + dateCreated = streamInput.readString(), + dateModified = streamInput.readString(), + id = streamInput.readString() + ) + + override fun writeTo(streamOutput: StreamOutput) { + streamOutput.writeCollection(output) + streamOutput.writeOptionalWriteable(input) + streamOutput.writeString(dateCreated) + streamOutput.writeString(dateModified) + streamOutput.writeString(id) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + val xContentParams = params ?: RestTag.REST_OUTPUT_PARAMS + builder!! + builder.startObject() + .startArray(OUTPUT_TAG) + output.forEach { it.toXContent(builder, xContentParams) } + builder.endArray() + .field(INPUT_TAG, input) + .field(DATE_CREATED_TAG, dateCreated) + .field(DATE_MODIFIED_TAG, dateModified) + .field(ID_TAG, id) + return builder.endObject() + } + } + + /** + * Notebook output data class + */ + internal data class Output( + val result: String?, + val outputType: String?, + val executionTime: String? + ) : BaseModel { + internal companion object { + private const val RESULT_TAG = "result" + private const val OUTPUT_TYPE_TAG = "outputType" + private const val EXECUTION_TIME_TAG = "execution_time" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Output(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Format object + * @param parser data referenced at parser + * @return created Format object + */ + fun parse(parser: XContentParser): Output { + var result: String? = null + var outputType: String? = null + var executionTime: 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) { + RESULT_TAG -> result = parser.text() + OUTPUT_TYPE_TAG -> outputType = parser.text() + EXECUTION_TIME_TAG -> executionTime = parser.text() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Format Skipping Unknown field $fieldName") + } + } + } + result ?: throw IllegalArgumentException("$RESULT_TAG field absent") + outputType ?: throw IllegalArgumentException("$OUTPUT_TYPE_TAG field absent") + executionTime ?: throw IllegalArgumentException("$EXECUTION_TIME_TAG field absent") + return Output(result, outputType, executionTime) + } + } + + constructor(input: StreamInput) : this( + result = input.readString(), + outputType = input.readString(), + executionTime = input.readString() + ) + + override fun writeTo(output: StreamOutput) { + output.writeString(result) + output.writeString(outputType) + output.writeString(executionTime) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(RESULT_TAG, result) + .field(OUTPUT_TYPE_TAG, outputType) + .field(EXECUTION_TIME_TAG, executionTime) + builder.endObject() + return builder + } + } + + /** + * Notebook input data class + */ + internal data class Input( + val inputText: String?, + val inputType: String? + ) : BaseModel { + internal companion object { + private const val INPUT_TEXT_TAG = "inputText" + private const val INPUT_TYPE_TAG = "inputType" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Input(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): Input { + var inputText: String? = null + var inputType: 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) { + INPUT_TEXT_TAG -> inputText = parser.text() + INPUT_TYPE_TAG -> inputType = parser.text() + else -> log.info("$LOG_PREFIX: Trigger Skipping Unknown field $fieldName") + } + } + inputText ?: throw IllegalArgumentException("$INPUT_TEXT_TAG field absent") + inputType ?: throw IllegalArgumentException("$INPUT_TYPE_TAG field absent") + return Input(inputText, inputType) + } + } + + constructor(input: StreamInput) : this( + inputText = input.readString(), + inputType = input.readString() + ) + + override fun writeTo(output: StreamOutput) { + output.writeString(inputText) + output.writeString(inputType) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(INPUT_TEXT_TAG, inputText) + .field(INPUT_TYPE_TAG, inputType) + 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 new file mode 100644 index 000000000..c64f7fce5 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDataProperties.kt @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.xcontent.XContentParser + +internal object ObservabilityObjectDataProperties { + /** + * Properties for ConfigTypes. + * This data class is used to provide contract across configTypes without reading into config data classes. + */ + private data class ObjectProperty( + val objectDataReader: Writeable.Reader?, + val objectDataParser: XParser + ) + + private val OBJECT_PROPERTIES_MAP = mapOf( + Pair(ObservabilityObjectType.NOTEBOOK, ObjectProperty(Notebook.reader, Notebook.xParser)), + Pair(ObservabilityObjectType.SAVED_QUERY, ObjectProperty(SavedQuery.reader, SavedQuery.xParser)), + Pair( + ObservabilityObjectType.SAVED_VISUALIZATION, + ObjectProperty(SavedVisualization.reader, SavedVisualization.xParser) + ), + Pair( + ObservabilityObjectType.OPERATIONAL_PANEL, + ObjectProperty(OperationalPanel.reader, OperationalPanel.xParser) + ), + Pair( + ObservabilityObjectType.TIMESTAMP, + ObjectProperty(Timestamp.reader, Timestamp.xParser) + ) + ) + + /** + * Get Reader for provided config type + * @param @ConfigType + * @return Reader + */ + fun getReaderForObjectType(objectType: ObservabilityObjectType): Writeable.Reader { + return OBJECT_PROPERTIES_MAP[objectType]?.objectDataReader + ?: throw IllegalArgumentException("Transport action used with unknown ConfigType:$objectType") + } + + /** + * Validate config data is of ConfigType + */ + fun validateObjectData(objectType: ObservabilityObjectType, objectData: BaseObjectData?): Boolean { + return when (objectType) { + ObservabilityObjectType.NOTEBOOK -> objectData is Notebook + ObservabilityObjectType.SAVED_QUERY -> objectData is SavedQuery + ObservabilityObjectType.SAVED_VISUALIZATION -> objectData is SavedVisualization + ObservabilityObjectType.OPERATIONAL_PANEL -> objectData is OperationalPanel + ObservabilityObjectType.TIMESTAMP -> objectData is Timestamp + ObservabilityObjectType.NONE -> true + } + } + + /** + * Creates config data from parser for given configType + * @param objectType the ConfigType + * @param parser parser for ConfigType + * @return created BaseObjectData on success. null if configType is not recognized + * + */ + fun createObjectData(objectType: ObservabilityObjectType, parser: XContentParser): BaseObjectData? { + return OBJECT_PROPERTIES_MAP[objectType]?.objectDataParser?.parse(parser) + } +} 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 new file mode 100644 index 000000000..db44d6d89 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDoc.kt @@ -0,0 +1,153 @@ +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.logger +import org.opensearch.commons.utils.stringList +import org.opensearch.observability.model.ObservabilityObjectDataProperties.getReaderForObjectType +import org.opensearch.observability.model.RestTag.ACCESS_LIST_FIELD +import org.opensearch.observability.model.RestTag.CREATED_TIME_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import org.opensearch.observability.model.RestTag.TENANT_FIELD +import org.opensearch.observability.model.RestTag.UPDATED_TIME_FIELD +import org.opensearch.observability.security.UserAccessManager +import java.io.IOException +import java.time.Instant + +/** + * Data class representing ObservabilityObject. + */ +data class ObservabilityObjectDoc( + val objectId: String, + val updatedTime: Instant, + val createdTime: Instant, + val tenant: String, + val access: List, // "User:user", "Role:sample_role", "BERole:sample_backend_role" + val type: ObservabilityObjectType, + val objectData: BaseObjectData? +) : BaseModel { + + companion object { + private val log by logger(ObservabilityObjectDoc::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { ObservabilityObjectDoc(it) } + + /** + * Parse the data from parser and create object + * @param parser data referenced at parser + * @return created object + */ + @JvmStatic + @Throws(IOException::class) + @Suppress("ComplexMethod") + fun parse(parser: XContentParser, useId: String? = null): ObservabilityObjectDoc { + var objectId: String? = useId + var updatedTime: Instant? = null + var createdTime: Instant? = null + var tenant: String? = null + var access: List = listOf() + var type: ObservabilityObjectType? = null + var objectData: BaseObjectData? = null + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_FIELD -> objectId = parser.text() + UPDATED_TIME_FIELD -> updatedTime = Instant.ofEpochMilli(parser.longValue()) + CREATED_TIME_FIELD -> createdTime = Instant.ofEpochMilli(parser.longValue()) + TENANT_FIELD -> tenant = parser.text() + ACCESS_LIST_FIELD -> access = parser.stringList() + else -> { + val objectTypeForTag = ObservabilityObjectType.fromTagOrDefault(fieldName) + if (objectTypeForTag != ObservabilityObjectType.NONE && objectData == null) { + objectData = + ObservabilityObjectDataProperties.createObjectData(objectTypeForTag, parser) + type = objectTypeForTag + } else { + parser.skipChildren() + log.info("Unexpected field: $fieldName, while parsing ObservabilityObjectDoc") + } + } + } + } + objectId ?: throw IllegalArgumentException("$OBJECT_ID_FIELD field absent") + updatedTime ?: throw IllegalArgumentException("$UPDATED_TIME_FIELD field absent") + createdTime ?: throw IllegalArgumentException("$CREATED_TIME_FIELD field absent") + tenant = tenant ?: UserAccessManager.DEFAULT_TENANT + type ?: throw IllegalArgumentException("Object data field absent") + objectData ?: throw IllegalArgumentException("Object data field absent") + return ObservabilityObjectDoc(objectId, updatedTime, createdTime, tenant, access, type, objectData) + } + } + + /** + * 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( + objectId = input.readString(), + updatedTime = input.readInstant(), + createdTime = input.readInstant(), + tenant = input.readString(), + access = input.readStringList(), + type = input.readEnum(ObservabilityObjectType::class.java), + objectData = input.readOptionalWriteable(getReaderForObjectType(input.readEnum(ObservabilityObjectType::class.java))) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(objectId) + output.writeInstant(updatedTime) + output.writeInstant(createdTime) + output.writeString(tenant) + output.writeStringCollection(access) + output.writeEnum(type) + output.writeOptionalWriteable(objectData) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + if (params?.paramAsBoolean(OBJECT_ID_FIELD, false) == true) { + builder.field(OBJECT_ID_FIELD, objectId) + } + builder.field(UPDATED_TIME_FIELD, updatedTime.toEpochMilli()) + .field(CREATED_TIME_FIELD, createdTime.toEpochMilli()) + .field(TENANT_FIELD, tenant) + if (params?.paramAsBoolean(ACCESS_LIST_FIELD, true) == true && access.isNotEmpty()) { + builder.field(ACCESS_LIST_FIELD, access) + } + builder.field(type.tag, objectData) + .endObject() + return builder + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDocInfo.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDocInfo.kt new file mode 100644 index 000000000..f6dc577a6 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectDocInfo.kt @@ -0,0 +1,14 @@ +package org.opensearch.observability.model + +import org.opensearch.index.seqno.SequenceNumbers + +/** + * Class for storing the observability object document with document properties. + */ +data class ObservabilityObjectDocInfo( + val id: String? = null, + val version: Long = -1L, + val seqNo: Long = SequenceNumbers.UNASSIGNED_SEQ_NO, + val primaryTerm: Long = SequenceNumbers.UNASSIGNED_PRIMARY_TERM, + val observabilityObjectDoc: ObservabilityObjectDoc +) diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectSearchResult.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectSearchResult.kt new file mode 100644 index 000000000..3a4cc085a --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectSearchResult.kt @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.apache.lucene.search.TotalHits +import org.opensearch.action.search.SearchResponse +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.observability.model.RestTag.OBJECT_LIST_FIELD + +/** + * ObservabilityObject search results + */ +internal class ObservabilityObjectSearchResult : SearchResults { + + /** + * single item result constructor + */ + constructor(objectItem: ObservabilityObjectDoc) : super(OBJECT_LIST_FIELD, objectItem) + + /** + * multiple items result constructor + */ + constructor(objectList: List) : this( + 0, + objectList.size.toLong(), + TotalHits.Relation.EQUAL_TO, + objectList + ) + + /** + * all param constructor + */ + constructor( + startIndex: Long, + totalHits: Long, + totalHitRelation: TotalHits.Relation, + objectList: List + ) : super(startIndex, totalHits, totalHitRelation, OBJECT_LIST_FIELD, objectList) + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : super(input, ObservabilityObjectDoc.reader) + + /** + * Construct object from XContentParser + */ + constructor(parser: XContentParser) : super(parser, OBJECT_LIST_FIELD) + + /** + * Construct object from SearchResponse + */ + constructor(from: Long, response: SearchResponse, searchHitParser: SearchHitParser) : super( + from, + response, + searchHitParser, + OBJECT_LIST_FIELD + ) + + /** + * {@inheritDoc} + */ + override fun parseItem(parser: XContentParser, useId: String?): ObservabilityObjectDoc { + return ObservabilityObjectDoc.parse(parser, useId) + } +} 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 new file mode 100644 index 000000000..9305013ab --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/ObservabilityObjectType.kt @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.commons.utils.EnumParser +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 +import org.opensearch.observability.model.RestTag.SAVED_VISUALIZATION_FIELD +import org.opensearch.observability.model.RestTag.TIMESTAMP_FIELD +import java.util.EnumSet + +/** + * Enum for ObservabilityObject type + */ +enum class ObservabilityObjectType(val tag: String) { + NONE("none") { + override fun toString(): String { + return tag + } + }, + NOTEBOOK(NOTEBOOK_FIELD) { + override fun toString(): String { + return tag + } + }, + SAVED_QUERY(SAVED_QUERY_FIELD) { + override fun toString(): String { + return tag + } + }, + SAVED_VISUALIZATION(SAVED_VISUALIZATION_FIELD) { + override fun toString(): String { + return tag + } + }, + OPERATIONAL_PANEL(OPERATIONAL_PANEL_FIELD) { + override fun toString(): String { + return tag + } + }, + TIMESTAMP(TIMESTAMP_FIELD) { + override fun toString(): String { + return tag + } + }; + + companion object { + private val tagMap = values().associateBy { it.tag } + + val enumParser = EnumParser { fromTagOrDefault(it) } + + /** + * Get ConfigType from tag or NONE if not found + * @param tag the tag + * @return ConfigType corresponding to tag. NONE if invalid tag. + */ + fun fromTagOrDefault(tag: String): ObservabilityObjectType { + return tagMap[tag] ?: NONE + } + + fun getAll(): EnumSet { + val allTypes = EnumSet.allOf(ObservabilityObjectType::class.java) + allTypes.remove(NONE) + return allTypes + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/OperationalPanel.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/OperationalPanel.kt new file mode 100644 index 000000000..60dab305c --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/OperationalPanel.kt @@ -0,0 +1,448 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +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.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * OperationalPanel main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "operationalPanel": {
+ *     "name": "Demo Panel 1",
+ *     "visualizations": [
+ *       {
+ *         "id": "panelViz_7ba28e34-6fd8-489d-9b9f-1f83e006fb17",
+ *         "savedVisualizationId": "oyuecXwBYVazWqOOde0o",
+ *         "x": 0,
+ *         "y": 0,
+ *         "w": 10,
+ *         "h": 10
+ *       },
+ *       {
+ *         "id": "panelViz_7ba28e34-6fd8-489d-9b9f-165fdv6wd611",
+ *         "savedVisualizationId": "oiuccXwBYVazWqOO1e06",
+ *         "x": 20,
+ *         "y": 20,
+ *         "w": 30,
+ *         "h": 20
+ *       }
+ *     ],
+ *     "timeRange": {
+ *       "to": "now",
+ *       "from": "now-1d"
+ *     },
+ *     "queryFilter": {
+ *       "query": "| where Carrier='OpenSearch-Air'",
+ *       "language": "ppl"
+ *     }
+ *   }
+ * }
+ * }
+ */ + +internal data class OperationalPanel( + val name: String?, + val visualizations: List?, + val timeRange: TimeRange?, + val queryFilter: QueryFilter?, +) : BaseObjectData { + + internal companion object { + private val log by logger(OperationalPanel::class.java) + private const val NAME_TAG = "name" + private const val VISUALIZATIONS_TAG = "visualizations" + private const val TIME_RANGE_TAG = "timeRange" + private const val QUERY_FILTER_TAG = "queryFilter" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { OperationalPanel(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(Visualization.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create OperationalPanel object + * @param parser data referenced at parser + * @return created OperationalPanel object + */ + fun parse(parser: XContentParser): OperationalPanel { + var name: String? = null + var visualizations: List? = null + var timeRange: TimeRange? = null + var queryFilter: QueryFilter? = 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) { + NAME_TAG -> name = parser.text() + VISUALIZATIONS_TAG -> visualizations = parseItemList(parser) + TIME_RANGE_TAG -> timeRange = TimeRange.parse(parser) + QUERY_FILTER_TAG -> queryFilter = QueryFilter.parse(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:OperationalPanel Skipping Unknown field $fieldName") + } + } + } + return OperationalPanel(name, visualizations, timeRange, queryFilter) + } + } + + /** + * 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(), + visualizations = input.readList(Visualization.reader), + timeRange = input.readOptionalWriteable(TimeRange.reader), + queryFilter = input.readOptionalWriteable(QueryFilter.reader), + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeCollection(visualizations) + output.writeOptionalWriteable(timeRange) + output.writeOptionalWriteable(queryFilter) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + val xContentParams = params ?: RestTag.REST_OUTPUT_PARAMS + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + if (visualizations != null) { + builder.startArray(VISUALIZATIONS_TAG) + visualizations.forEach { it.toXContent(builder, xContentParams) } + builder.endArray() + } + builder.fieldIfNotNull(TIME_RANGE_TAG, timeRange) + .fieldIfNotNull(QUERY_FILTER_TAG, queryFilter) + return builder.endObject() + } + + /** + * OperationalPanel visualization data class + */ + internal data class Visualization( + val id: String, + val savedVisualizationId: String, + val x: Int, + val y: Int, + val w: Int, + val h: Int + ) : BaseModel { + internal companion object { + private const val ID_TAG = "id" + private const val SAVED_VISUALIZATION_ID_TAG = "savedVisualizationId" + private const val X_TAG = "x" + private const val Y_TAG = "y" + private const val W_TAG = "w" + private const val H_TAG = "h" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Visualization(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Source object + * @param parser data referenced at parser + * @return created Source object + */ + @Suppress("ComplexMethod") + fun parse(parser: XContentParser): Visualization { + var id: String? = null + var savedVisualizationId: String? = null + var x: Int? = null + var y: Int? = null + var w: Int? = null + var h: Int? = 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) { + ID_TAG -> id = parser.text() + SAVED_VISUALIZATION_ID_TAG -> savedVisualizationId = parser.text() + X_TAG -> x = parser.intValue() + Y_TAG -> y = parser.intValue() + W_TAG -> w = parser.intValue() + H_TAG -> h = parser.intValue() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Source Skipping Unknown field $fieldName") + } + } + } + id ?: throw IllegalArgumentException("$ID_TAG field absent") + savedVisualizationId ?: throw IllegalArgumentException("$SAVED_VISUALIZATION_ID_TAG field absent") + x ?: throw IllegalArgumentException("$X_TAG field absent") + y ?: throw IllegalArgumentException("$Y_TAG field absent") + w ?: throw IllegalArgumentException("$W_TAG field absent") + h ?: throw IllegalArgumentException("$H_TAG field absent") + return Visualization(id, savedVisualizationId, x, y, w, h) + } + } + + constructor(streamInput: StreamInput) : this( + id = streamInput.readString(), + savedVisualizationId = streamInput.readString(), + x = streamInput.readInt(), + y = streamInput.readInt(), + w = streamInput.readInt(), + h = streamInput.readInt() + ) + + override fun writeTo(streamOutput: StreamOutput) { + streamOutput.writeString(id) + streamOutput.writeString(savedVisualizationId) + streamOutput.writeInt(x) + streamOutput.writeInt(y) + streamOutput.writeInt(w) + streamOutput.writeInt(h) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(ID_TAG, id) + .field(SAVED_VISUALIZATION_ID_TAG, savedVisualizationId) + .field(X_TAG, x) + .field(Y_TAG, y) + .field(W_TAG, w) + .field(H_TAG, h) + return builder.endObject() + } + } + + /** + * OperationalPanel TimeRange data class + */ + internal data class TimeRange( + val to: String, + val from: String + ) : BaseModel { + internal companion object { + private const val TO_TAG = "to" + private const val FROM_TAG = "from" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { TimeRange(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Format object + * @param parser data referenced at parser + * @return created Format object + */ + fun parse(parser: XContentParser): TimeRange { + var to: String? = null + var from: 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) { + TO_TAG -> to = parser.text() + FROM_TAG -> from = parser.text() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Format Skipping Unknown field $fieldName") + } + } + } + to ?: throw IllegalArgumentException("$TO_TAG field absent") + from ?: throw IllegalArgumentException("$FROM_TAG field absent") + return TimeRange(to, from) + } + } + + constructor(input: StreamInput) : this( + to = input.readString(), + from = input.readString() + ) + + override fun writeTo(output: StreamOutput) { + output.writeString(to) + output.writeString(from) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(TO_TAG, to) + .field(FROM_TAG, from) + builder.endObject() + return builder + } + } + + /** + * OperationalPanel QueryFilter data class + */ + internal data class QueryFilter( + val query: String, + val language: String + ) : BaseModel { + internal companion object { + private const val QUERY_TAG = "query" + private const val LANGUAGE_TAG = "language" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { QueryFilter(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): QueryFilter { + var inputText: String? = null + var inputType: 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) { + QUERY_TAG -> inputText = parser.text() + LANGUAGE_TAG -> inputType = parser.text() + else -> log.info("$LOG_PREFIX: Trigger Skipping Unknown field $fieldName") + } + } + inputText ?: throw IllegalArgumentException("$QUERY_TAG field absent") + inputType ?: throw IllegalArgumentException("$LANGUAGE_TAG field absent") + return QueryFilter(inputText, inputType) + } + } + + constructor(input: StreamInput) : this( + query = input.readString(), + language = input.readString() + ) + + override fun writeTo(output: StreamOutput) { + output.writeString(query) + output.writeString(language) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(QUERY_TAG, query) + .field(LANGUAGE_TAG, language) + builder.endObject() + return builder + } + } +} 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 new file mode 100644 index 000000000..3dd11bced --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/RestTag.kt @@ -0,0 +1,61 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ +package org.opensearch.observability.model + +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.ToXContent.Params + +/** + * Plugin Rest common Tags. + */ +internal object RestTag { + const val QUERY_FIELD = "query" + const val OBJECT_LIST_FIELD = "observabilityObjectList" + const val DELETE_RESPONSE_LIST_TAG = "deleteResponseList" + const val OBJECT_TYPE_FIELD = "objectType" + const val OBJECT_ID_FIELD = "objectId" + const val OBJECT_ID_LIST_FIELD = "objectIdList" + const val UPDATED_TIME_FIELD = "lastUpdatedTimeMs" + const val CREATED_TIME_FIELD = "createdTimeMs" + const val TENANT_FIELD = "tenant" + const val ACCESS_LIST_FIELD = "access" + const val NAME_FIELD = "name" + const val FROM_INDEX_FIELD = "fromIndex" + const val MAX_ITEMS_FIELD = "maxItems" + const val SORT_FIELD_FIELD = "sortField" + const val SORT_ORDER_FIELD = "sortOrder" + const val FILTER_PARAM_LIST_FIELD = "filterParamList" + const val NOTEBOOK_FIELD = "notebook" + const val SAVED_QUERY_FIELD = "savedQuery" + const val SAVED_VISUALIZATION_FIELD = "savedVisualization" + const val OPERATIONAL_PANEL_FIELD = "operationalPanel" + const val TIMESTAMP_FIELD = "timestamp" + private val INCLUDE_ID = Pair(OBJECT_ID_FIELD, "true") + private val EXCLUDE_ACCESS = Pair(ACCESS_LIST_FIELD, "false") + val REST_OUTPUT_PARAMS: Params = ToXContent.MapParams(mapOf(INCLUDE_ID)) + val FILTERED_REST_OUTPUT_PARAMS: Params = ToXContent.MapParams(mapOf(INCLUDE_ID, EXCLUDE_ACCESS)) +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SavedQuery.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SavedQuery.kt new file mode 100644 index 000000000..00dc53ddd --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SavedQuery.kt @@ -0,0 +1,417 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Saved query main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "query": "source=index | where utc_time > timestamp('2021-07-01 00:00:00') and utc_time < timestamp('2021-07-02 00:00:00')",
+ *   "selected_date_range": {
+ *     "start": "now/15m",
+ *     "end": "now",
+ *     "text": "utc_time > timestamp('2021-07-01 00:00:00') and utc_time < timestamp('2021-07-02 00:00:00')"
+ *   },
+ *   "selected_timestamp": {
+ *       "name": "utc_time",
+ *       "type": "timestamp"
+ *   },
+ *   "selected_fields": {
+ *       "text": "| fields clientip, bytes, memory, host",
+ *       "tokens": [
+ *           {"name":"bytes","type":"long"},
+ *           {"name":"clientip","type":"ip"}
+ *       ]
+ *   },
+ *   "name": "Logs between dates",
+ *   "description": "some descriptions related to this query"
+ * }
+ * }
+ */ + +internal data class SavedQuery( + val name: String?, + val description: String?, + val query: String?, + val selectedDateRange: SelectedDateRange?, + val selectedTimestamp: Token?, + val selectedFields: SelectedFields? +) : BaseObjectData { + + internal companion object { + private val log by logger(SavedQuery::class.java) + private const val NAME_TAG = "name" + private const val DESCRIPTION_TAG = "description" + private const val QUERY_TAG = "query" + private const val SELECTED_DATE_RANGE_TAG = "selected_date_range" + private const val SELECTED_TIMESTAMP_TAG = "selected_timestamp" + private const val SELECTED_FIELDS_TAG = "selected_fields" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { SavedQuery(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create ObservabilityObject object + * @param parser data referenced at parser + * @return created ObservabilityObject object + */ + fun parse(parser: XContentParser): SavedQuery { + var name: String? = null + var description: String? = null + var query: String? = null + var selectedDateRange: SelectedDateRange? = null + var selectedTimestamp: Token? = null + var selectedFields: SelectedFields? = 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) { + NAME_TAG -> name = parser.text() + DESCRIPTION_TAG -> description = parser.text() + QUERY_TAG -> query = parser.text() + SELECTED_DATE_RANGE_TAG -> selectedDateRange = SelectedDateRange.parse(parser) + SELECTED_TIMESTAMP_TAG -> selectedTimestamp = Token.parse(parser) + SELECTED_FIELDS_TAG -> selectedFields = SelectedFields.parse(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:SavedQuery Skipping Unknown field $fieldName") + } + } + } + return SavedQuery(name, description, query, selectedDateRange, selectedTimestamp, selectedFields) + } + } + + /** + * 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(), + query = input.readString(), + selectedDateRange = input.readOptionalWriteable(SelectedDateRange.reader), + selectedTimestamp = input.readOptionalWriteable(Token.reader), + selectedFields = input.readOptionalWriteable(SelectedFields.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(description) + output.writeString(query) + output.writeOptionalWriteable(selectedDateRange) + output.writeOptionalWriteable(selectedTimestamp) + output.writeOptionalWriteable(selectedFields) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(DESCRIPTION_TAG, description) + .fieldIfNotNull(QUERY_TAG, query) + .fieldIfNotNull(SELECTED_DATE_RANGE_TAG, selectedDateRange) + .fieldIfNotNull(SELECTED_TIMESTAMP_TAG, selectedTimestamp) + .fieldIfNotNull(SELECTED_FIELDS_TAG, selectedFields) + return builder.endObject() + } + + internal data class SelectedDateRange( + val start: String, + val end: String, + val text: String + ) : BaseModel { + internal companion object { + private const val START_TAG = "start" + private const val END_TAG = "end" + private const val TEXT_TAG = "text" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { SelectedDateRange(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): SelectedDateRange { + var start: String? = null + var end: String? = null + var text: 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) { + START_TAG -> start = parser.text() + END_TAG -> end = parser.text() + TEXT_TAG -> text = parser.text() + else -> log.info("$LOG_PREFIX: Trigger Skipping Unknown field $fieldName") + } + } + start ?: throw IllegalArgumentException("$START_TAG field absent") + end ?: throw IllegalArgumentException("$END_TAG field absent") + text ?: throw IllegalArgumentException("$TEXT_TAG field absent") + return SelectedDateRange(start, end, text) + } + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + start = input.readString(), + end = input.readString(), + text = input.readString() + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(start) + output.writeString(end) + output.writeString(text) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(START_TAG, start) + .field(END_TAG, end) + .field(TEXT_TAG, text) + builder.endObject() + return builder + } + } + + internal data class Token( + val name: String, + val type: String, + ) : BaseModel { + internal companion object { + private const val NAME_TAG = "name" + private const val TYPE_TAG = "type" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Token(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): Token { + var name: String? = null + var type: 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) { + NAME_TAG -> name = parser.text() + TYPE_TAG -> type = parser.text() + else -> log.info("$LOG_PREFIX: Trigger Skipping Unknown field $fieldName") + } + } + name ?: throw IllegalArgumentException("$NAME_TAG field absent") + type ?: throw IllegalArgumentException("$TYPE_TAG field absent") + return Token(name, type) + } + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + name = input.readString(), + type = input.readString(), + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(type) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(NAME_TAG, name) + .field(TYPE_TAG, type) + builder.endObject() + return builder + } + } + + internal data class SelectedFields( + val text: String?, + val tokens: List? + ) : BaseModel { + internal companion object { + private const val TEXT_TAG = "text" + private const val TOKENS_TAG = "tokens" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { SelectedFields(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(Token.parse(parser)) + } + return retList + } + + /** + * Parse the data from parser and create Trigger object + * @param parser data referenced at parser + * @return created Trigger object + */ + fun parse(parser: XContentParser): SelectedFields { + var text: String? = null + var tokens: List? = 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) { + TEXT_TAG -> text = parser.text() + TOKENS_TAG -> tokens = parseItemList(parser) + else -> log.info("$LOG_PREFIX: Trigger Skipping Unknown field $fieldName") + } + } + text ?: throw IllegalArgumentException("$TEXT_TAG field absent") + tokens ?: throw IllegalArgumentException("$TOKENS_TAG field absent") + return SelectedFields(text, tokens) + } + } + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + */ + constructor(input: StreamInput) : this( + text = input.readString(), + tokens = input.readList(Token.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(text) + output.writeCollection(tokens) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .field(TEXT_TAG, text) + if (tokens != null) { + builder.startArray(TOKENS_TAG) + tokens.forEach { it.toXContent(builder, params) } + builder.endArray() + } + builder.endObject() + return builder + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SavedVisualization.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SavedVisualization.kt new file mode 100644 index 000000000..3e76cdd95 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SavedVisualization.kt @@ -0,0 +1,179 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Saved query main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "query": "source=index | where utc_time > timestamp('2021-07-01 00:00:00') and utc_time < timestamp('2021-07-02 00:00:00')",
+ *   "selected_date_range": {
+ *     "start": "now/15m",
+ *     "end": "now",
+ *     "text": "utc_time > timestamp('2021-07-01 00:00:00') and utc_time < timestamp('2021-07-02 00:00:00')"
+ *   },
+ *   "selected_timestamp": {
+ *       "name": "utc_time",
+ *       "type": "timestamp"
+ *   },
+ *   "selected_fields": {
+ *       "text": "| fields clientip, bytes, memory, host",
+ *       "tokens": [
+ *           {"name":"bytes","type":"long"},
+ *           {"name":"clientip","type":"ip"}
+ *       ]
+ *   },
+ *   "type": "bar",
+ *   "name": "Logs between dates",
+ *   "description": "some descriptions related to this query"
+ * }
+ * }
+ */ + +internal data class SavedVisualization( + val name: String?, + val description: String?, + val query: String?, + val type: String?, + val selectedDateRange: SavedQuery.SelectedDateRange?, + val selectedTimestamp: SavedQuery.Token?, + val selectedFields: SavedQuery.SelectedFields? +) : BaseObjectData { + + internal companion object { + private val log by logger(SavedVisualization::class.java) + private const val NAME_TAG = "name" + private const val DESCRIPTION_TAG = "description" + private const val QUERY_TAG = "query" + private const val TYPE_TAG = "type" + private const val SELECTED_DATE_RANGE_TAG = "selected_date_range" + private const val SELECTED_TIMESTAMP_TAG = "selected_timestamp" + private const val SELECTED_FIELDS_TAG = "selected_fields" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { SavedVisualization(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create SavedVisualization object + * @param parser data referenced at parser + * @return created SavedVisualization object + */ + fun parse(parser: XContentParser): SavedVisualization { + var name: String? = null + var description: String? = null + var query: String? = null + var type: String? = null + var selectedDateRange: SavedQuery.SelectedDateRange? = null + var selectedTimestamp: SavedQuery.Token? = null + var selectedFields: SavedQuery.SelectedFields? = 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) { + NAME_TAG -> name = parser.text() + DESCRIPTION_TAG -> description = parser.text() + QUERY_TAG -> query = parser.text() + TYPE_TAG -> type = parser.text() + SELECTED_DATE_RANGE_TAG -> selectedDateRange = SavedQuery.SelectedDateRange.parse(parser) + SELECTED_TIMESTAMP_TAG -> selectedTimestamp = SavedQuery.Token.parse(parser) + SELECTED_FIELDS_TAG -> selectedFields = SavedQuery.SelectedFields.parse(parser) + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:SavedVisualization Skipping Unknown field $fieldName") + } + } + } + return SavedVisualization( + name, + description, + query, + type, + selectedDateRange, + selectedTimestamp, + selectedFields + ) + } + } + + /** + * 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(), + query = input.readString(), + type = input.readString(), + selectedDateRange = input.readOptionalWriteable(SavedQuery.SelectedDateRange.reader), + selectedTimestamp = input.readOptionalWriteable(SavedQuery.Token.reader), + selectedFields = input.readOptionalWriteable(SavedQuery.SelectedFields.reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(description) + output.writeString(query) + output.writeString(type) + output.writeOptionalWriteable(selectedDateRange) + output.writeOptionalWriteable(selectedTimestamp) + output.writeOptionalWriteable(selectedFields) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(DESCRIPTION_TAG, description) + .fieldIfNotNull(QUERY_TAG, query) + .fieldIfNotNull(TYPE_TAG, type) + .fieldIfNotNull(SELECTED_DATE_RANGE_TAG, selectedDateRange) + .fieldIfNotNull(SELECTED_TIMESTAMP_TAG, selectedTimestamp) + .fieldIfNotNull(SELECTED_FIELDS_TAG, selectedFields) + return builder.endObject() + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SearchResults.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SearchResults.kt new file mode 100644 index 000000000..2ceff501f --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/SearchResults.kt @@ -0,0 +1,220 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.model + +import org.apache.lucene.search.TotalHits.Relation +import org.apache.lucene.search.TotalHits.Relation.EQUAL_TO +import org.apache.lucene.search.TotalHits.Relation.GREATER_THAN_OR_EQUAL_TO +import org.opensearch.action.search.SearchResponse +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.Params +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.search.SearchHit + +internal abstract class SearchResults : BaseModel { + val startIndex: Long + val totalHits: Long + val totalHitRelation: Relation + val objectListFieldName: String + val objectList: List + + interface SearchHitParser { + fun parse(searchHit: SearchHit): ItemClass + } + + companion object { + private val log by org.opensearch.commons.utils.logger(SearchResults::class.java) + private const val START_INDEX_TAG = "startIndex" + private const val TOTAL_HITS_TAG = "totalHits" + private const val TOTAL_HIT_RELATION_TAG = "totalHitRelation" + private fun convertRelation(totalHitRelation: Relation): String { + return if (totalHitRelation == EQUAL_TO) { + "eq" + } else { + "gte" + } + } + + private fun convertRelation(totalHitRelation: String): Relation { + return if (totalHitRelation == "eq") { + EQUAL_TO + } else { + GREATER_THAN_OR_EQUAL_TO + } + } + } + + constructor( + objectListFieldName: String, + objectItem: ItemClass + ) { + this.startIndex = 0 + this.totalHits = 1 + this.totalHitRelation = EQUAL_TO + this.objectListFieldName = objectListFieldName + this.objectList = listOf(objectItem) + } + + constructor( + startIndex: Long, + totalHits: Long, + totalHitRelation: Relation, + objectListFieldName: String, + objectList: List + ) { + this.startIndex = startIndex + this.totalHits = totalHits + this.totalHitRelation = totalHitRelation + this.objectListFieldName = objectListFieldName + this.objectList = objectList + } + + constructor( + from: Long, + response: SearchResponse, + searchHitParser: SearchHitParser, + objectListFieldName: String + ) { + val mutableList: MutableList = mutableListOf() + response.hits.forEach { + mutableList.add(searchHitParser.parse(it)) + } + val totalHits = response.hits.totalHits + val totalHitsVal: Long + val totalHitsRelation: Relation + if (totalHits == null) { + totalHitsVal = mutableList.size.toLong() + totalHitsRelation = EQUAL_TO + } else { + totalHitsVal = totalHits.value + totalHitsRelation = totalHits.relation + } + this.startIndex = from + this.totalHits = totalHitsVal + this.totalHitRelation = totalHitsRelation + this.objectListFieldName = objectListFieldName + this.objectList = mutableList + } + + /** + * Parse the data from parser and create object + * @param parser data referenced at parser + */ + constructor(parser: XContentParser, objectListFieldName: String) { + var startIndex: Long = 0 + var totalHits: Long = 0 + var totalHitRelation: Relation = EQUAL_TO + var objectList: List? = 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) { + START_INDEX_TAG -> startIndex = parser.longValue() + TOTAL_HITS_TAG -> totalHits = parser.longValue() + TOTAL_HIT_RELATION_TAG -> totalHitRelation = convertRelation(parser.text()) + objectListFieldName -> objectList = parseItemList(parser) + else -> { + parser.skipChildren() + log.info("Skipping Unknown field $fieldName") + } + } + } + objectList ?: throw IllegalArgumentException("$objectListFieldName field absent") + if (totalHits == 0L) { + totalHits = objectList.size.toLong() + } + this.startIndex = startIndex + this.totalHits = totalHits + this.totalHitRelation = totalHitRelation + this.objectListFieldName = objectListFieldName + this.objectList = objectList + } + + /** + * 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(parseItem(parser)) + } + return retList + } + + /** + * Parse the object item + * @param parser data referenced at parser + * @return created item + */ + abstract fun parseItem(parser: XContentParser, useId: String? = null): ItemClass + + /** + * Constructor used in transport action communication. + * @param input StreamInput stream to deserialize data from. + * @param reader StreamInput reader class for reading item. + */ + constructor(input: StreamInput, reader: Writeable.Reader) : this( + startIndex = input.readLong(), + totalHits = input.readLong(), + totalHitRelation = input.readEnum(Relation::class.java), + objectListFieldName = input.readString(), + objectList = input.readList(reader) + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeLong(startIndex) + output.writeLong(totalHits) + output.writeEnum(totalHitRelation) + output.writeString(objectListFieldName) + output.writeList(objectList) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: Params?): XContentBuilder { + builder!!.startObject() + .field(START_INDEX_TAG, startIndex) + .field(TOTAL_HITS_TAG, totalHits) + .field(TOTAL_HIT_RELATION_TAG, convertRelation(totalHitRelation)) + .startArray(objectListFieldName) + objectList.forEach { it.toXContent(builder, params) } + return builder.endArray().endObject() + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Timestamp.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Timestamp.kt new file mode 100644 index 000000000..37c9b5756 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/Timestamp.kt @@ -0,0 +1,134 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +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.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.util.fieldIfNotNull +import org.opensearch.observability.util.logger + +/** + * Timestamp main data class. + * *
 JSON format
+ * {@code
+ * {
+ *   "name": "Logs between dates",
+ *   "index": "opensearch_dashboards_sample_data_logs",
+ *   "type": "timestamp",
+ *   "dsl_type": "date"
+ * }
+ * }
+ */ + +internal data class Timestamp( + val name: String?, + val index: String?, + val type: String?, + val dslType: String?, +) : BaseObjectData { + + internal companion object { + private val log by logger(Timestamp::class.java) + private const val NAME_TAG = "name" + private const val INDEX_TAG = "index" + private const val TYPE_TAG = "type" + private const val DSL_TYPE_TAG = "dsl_type" + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { Timestamp(it) } + + /** + * Parser to parse xContent + */ + val xParser = XParser { parse(it) } + + /** + * Parse the data from parser and create Timestamp object + * @param parser data referenced at parser + * @return created Timestamp object + */ + fun parse(parser: XContentParser): Timestamp { + var name: String? = null + var index: String? = null + var type: String? = null + var dslType: 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) { + NAME_TAG -> name = parser.text() + INDEX_TAG -> index = parser.text() + TYPE_TAG -> type = parser.text() + DSL_TYPE_TAG -> dslType = parser.text() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:SavedVisualization Skipping Unknown field $fieldName") + } + } + } + return Timestamp(name, index, type, dslType) + } + } + + /** + * 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(), + index = input.readString(), + type = input.readString(), + dslType = input.readString() + ) + + /** + * {@inheritDoc} + */ + override fun writeTo(output: StreamOutput) { + output.writeString(name) + output.writeString(index) + output.writeString(type) + output.writeString(dslType) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + builder.startObject() + .fieldIfNotNull(NAME_TAG, name) + .fieldIfNotNull(INDEX_TAG, index) + .fieldIfNotNull(TYPE_TAG, type) + .fieldIfNotNull(DSL_TYPE_TAG, dslType) + return builder.endObject() + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/UpdateObservabilityObjectRequest.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/UpdateObservabilityObjectRequest.kt new file mode 100644 index 000000000..fe6a19db3 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/UpdateObservabilityObjectRequest.kt @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.action.ValidateActions +import org.opensearch.common.Strings +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.ToXContentObject +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.commons.utils.fieldIfNotNull +import org.opensearch.commons.utils.logger +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import java.io.IOException + +/** + * Action request for creating new configuration. + */ +internal class UpdateObservabilityObjectRequest : ActionRequest, ToXContentObject { + val objectId: String + val type: ObservabilityObjectType + val objectData: BaseObjectData? + + companion object { + private val log by logger(UpdateObservabilityObjectRequest::class.java) + + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { UpdateObservabilityObjectRequest(it) } + + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + * @param id optional id to use if missed in XContent + */ + @JvmStatic + @Throws(IOException::class) + fun parse(parser: XContentParser, id: String? = null): UpdateObservabilityObjectRequest { + var objectId: String? = id + var type: ObservabilityObjectType? = null + var baseObjectData: BaseObjectData? = null + + XContentParserUtils.ensureExpectedToken( + XContentParser.Token.START_OBJECT, + parser.currentToken(), + parser + ) + while (parser.nextToken() != XContentParser.Token.END_OBJECT) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_FIELD -> objectId = parser.text() + else -> { + val objectTypeForTag = ObservabilityObjectType.fromTagOrDefault(fieldName) + if (objectTypeForTag != ObservabilityObjectType.NONE && baseObjectData == null) { + baseObjectData = + ObservabilityObjectDataProperties.createObjectData(objectTypeForTag, parser) + type = objectTypeForTag + } else { + parser.skipChildren() + log.info("Unexpected field: $fieldName, while parsing CreateObservabilityObjectRequest") + } + } + } + } + objectId ?: throw IllegalArgumentException("$OBJECT_ID_FIELD field absent") + type ?: throw IllegalArgumentException("Object data field absent") + baseObjectData ?: throw IllegalArgumentException("Object data field absent") + return UpdateObservabilityObjectRequest(baseObjectData, type, objectId) + } + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + builder!! + return builder.startObject() + .fieldIfNotNull(OBJECT_ID_FIELD, objectId) + .field(type.tag, objectData) + .endObject() + } + + /** + * constructor for creating the class + * @param objectData the ObservabilityObject + * @param objectId optional id to use for ObservabilityObject + */ + constructor(objectData: BaseObjectData, type: ObservabilityObjectType, objectId: String) { + this.objectData = objectData + this.type = type + this.objectId = objectId + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + objectId = input.readString() + type = input.readEnum(ObservabilityObjectType::class.java) + objectData = input.readOptionalWriteable( + ObservabilityObjectDataProperties.getReaderForObjectType( + input.readEnum( + ObservabilityObjectType::class.java + ) + ) + ) + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + super.writeTo(output) + output.writeString(objectId) + output.writeEnum(type) + output.writeOptionalWriteable(objectData) + } + + /** + * {@inheritDoc} + */ + override fun validate(): ActionRequestValidationException? { + var validationException: ActionRequestValidationException? = null + if (Strings.isNullOrEmpty(objectId)) { + validationException = ValidateActions.addValidationError("objectId is null or empty", validationException) + } + return validationException + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/UpdateObservabilityObjectResponse.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/UpdateObservabilityObjectResponse.kt new file mode 100644 index 000000000..9114ad809 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/UpdateObservabilityObjectResponse.kt @@ -0,0 +1,103 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.model + +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParser.Token +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import org.opensearch.observability.util.logger +import java.io.IOException + +/** + * ObservabilityObject-update response. + *
 JSON format
+ * {@code
+ * {
+ *   "objectId":"objectId"
+ * }
+ * }
+ */ +internal class UpdateObservabilityObjectResponse( + val objectId: String? +) : BaseResponse() { + + @Throws(IOException::class) + constructor(input: StreamInput) : this( + objectId = input.readString() + ) + + companion object { + private val log by logger(UpdateObservabilityObjectResponse::class.java) + + /** + * Parse the data from parser and create [UpdateObservabilityObjectResponse] object + * @param parser data referenced at parser + * @return created [UpdateObservabilityObjectResponse] object + */ + fun parse(parser: XContentParser): UpdateObservabilityObjectResponse { + var objectId: String? = null + XContentParserUtils.ensureExpectedToken(Token.START_OBJECT, parser.currentToken(), parser) + while (Token.END_OBJECT != parser.nextToken()) { + val fieldName = parser.currentName() + parser.nextToken() + when (fieldName) { + OBJECT_ID_FIELD -> objectId = parser.text() + else -> { + parser.skipChildren() + log.info("$LOG_PREFIX:Skipping Unknown field $fieldName") + } + } + } + objectId ?: throw IllegalArgumentException("$OBJECT_ID_FIELD field absent") + return UpdateObservabilityObjectResponse(objectId) + } + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + output.writeString(objectId) + } + + /** + * {@inheritDoc} + */ + override fun toXContent(builder: XContentBuilder?, params: ToXContent.Params?): XContentBuilder { + return builder!!.startObject() + .field(OBJECT_ID_FIELD, objectId) + .endObject() + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/XParser.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/XParser.kt new file mode 100644 index 000000000..8aebce510 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/model/XParser.kt @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.observability.model + +import org.opensearch.common.xcontent.XContentParser + +/** + * Functional interface to create object using XContentParser + */ +fun interface XParser { + /** + * Creator used in REST communication. + * @param parser XContentParser to deserialize data from. + */ + fun parse(parser: XContentParser): V +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/resthandler/ObservabilityRestHandler.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/resthandler/ObservabilityRestHandler.kt new file mode 100644 index 000000000..5a31a3fa9 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/resthandler/ObservabilityRestHandler.kt @@ -0,0 +1,251 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ +package org.opensearch.observability.resthandler + +import org.opensearch.client.node.NodeClient +import org.opensearch.commons.utils.logger +import org.opensearch.observability.ObservabilityPlugin.Companion.BASE_OBSERVABILITY_URI +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.action.CreateObservabilityObjectAction +import org.opensearch.observability.action.DeleteObservabilityObjectAction +import org.opensearch.observability.action.GetObservabilityObjectAction +import org.opensearch.observability.action.ObservabilityActions +import org.opensearch.observability.action.UpdateObservabilityObjectAction +import org.opensearch.observability.index.ObservabilityQueryHelper +import org.opensearch.observability.model.CreateObservabilityObjectRequest +import org.opensearch.observability.model.DeleteObservabilityObjectRequest +import org.opensearch.observability.model.GetObservabilityObjectRequest +import org.opensearch.observability.model.ObservabilityObjectType +import org.opensearch.observability.model.RestTag.FROM_INDEX_FIELD +import org.opensearch.observability.model.RestTag.MAX_ITEMS_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_ID_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_ID_LIST_FIELD +import org.opensearch.observability.model.RestTag.OBJECT_TYPE_FIELD +import org.opensearch.observability.model.RestTag.SORT_FIELD_FIELD +import org.opensearch.observability.model.RestTag.SORT_ORDER_FIELD +import org.opensearch.observability.model.UpdateObservabilityObjectRequest +import org.opensearch.observability.settings.PluginSettings +import org.opensearch.observability.util.contentParserNextToken +import org.opensearch.rest.BaseRestHandler +import org.opensearch.rest.BaseRestHandler.RestChannelConsumer +import org.opensearch.rest.BytesRestResponse +import org.opensearch.rest.RestHandler.Route +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestRequest.Method.DELETE +import org.opensearch.rest.RestRequest.Method.GET +import org.opensearch.rest.RestRequest.Method.POST +import org.opensearch.rest.RestRequest.Method.PUT +import org.opensearch.rest.RestStatus +import org.opensearch.search.sort.SortOrder +import java.util.EnumSet + +/** + * Rest handler for observability object lifecycle management. + * This handler uses [ObservabilityActions]. + */ +internal class ObservabilityRestHandler : BaseRestHandler() { + companion object { + private const val OBSERVABILITY_ACTION = "observability_actions" + private const val OBSERVABILITY_URL = "$BASE_OBSERVABILITY_URI/object" + private val log by logger(ObservabilityRestHandler::class.java) + } + + /** + * {@inheritDoc} + */ + override fun getName(): String { + return OBSERVABILITY_ACTION + } + + /** + * {@inheritDoc} + */ + override fun routes(): List { + return listOf( + /** + * Create a new object + * Request URL: POST OBSERVABILITY_URL + * Request body: Ref [org.opensearch.observability.model.CreateObservabilityObjectRequest] + * Response body: Ref [org.opensearch.observability.model.CreateObservabilityObjectResponse] + */ + Route(POST, OBSERVABILITY_URL), + /** + * Update object + * Request URL: PUT OBSERVABILITY_URL/{objectId} + * Request body: Ref [org.opensearch.observability.model.UpdateObservabilityObjectRequest] + * Response body: Ref [org.opensearch.observability.model.UpdateObservabilityObjectResponse] + */ + Route(PUT, "$OBSERVABILITY_URL/{$OBJECT_ID_FIELD}"), + /** + * Get a object + * Request URL: GET OBSERVABILITY_URL/{objectId} + * Request body: Ref [org.opensearch.observability.model.GetObservabilityObjectRequest] + * Response body: Ref [org.opensearch.observability.model.GetObservabilityObjectResponse] + */ + Route(GET, "$OBSERVABILITY_URL/{$OBJECT_ID_FIELD}"), + Route(GET, OBSERVABILITY_URL), + /** + * Delete object + * Request URL: DELETE OBSERVABILITY_URL/{objectId} + * Request body: Ref [org.opensearch.observability.model.DeleteObservabilityObjectRequest] + * Response body: Ref [org.opensearch.observability.model.DeleteObservabilityObjectResponse] + */ + Route(DELETE, "$OBSERVABILITY_URL/{$OBJECT_ID_FIELD}"), + Route(DELETE, "$OBSERVABILITY_URL") + ) + } + + /** + * {@inheritDoc} + */ + override fun responseParams(): Set { + return setOf( + OBJECT_ID_FIELD, + OBJECT_ID_LIST_FIELD, + OBJECT_TYPE_FIELD, + SORT_FIELD_FIELD, + SORT_ORDER_FIELD, + FROM_INDEX_FIELD, + MAX_ITEMS_FIELD + ) + } + + private fun executePostRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + return RestChannelConsumer { + client.execute( + CreateObservabilityObjectAction.ACTION_TYPE, + CreateObservabilityObjectRequest.parse(request.contentParserNextToken()), + RestResponseToXContentListener(it) + ) + } + } + + private fun executePutRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + return RestChannelConsumer { + client.execute( + UpdateObservabilityObjectAction.ACTION_TYPE, + UpdateObservabilityObjectRequest.parse(request.contentParserNextToken(), request.param(OBJECT_ID_FIELD)), + RestResponseToXContentListener(it) + ) + } + } + + private fun executeGetRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + val objectId: String? = request.param(OBJECT_ID_FIELD) + val objectIdListString: String? = request.param(OBJECT_ID_LIST_FIELD) + val objectIdList = getObjectIdSet(objectId, objectIdListString) + val types: EnumSet = getTypesSet(request.param(OBJECT_TYPE_FIELD)) + val sortField: String? = request.param(SORT_FIELD_FIELD) + val sortOrderString: String? = request.param(SORT_ORDER_FIELD) + val sortOrder: SortOrder? = if (sortOrderString == null) { + null + } else { + SortOrder.fromString(sortOrderString) + } + val fromIndex = request.param(FROM_INDEX_FIELD)?.toIntOrNull() ?: 0 + val maxItems = request.param(MAX_ITEMS_FIELD)?.toIntOrNull() ?: PluginSettings.defaultItemsQueryCount + val filterParams = request.params() + .filter { ObservabilityQueryHelper.FILTER_PARAMS.contains(it.key) } + .map { Pair(it.key, request.param(it.key)) } + .toMap() + log.info( + "$LOG_PREFIX:executeGetRequest idList:$objectIdList types:$types, from:$fromIndex, maxItems:$maxItems," + + " sortField:$sortField, sortOrder=$sortOrder, filters=$filterParams" + ) + return RestChannelConsumer { + client.execute( + GetObservabilityObjectAction.ACTION_TYPE, + GetObservabilityObjectRequest( + objectIdList, + types, + fromIndex, + maxItems, + sortField, + sortOrder, + filterParams + ), + RestResponseToXContentListener(it) + ) + } + } + + private fun executeDeleteRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + val objectId: String? = request.param(OBJECT_ID_FIELD) + val objectIdSet: Set = + request.paramAsStringArray(OBJECT_ID_LIST_FIELD, arrayOf(objectId)) + .filter { s -> !s.isNullOrBlank() } + .toSet() + return RestChannelConsumer { + if (objectIdSet.isEmpty()) { + it.sendResponse( + BytesRestResponse( + RestStatus.BAD_REQUEST, + "either $OBJECT_ID_FIELD or $OBJECT_ID_LIST_FIELD is required" + ) + ) + } else { + client.execute( + DeleteObservabilityObjectAction.ACTION_TYPE, + DeleteObservabilityObjectRequest(objectIdSet), + RestResponseToXContentListener(it) + ) + } + } + } + + /** + * {@inheritDoc} + */ + override fun prepareRequest(request: RestRequest, client: NodeClient): RestChannelConsumer { + return when (request.method()) { + POST -> executePostRequest(request, client) + PUT -> executePutRequest(request, client) + GET -> executeGetRequest(request, client) + DELETE -> executeDeleteRequest(request, client) + else -> RestChannelConsumer { + it.sendResponse(BytesRestResponse(RestStatus.METHOD_NOT_ALLOWED, "${request.method()} is not allowed")) + } + } + } + + private fun getObjectIdSet(objectId: String?, objectIdList: String?): Set { + var retIds: Set = setOf() + if (objectId != null) { + retIds = setOf(objectId) + } + if (objectIdList != null) { + retIds = objectIdList.split(",").union(retIds) + } + return retIds + } + + private fun getTypesSet(typesString: String?): EnumSet { + var types: EnumSet = EnumSet.noneOf(ObservabilityObjectType::class.java) + typesString?.split(",")?.forEach { types.add(ObservabilityObjectType.fromTagOrDefault(it)) } + return types + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/resthandler/RestResponseToXContentListener.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/resthandler/RestResponseToXContentListener.kt new file mode 100644 index 000000000..d87f4745c --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/resthandler/RestResponseToXContentListener.kt @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.resthandler + +import org.opensearch.observability.model.BaseResponse +import org.opensearch.rest.RestChannel +import org.opensearch.rest.RestStatus +import org.opensearch.rest.action.RestToXContentListener + +/** + * Overrides RestToXContentListener REST based action listener that assumes the response is of type + * {@link ToXContent} and automatically builds an XContent based response + * (wrapping the toXContent in startObject/endObject). + */ +internal class RestResponseToXContentListener(channel: RestChannel) : RestToXContentListener(channel) { + /** + * {@inheritDoc} + */ + override fun getStatus(response: Response): RestStatus { + return response.getStatus() + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/security/SecurityAccess.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/security/SecurityAccess.kt new file mode 100644 index 000000000..eb4abf381 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/security/SecurityAccess.kt @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.security + +import org.opensearch.SpecialPermission +import java.security.AccessController +import java.security.PrivilegedActionException +import java.security.PrivilegedExceptionAction + +/** + * Class for providing the elevated permission for the function call. + * Ref: + * https://www.elastic.co/guide/en/elasticsearch/plugins/current/plugin-authors.html#_java_security_permissions + */ +internal object SecurityAccess { + /** + * Execute the operation in privileged mode. + */ + @Throws(Exception::class) + fun doPrivileged(operation: PrivilegedExceptionAction?): T { + SpecialPermission.check() + return try { + AccessController.doPrivileged(operation) + } catch (e: PrivilegedActionException) { + throw (e.cause as Exception?)!! + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/security/UserAccessManager.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/security/UserAccessManager.kt new file mode 100644 index 000000000..c2e80cdb3 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/security/UserAccessManager.kt @@ -0,0 +1,207 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.security + +import org.opensearch.OpenSearchStatusException +import org.opensearch.commons.authuser.User +import org.opensearch.observability.settings.PluginSettings +import org.opensearch.observability.settings.PluginSettings.FilterBy +import org.opensearch.rest.RestStatus +import java.util.stream.Collectors + +/** + * Class for checking/filtering user access. + */ +internal object UserAccessManager { + private const val USER_TAG = "User:" + private const val ROLE_TAG = "Role:" + private const val BACKEND_ROLE_TAG = "BERole:" + private const val ALL_ACCESS_ROLE = "all_access" + private const val OPENSEARCH_DASHBOARDS_SERVER_USER = "opensearchdashboardsserver" // TODO: Change it to background user when created. + private const val PRIVATE_TENANT = "__user__" + const val DEFAULT_TENANT = "" + + /** + * Validate User if eligible to do operation + * If filterBy == NoFilter + * -> No validation + * If filterBy == User + * -> User name should be present + * If filterBy == Roles + * -> roles should be present + * If filterBy == BackendRoles + * -> backend roles should be present + */ + fun validateUser(user: User?) { + if (isUserPrivateTenant(user) && user?.name == null) { + throw OpenSearchStatusException( + "User name not provided for private tenant access", + RestStatus.FORBIDDEN + ) + } + when (PluginSettings.filterBy) { + FilterBy.NoFilter -> { // No validation + } + FilterBy.User -> { // User name must be present + user?.name + ?: throw OpenSearchStatusException( + "Filter-by enabled with security disabled", + RestStatus.FORBIDDEN + ) + } + FilterBy.Roles -> { // backend roles must be present + if (user == null || user.roles.isNullOrEmpty()) { + throw OpenSearchStatusException( + "User doesn't have roles configured. Contact administrator.", + RestStatus.FORBIDDEN + ) + } else if (user.roles.stream().filter { !PluginSettings.ignoredRoles.contains(it) }.count() == 0L) { + throw OpenSearchStatusException( + "No distinguishing roles configured. Contact administrator.", + RestStatus.FORBIDDEN + ) + } + } + FilterBy.BackendRoles -> { // backend roles must be present + if (user?.backendRoles.isNullOrEmpty()) { + throw OpenSearchStatusException( + "User doesn't have backend roles configured. Contact administrator.", + RestStatus.FORBIDDEN + ) + } + } + } + } + + /** + * validate if user has access to polling actions + */ + fun validatePollingUser(user: User?) { + if (user != null) { // Check only if security is enabled + if (user.name != OPENSEARCH_DASHBOARDS_SERVER_USER) { + throw OpenSearchStatusException("Permission denied", RestStatus.FORBIDDEN) + } + } + } + + /** + * Get tenant info from user object. + */ + fun getUserTenant(user: User?): String { + return when (val requestedTenant = user?.requestedTenant) { + null -> DEFAULT_TENANT + else -> requestedTenant + } + } + + /** + * Get all user access info from user object. + */ + fun getAllAccessInfo(user: User?): List { + if (user == null) { // Security is disabled + return listOf() + } + val retList: MutableList = mutableListOf() + if (user.name != null) { + retList.add("$USER_TAG${user.name}") + } + user.roles.forEach { retList.add("$ROLE_TAG$it") } + user.backendRoles.forEach { retList.add("$BACKEND_ROLE_TAG$it") } + return retList + } + + /** + * Get access info for search filtering + */ + fun getSearchAccessInfo(user: User?): List { + if (user == null) { // Security is disabled + return listOf() + } + if (isUserPrivateTenant(user)) { + return listOf("$USER_TAG${user.name}") // No sharing allowed in private tenant. + } + if (canAdminViewAllItems(user)) { + return listOf() + } + return when (PluginSettings.filterBy) { + FilterBy.NoFilter -> listOf() + FilterBy.User -> listOf("$USER_TAG${user.name}") + FilterBy.Roles -> user.roles.stream() + .filter { !PluginSettings.ignoredRoles.contains(it) } + .map { "$ROLE_TAG$it" } + .collect(Collectors.toList()) + FilterBy.BackendRoles -> user.backendRoles.map { "$BACKEND_ROLE_TAG$it" } + } + } + + /** + * validate if user has access based on given access list + */ + fun doesUserHasAccess(user: User?, tenant: String, access: List): Boolean { + if (user == null) { // Security is disabled + return true + } + if (getUserTenant(user) != tenant) { + return false + } + if (canAdminViewAllItems(user)) { + return true + } + return when (PluginSettings.filterBy) { + FilterBy.NoFilter -> true + FilterBy.User -> access.contains("$USER_TAG${user.name}") + FilterBy.Roles -> user.roles.stream() + .filter { !PluginSettings.ignoredRoles.contains(it) } + .map { "$ROLE_TAG$it" } + .anyMatch { it in access } + FilterBy.BackendRoles -> user.backendRoles.map { "$BACKEND_ROLE_TAG$it" }.any { it in access } + } + } + + /** + * Check if user has all info access. + */ + fun hasAllInfoAccess(user: User?): Boolean { + if (user == null) { // Security is disabled + return true + } + return isAdminUser(user) + } + + private fun canAdminViewAllItems(user: User): Boolean { + return PluginSettings.adminAccess == PluginSettings.AdminAccess.AllNotebooks && isAdminUser(user) + } + + private fun isAdminUser(user: User): Boolean { + return user.roles.contains(ALL_ACCESS_ROLE) + } + + private fun isUserPrivateTenant(user: User?): Boolean { + return getUserTenant(user) == PRIVATE_TENANT + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/settings/PluginSettings.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/settings/PluginSettings.kt new file mode 100644 index 000000000..49afb5f22 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/settings/PluginSettings.kt @@ -0,0 +1,499 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.settings + +import org.apache.logging.log4j.LogManager +import org.opensearch.bootstrap.BootstrapInfo +import org.opensearch.cluster.service.ClusterService +import org.opensearch.common.settings.Setting +import org.opensearch.common.settings.Setting.Property.Dynamic +import org.opensearch.common.settings.Setting.Property.NodeScope +import org.opensearch.common.settings.Settings +import org.opensearch.observability.ObservabilityPlugin.Companion.LOG_PREFIX +import org.opensearch.observability.ObservabilityPlugin.Companion.PLUGIN_NAME +import java.io.IOException +import java.nio.file.Path + +/** + * settings specific to observability Plugin. + */ +internal object PluginSettings { + + /** + * Settings Key prefix for this plugin. + */ + private const val KEY_PREFIX = "opensearch.observability" + + /** + * General settings Key prefix. + */ + private const val GENERAL_KEY_PREFIX = "$KEY_PREFIX.general" + + /** + * Polling settings Key prefix. + */ + private const val POLLING_KEY_PREFIX = "$KEY_PREFIX.polling" + + /** + * Access settings Key prefix. + */ + private const val ACCESS_KEY_PREFIX = "$KEY_PREFIX.access" + + /** + * Operation timeout for network operations. + */ + private const val OPERATION_TIMEOUT_MS_KEY = "$GENERAL_KEY_PREFIX.operationTimeoutMs" + + /** + * Setting to choose Job lock duration. + */ + private const val JOB_LOCK_DURATION_S_KEY = "$POLLING_KEY_PREFIX.jobLockDurationSeconds" + + /** + * Setting to choose Minimum polling duration. + */ + private const val MIN_POLLING_DURATION_S_KEY = "$POLLING_KEY_PREFIX.minPollingDurationSeconds" + + /** + * Setting to choose Maximum polling duration. + */ + private const val MAX_POLLING_DURATION_S_KEY = "$POLLING_KEY_PREFIX.maxPollingDurationSeconds" + + /** + * Setting to choose Maximum number of retries to try locking. + */ + private const val MAX_LOCK_RETRIES_KEY = "$POLLING_KEY_PREFIX.maxLockRetries" + + /** + * Setting to choose default number of items to query. + */ + private const val DEFAULT_ITEMS_QUERY_COUNT_KEY = "$GENERAL_KEY_PREFIX.defaultItemsQueryCount" + + /** + * Setting to choose admin access restriction. + */ + private const val ADMIN_ACCESS_KEY = "$ACCESS_KEY_PREFIX.adminAccess" + + /** + * Setting to choose filter method. + */ + private const val FILTER_BY_KEY = "$ACCESS_KEY_PREFIX.filterBy" + + /** + * Setting to choose ignored roles for filtering. + */ + private const val IGNORE_ROLE_KEY = "$ACCESS_KEY_PREFIX.ignoreRoles" + + /** + * Default operation timeout for network operations. + */ + private const val DEFAULT_OPERATION_TIMEOUT_MS = 60000L + + /** + * Minimum operation timeout for network operations. + */ + private const val MINIMUM_OPERATION_TIMEOUT_MS = 100L + + /** + * Default Job lock duration. + */ + private const val DEFAULT_JOB_LOCK_DURATION_S = 300 + + /** + * Minimum Job lock duration. + */ + private const val MINIMUM_JOB_LOCK_DURATION_S = 10 + + /** + * Default Minimum polling duration. + */ + private const val DEFAULT_MIN_POLLING_DURATION_S = 300 + + /** + * Minimum Min polling duration. + */ + private const val MINIMUM_MIN_POLLING_DURATION_S = 60 + + /** + * Default Maximum polling duration. + */ + private const val DEFAULT_MAX_POLLING_DURATION_S = 900 + + /** + * Minimum Maximum polling duration. + */ + private const val MINIMUM_MAX_POLLING_DURATION_S = 300 + + /** + * Default number of retries to try locking. + */ + private const val DEFAULT_MAX_LOCK_RETRIES = 4 + + /** + * Minimum number of retries to try locking. + */ + private const val MINIMUM_LOCK_RETRIES = 1 + + /** + * Default number of items to query. + */ + private const val DEFAULT_ITEMS_QUERY_COUNT_VALUE = 1000 + + /** + * Minimum number of items to query. + */ + private const val MINIMUM_ITEMS_QUERY_COUNT = 10 + + /** + * Default admin access method. + */ + private const val DEFAULT_ADMIN_ACCESS_METHOD = "AllNotebooks" + + /** + * Default filter-by method. + */ + private const val DEFAULT_FILTER_BY_METHOD = "NoFilter" + + /** + * Default filter-by method. + */ + private val DEFAULT_IGNORED_ROLES = listOf( + "own_index", + "opensearch_dashboards_user", + "notebooks_full_access", + "notebooks_read_access" + ) + + /** + * Operation timeout setting in ms for I/O operations + */ + @Volatile + var operationTimeoutMs: Long + + /** + * Job lock duration + */ + @Volatile + var jobLockDurationSeconds: Int + + /** + * Minimum polling duration + */ + @Volatile + var minPollingDurationSeconds: Int + + /** + * Maximum polling duration. + */ + @Volatile + var maxPollingDurationSeconds: Int + + /** + * Max number of retries to try locking. + */ + @Volatile + var maxLockRetries: Int + + /** + * Default number of items to query. + */ + @Volatile + var defaultItemsQueryCount: Int + + /** + * admin access method. + */ + @Volatile + var adminAccess: AdminAccess + + /** + * Filter-by method. + */ + @Volatile + var filterBy: FilterBy + + /** + * list of ignored roles. + */ + @Volatile + var ignoredRoles: List + + /** + * Enum for types of admin access + * "Standard" -> Admin user access follows standard user + * "AllNotebooks" -> Admin user with "all_access" role can see all notebooks of all users. + */ + internal enum class AdminAccess { Standard, AllNotebooks } + + /** + * Enum for types of filterBy options + * NoFilter -> everyone see each other's notebooks + * User -> notebooks are visible to only themselves + * Roles -> notebooks are visible to users having any one of the role of creator + * BackendRoles -> notebooks are visible to users having any one of the backend role of creator + */ + internal enum class FilterBy { NoFilter, User, Roles, BackendRoles } + + private const val DECIMAL_RADIX: Int = 10 + + private val log = LogManager.getLogger(javaClass) + private val defaultSettings: Map + + init { + var settings: Settings? = null + val configDirName = BootstrapInfo.getSystemProperties()?.get("opensearch.path.conf")?.toString() + if (configDirName != null) { + val defaultSettingYmlFile = Path.of(configDirName, PLUGIN_NAME, "observability.yml") + try { + settings = Settings.builder().loadFromPath(defaultSettingYmlFile).build() + } catch (exception: IOException) { + log.warn("$LOG_PREFIX:Failed to load ${defaultSettingYmlFile.toAbsolutePath()}") + } + } + // Initialize the settings values to default values + operationTimeoutMs = (settings?.get(OPERATION_TIMEOUT_MS_KEY)?.toLong()) ?: DEFAULT_OPERATION_TIMEOUT_MS + jobLockDurationSeconds = (settings?.get(JOB_LOCK_DURATION_S_KEY)?.toInt()) ?: DEFAULT_JOB_LOCK_DURATION_S + minPollingDurationSeconds = (settings?.get(MIN_POLLING_DURATION_S_KEY)?.toInt()) + ?: DEFAULT_MIN_POLLING_DURATION_S + maxPollingDurationSeconds = (settings?.get(MAX_POLLING_DURATION_S_KEY)?.toInt()) + ?: DEFAULT_MAX_POLLING_DURATION_S + maxLockRetries = (settings?.get(MAX_LOCK_RETRIES_KEY)?.toInt()) ?: DEFAULT_MAX_LOCK_RETRIES + defaultItemsQueryCount = (settings?.get(DEFAULT_ITEMS_QUERY_COUNT_KEY)?.toInt()) + ?: DEFAULT_ITEMS_QUERY_COUNT_VALUE + adminAccess = AdminAccess.valueOf(settings?.get(ADMIN_ACCESS_KEY) ?: DEFAULT_ADMIN_ACCESS_METHOD) + filterBy = FilterBy.valueOf(settings?.get(FILTER_BY_KEY) ?: DEFAULT_FILTER_BY_METHOD) + ignoredRoles = settings?.getAsList(IGNORE_ROLE_KEY) ?: DEFAULT_IGNORED_ROLES + + defaultSettings = mapOf( + OPERATION_TIMEOUT_MS_KEY to operationTimeoutMs.toString(DECIMAL_RADIX), + JOB_LOCK_DURATION_S_KEY to jobLockDurationSeconds.toString(DECIMAL_RADIX), + MIN_POLLING_DURATION_S_KEY to minPollingDurationSeconds.toString(DECIMAL_RADIX), + MAX_POLLING_DURATION_S_KEY to maxPollingDurationSeconds.toString(DECIMAL_RADIX), + MAX_LOCK_RETRIES_KEY to maxLockRetries.toString(DECIMAL_RADIX), + DEFAULT_ITEMS_QUERY_COUNT_KEY to defaultItemsQueryCount.toString(DECIMAL_RADIX), + ADMIN_ACCESS_KEY to adminAccess.name, + FILTER_BY_KEY to filterBy.name + ) + } + + private val OPERATION_TIMEOUT_MS: Setting = Setting.longSetting( + OPERATION_TIMEOUT_MS_KEY, + defaultSettings[OPERATION_TIMEOUT_MS_KEY]!!.toLong(), + MINIMUM_OPERATION_TIMEOUT_MS, + NodeScope, Dynamic + ) + + private val JOB_LOCK_DURATION_S: Setting = Setting.intSetting( + JOB_LOCK_DURATION_S_KEY, + defaultSettings[JOB_LOCK_DURATION_S_KEY]!!.toInt(), + MINIMUM_JOB_LOCK_DURATION_S, + NodeScope, Dynamic + ) + + private val MIN_POLLING_DURATION_S: Setting = Setting.intSetting( + MIN_POLLING_DURATION_S_KEY, + defaultSettings[MIN_POLLING_DURATION_S_KEY]!!.toInt(), + MINIMUM_MIN_POLLING_DURATION_S, + NodeScope, Dynamic + ) + + private val MAX_POLLING_DURATION_S: Setting = Setting.intSetting( + MAX_POLLING_DURATION_S_KEY, + defaultSettings[MAX_POLLING_DURATION_S_KEY]!!.toInt(), + MINIMUM_MAX_POLLING_DURATION_S, + NodeScope, Dynamic + ) + + private val MAX_LOCK_RETRIES: Setting = Setting.intSetting( + MAX_LOCK_RETRIES_KEY, + defaultSettings[MAX_LOCK_RETRIES_KEY]!!.toInt(), + MINIMUM_LOCK_RETRIES, + NodeScope, Dynamic + ) + + private val DEFAULT_ITEMS_QUERY_COUNT: Setting = Setting.intSetting( + DEFAULT_ITEMS_QUERY_COUNT_KEY, + defaultSettings[DEFAULT_ITEMS_QUERY_COUNT_KEY]!!.toInt(), + MINIMUM_ITEMS_QUERY_COUNT, + NodeScope, Dynamic + ) + + private val ADMIN_ACCESS: Setting = Setting.simpleString( + ADMIN_ACCESS_KEY, + defaultSettings[ADMIN_ACCESS_KEY]!!, + NodeScope, Dynamic + ) + + private val FILTER_BY: Setting = Setting.simpleString( + FILTER_BY_KEY, + defaultSettings[FILTER_BY_KEY]!!, + NodeScope, Dynamic + ) + + private val IGNORED_ROLES: Setting> = Setting.listSetting( + IGNORE_ROLE_KEY, + DEFAULT_IGNORED_ROLES, + { it }, + NodeScope, Dynamic + ) + + /** + * Returns list of additional settings available specific to this plugin. + * + * @return list of settings defined in this plugin + */ + fun getAllSettings(): List> { + return listOf( + OPERATION_TIMEOUT_MS, + JOB_LOCK_DURATION_S, + MIN_POLLING_DURATION_S, + MAX_POLLING_DURATION_S, + MAX_LOCK_RETRIES, + DEFAULT_ITEMS_QUERY_COUNT, + ADMIN_ACCESS, + FILTER_BY, + IGNORED_ROLES + ) + } + + /** + * Update the setting variables to setting values from local settings + * @param clusterService cluster service instance + */ + private fun updateSettingValuesFromLocal(clusterService: ClusterService) { + operationTimeoutMs = OPERATION_TIMEOUT_MS.get(clusterService.settings) + jobLockDurationSeconds = JOB_LOCK_DURATION_S.get(clusterService.settings) + minPollingDurationSeconds = MIN_POLLING_DURATION_S.get(clusterService.settings) + maxPollingDurationSeconds = MAX_POLLING_DURATION_S.get(clusterService.settings) + maxLockRetries = MAX_LOCK_RETRIES.get(clusterService.settings) + defaultItemsQueryCount = DEFAULT_ITEMS_QUERY_COUNT.get(clusterService.settings) + adminAccess = AdminAccess.valueOf(ADMIN_ACCESS.get(clusterService.settings)) + filterBy = FilterBy.valueOf(FILTER_BY.get(clusterService.settings)) + ignoredRoles = IGNORED_ROLES.get(clusterService.settings) + } + + /** + * Update the setting variables to setting values from cluster settings + * @param clusterService cluster service instance + */ + private fun updateSettingValuesFromCluster(clusterService: ClusterService) { + val clusterOperationTimeoutMs = clusterService.clusterSettings.get(OPERATION_TIMEOUT_MS) + if (clusterOperationTimeoutMs != null) { + log.debug("$LOG_PREFIX:$OPERATION_TIMEOUT_MS_KEY -autoUpdatedTo-> $clusterOperationTimeoutMs") + operationTimeoutMs = clusterOperationTimeoutMs + } + val clusterJobLockDurationSeconds = clusterService.clusterSettings.get(JOB_LOCK_DURATION_S) + if (clusterJobLockDurationSeconds != null) { + log.debug("$LOG_PREFIX:$JOB_LOCK_DURATION_S_KEY -autoUpdatedTo-> $clusterJobLockDurationSeconds") + jobLockDurationSeconds = clusterJobLockDurationSeconds + } + val clusterMinPollingDurationSeconds = clusterService.clusterSettings.get(MIN_POLLING_DURATION_S) + if (clusterMinPollingDurationSeconds != null) { + log.debug("$LOG_PREFIX:$MIN_POLLING_DURATION_S_KEY -autoUpdatedTo-> $clusterMinPollingDurationSeconds") + minPollingDurationSeconds = clusterMinPollingDurationSeconds + } + val clusterMaxPollingDurationSeconds = clusterService.clusterSettings.get(MAX_POLLING_DURATION_S) + if (clusterMaxPollingDurationSeconds != null) { + log.debug("$LOG_PREFIX:$MAX_POLLING_DURATION_S_KEY -autoUpdatedTo-> $clusterMaxPollingDurationSeconds") + maxPollingDurationSeconds = clusterMaxPollingDurationSeconds + } + val clusterMaxLockRetries = clusterService.clusterSettings.get(MAX_LOCK_RETRIES) + if (clusterMaxLockRetries != null) { + log.debug("$LOG_PREFIX:$MAX_LOCK_RETRIES_KEY -autoUpdatedTo-> $clusterMaxLockRetries") + maxLockRetries = clusterMaxLockRetries + } + val clusterDefaultItemsQueryCount = clusterService.clusterSettings.get(DEFAULT_ITEMS_QUERY_COUNT) + if (clusterDefaultItemsQueryCount != null) { + log.debug("$LOG_PREFIX:$DEFAULT_ITEMS_QUERY_COUNT_KEY -autoUpdatedTo-> $clusterDefaultItemsQueryCount") + defaultItemsQueryCount = clusterDefaultItemsQueryCount + } + val clusterAdminAccess = clusterService.clusterSettings.get(ADMIN_ACCESS) + if (clusterAdminAccess != null) { + log.debug("$LOG_PREFIX:$ADMIN_ACCESS_KEY -autoUpdatedTo-> $clusterAdminAccess") + adminAccess = AdminAccess.valueOf(clusterAdminAccess) + } + val clusterFilterBy = clusterService.clusterSettings.get(FILTER_BY) + if (clusterFilterBy != null) { + log.debug("$LOG_PREFIX:$FILTER_BY_KEY -autoUpdatedTo-> $clusterFilterBy") + filterBy = FilterBy.valueOf(clusterFilterBy) + } + val clusterIgnoredRoles = clusterService.clusterSettings.get(IGNORED_ROLES) + if (clusterIgnoredRoles != null) { + log.debug("$LOG_PREFIX:$IGNORE_ROLE_KEY -autoUpdatedTo-> $clusterIgnoredRoles") + ignoredRoles = clusterIgnoredRoles + } + } + + /** + * adds Settings update listeners to all settings. + * @param clusterService cluster service instance + */ + fun addSettingsUpdateConsumer(clusterService: ClusterService) { + updateSettingValuesFromLocal(clusterService) + // Update the variables to cluster setting values + // If the cluster is not yet started then we get default values again + updateSettingValuesFromCluster(clusterService) + + clusterService.clusterSettings.addSettingsUpdateConsumer(OPERATION_TIMEOUT_MS) { + operationTimeoutMs = it + log.info("$LOG_PREFIX:$OPERATION_TIMEOUT_MS_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(JOB_LOCK_DURATION_S) { + jobLockDurationSeconds = it + log.info("$LOG_PREFIX:$JOB_LOCK_DURATION_S_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(MIN_POLLING_DURATION_S) { + minPollingDurationSeconds = it + log.info("$LOG_PREFIX:$MIN_POLLING_DURATION_S_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_POLLING_DURATION_S) { + maxPollingDurationSeconds = it + log.info("$LOG_PREFIX:$MAX_POLLING_DURATION_S_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_LOCK_RETRIES) { + maxLockRetries = it + log.info("$LOG_PREFIX:$MAX_LOCK_RETRIES_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(DEFAULT_ITEMS_QUERY_COUNT) { + defaultItemsQueryCount = it + log.info("$LOG_PREFIX:$DEFAULT_ITEMS_QUERY_COUNT_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(ADMIN_ACCESS) { + adminAccess = AdminAccess.valueOf(it) + log.info("$LOG_PREFIX:$ADMIN_ACCESS_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(FILTER_BY) { + filterBy = FilterBy.valueOf(it) + log.info("$LOG_PREFIX:$FILTER_BY_KEY -updatedTo-> $it") + } + clusterService.clusterSettings.addSettingsUpdateConsumer(IGNORED_ROLES) { + ignoredRoles = it + log.info("$LOG_PREFIX:$IGNORE_ROLE_KEY -updatedTo-> $it") + } + } +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/Helpers.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/Helpers.kt new file mode 100644 index 000000000..6d0a5248d --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/Helpers.kt @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.util + +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.xcontent.DeprecationHandler +import org.opensearch.common.xcontent.NamedXContentRegistry +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.ToXContentObject +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.common.xcontent.XContentParser +import org.opensearch.common.xcontent.XContentParser.Token +import org.opensearch.common.xcontent.XContentParserUtils +import org.opensearch.common.xcontent.XContentType +import org.opensearch.rest.RestRequest + +internal fun StreamInput.createJsonParser(): XContentParser { + return XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.IGNORE_DEPRECATIONS, this) +} + +internal fun RestRequest.contentParserNextToken(): XContentParser { + val parser = this.contentParser() + parser.nextToken() + return parser +} + +internal fun XContentParser.stringList(): List { + val retList: MutableList = mutableListOf() + XContentParserUtils.ensureExpectedToken(Token.START_ARRAY, currentToken(), this) + while (nextToken() != Token.END_ARRAY) { + retList.add(text()) + } + return retList +} + +internal fun logger(forClass: Class): Lazy { + return lazy { LogManager.getLogger(forClass) } +} + +internal fun XContentBuilder.fieldIfNotNull(name: String, value: Any?): XContentBuilder { + if (value != null) { + this.field(name, value) + } + return this +} + +internal fun XContentBuilder.objectIfNotNull(name: String, xContentObject: ToXContentObject?): XContentBuilder { + if (xContentObject != null) { + this.field(name) + xContentObject.toXContent(this, ToXContent.EMPTY_PARAMS) + } + return this +} diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/OpenForTesting.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/OpenForTesting.kt new file mode 100644 index 000000000..05e6a2e38 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/OpenForTesting.kt @@ -0,0 +1,33 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.util + +/** + * Annotation to elevate access for testing purpose. + */ +annotation class OpenForTesting diff --git a/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/SecureIndexClient.kt b/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/SecureIndexClient.kt new file mode 100644 index 000000000..76b217750 --- /dev/null +++ b/opensearch-observability/src/main/kotlin/org/opensearch/observability/util/SecureIndexClient.kt @@ -0,0 +1,325 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package org.opensearch.observability.util + +import org.opensearch.action.ActionFuture +import org.opensearch.action.ActionListener +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionResponse +import org.opensearch.action.ActionType +import org.opensearch.action.bulk.BulkRequest +import org.opensearch.action.bulk.BulkResponse +import org.opensearch.action.delete.DeleteRequest +import org.opensearch.action.delete.DeleteResponse +import org.opensearch.action.explain.ExplainRequest +import org.opensearch.action.explain.ExplainResponse +import org.opensearch.action.fieldcaps.FieldCapabilitiesRequest +import org.opensearch.action.fieldcaps.FieldCapabilitiesResponse +import org.opensearch.action.get.GetRequest +import org.opensearch.action.get.GetResponse +import org.opensearch.action.get.MultiGetRequest +import org.opensearch.action.get.MultiGetResponse +import org.opensearch.action.index.IndexRequest +import org.opensearch.action.index.IndexResponse +import org.opensearch.action.search.ClearScrollRequest +import org.opensearch.action.search.ClearScrollResponse +import org.opensearch.action.search.MultiSearchRequest +import org.opensearch.action.search.MultiSearchResponse +import org.opensearch.action.search.SearchRequest +import org.opensearch.action.search.SearchResponse +import org.opensearch.action.search.SearchScrollRequest +import org.opensearch.action.termvectors.MultiTermVectorsRequest +import org.opensearch.action.termvectors.MultiTermVectorsResponse +import org.opensearch.action.termvectors.TermVectorsRequest +import org.opensearch.action.termvectors.TermVectorsResponse +import org.opensearch.action.update.UpdateRequest +import org.opensearch.action.update.UpdateResponse +import org.opensearch.client.Client +import org.opensearch.common.util.concurrent.ThreadContext + +/** + * Wrapper class on [Client] with security context removed. + */ +@Suppress("TooManyFunctions") +internal class SecureIndexClient(private val client: Client) : Client by client { + /** + * {@inheritDoc} + */ + override fun execute( + action: ActionType, + request: Request + ): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.execute(action, request) } + } + + /** + * {@inheritDoc} + */ + override fun execute( + action: ActionType, + request: Request, + listener: ActionListener + ) { + client.threadPool().threadContext.stashContext().use { return client.execute(action, request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun index(request: IndexRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.index(request) } + } + + /** + * {@inheritDoc} + */ + override fun index(request: IndexRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.index(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun update(request: UpdateRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.update(request) } + } + + /** + * {@inheritDoc} + */ + override fun update(request: UpdateRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.update(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun delete(request: DeleteRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.delete(request) } + } + + /** + * {@inheritDoc} + */ + override fun delete(request: DeleteRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.delete(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun bulk(request: BulkRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.bulk(request) } + } + + /** + * {@inheritDoc} + */ + override fun bulk(request: BulkRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.bulk(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun get(request: GetRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.get(request) } + } + + /** + * {@inheritDoc} + */ + override fun get(request: GetRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.get(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun multiGet(request: MultiGetRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.multiGet(request) } + } + + /** + * {@inheritDoc} + */ + override fun multiGet(request: MultiGetRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.multiGet(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun search(request: SearchRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.search(request) } + } + + /** + * {@inheritDoc} + */ + override fun search(request: SearchRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.search(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun searchScroll(request: SearchScrollRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.searchScroll(request) } + } + + /** + * {@inheritDoc} + */ + override fun searchScroll(request: SearchScrollRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.searchScroll(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun multiSearch(request: MultiSearchRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.multiSearch(request) } + } + + /** + * {@inheritDoc} + */ + override fun multiSearch(request: MultiSearchRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.multiSearch(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun termVectors(request: TermVectorsRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.termVectors(request) } + } + + /** + * {@inheritDoc} + */ + override fun termVectors(request: TermVectorsRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.termVectors(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun multiTermVectors(request: MultiTermVectorsRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.multiTermVectors(request) } + } + + /** + * {@inheritDoc} + */ + override fun multiTermVectors(request: MultiTermVectorsRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.multiTermVectors(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun explain(request: ExplainRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.explain(request) } + } + + /** + * {@inheritDoc} + */ + override fun explain(request: ExplainRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.explain(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun clearScroll(request: ClearScrollRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.clearScroll(request) } + } + + /** + * {@inheritDoc} + */ + override fun clearScroll(request: ClearScrollRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.clearScroll(request, listener) } + } + + /** + * {@inheritDoc} + */ + override fun fieldCaps(request: FieldCapabilitiesRequest): ActionFuture { + client.threadPool().threadContext.stashContext().use { return client.fieldCaps(request) } + } + + /** + * {@inheritDoc} + */ + override fun fieldCaps(request: FieldCapabilitiesRequest, listener: ActionListener) { + client.threadPool().threadContext.stashContext().use { return client.fieldCaps(request, listener) } + } + + /** + * Executes the given [block] function on this resource and then closes it down correctly whether an exception + * is thrown or not. + * + * In case if the resource is being closed due to an exception occurred in [block], and the closing also fails with an exception, + * the latter is added to the [suppressed][java.lang.Throwable.addSuppressed] exceptions of the former. + * + * @param block a function to process this [AutoCloseable] resource. + * @return the result of [block] function invoked on this resource. + */ + @Suppress("TooGenericExceptionCaught") + private inline fun T.use(block: (T) -> R): R { + var exception: Throwable? = null + try { + return block(this) + } catch (e: Throwable) { + exception = e + throw e + } finally { + closeFinally(exception) + } + } + + /** + * Closes this [AutoCloseable], suppressing possible exception or error thrown by [AutoCloseable.close] function when + * it's being closed due to some other [cause] exception occurred. + * + * The suppressed exception is added to the list of suppressed exceptions of [cause] exception. + */ + @Suppress("TooGenericExceptionCaught") + private fun ThreadContext.StoredContext.closeFinally(cause: Throwable?) = when (cause) { + null -> close() + else -> try { + close() + } catch (closeException: Throwable) { + cause.addSuppressed(closeException) + } + } +} diff --git a/opensearch-observability/src/main/plugin-metadata/plugin-security.policy b/opensearch-observability/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 000000000..9e033ea01 --- /dev/null +++ b/opensearch-observability/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +grant { + // needed to find the classloader to load whitelisted classes. + permission java.lang.RuntimePermission "createClassLoader"; + permission java.lang.RuntimePermission "getClassLoader"; + + permission java.net.SocketPermission "*", "connect,resolve"; + permission java.net.NetPermission "getProxySelector"; +}; diff --git a/opensearch-observability/src/main/resources/META-INF/services/com.amazon.opendistroforelasticsearch.jobscheduler.spi.JobSchedulerExtension b/opensearch-observability/src/main/resources/META-INF/services/com.amazon.opendistroforelasticsearch.jobscheduler.spi.JobSchedulerExtension new file mode 100644 index 000000000..c4d972fb0 --- /dev/null +++ b/opensearch-observability/src/main/resources/META-INF/services/com.amazon.opendistroforelasticsearch.jobscheduler.spi.JobSchedulerExtension @@ -0,0 +1 @@ +org.opensearch.observability.ObservabilityPlugin diff --git a/opensearch-observability/src/main/resources/observability-mapping.yml b/opensearch-observability/src/main/resources/observability-mapping.yml new file mode 100644 index 000000000..a47081d81 --- /dev/null +++ b/opensearch-observability/src/main/resources/observability-mapping.yml @@ -0,0 +1,86 @@ +--- +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +## +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +## + +# Schema file for the observability index +# Since we only search based on "access", sort on lastUpdatedTimeMs & createdTimeMs, +# other fields are not used in mapping to avoid index on those fields. +# Also "dynamic" is set to "false" so that other fields can be added. +dynamic: false +properties: + lastUpdatedTimeMs: + type: date + format: epoch_millis + createdTimeMs: + type: date + format: epoch_millis + tenant: + type: keyword + access: # Array of access details like user,role,backend_role etc + type: keyword + type: + type: keyword + notebook: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword + savedQuery: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword + savedVisualization: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword + operationalPanel: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword + timestamp: + type: object + properties: + name: + type: text + fields: + keyword: + type: keyword \ No newline at end of file diff --git a/opensearch-observability/src/main/resources/observability-settings.yml b/opensearch-observability/src/main/resources/observability-settings.yml new file mode 100644 index 000000000..1a3ea0a38 --- /dev/null +++ b/opensearch-observability/src/main/resources/observability-settings.yml @@ -0,0 +1,33 @@ +--- +## +# SPDX-License-Identifier: Apache-2.0 +# +# The OpenSearch Contributors require contributions made to +# this file be licensed under the Apache-2.0 license or a +# compatible open source license. +# +# Modifications Copyright OpenSearch Contributors. See +# GitHub history for details. +## + +## +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. +# +## + +# Settings file for the observability index +index: + number_of_shards: "1" + auto_expand_replicas: "0-1" + number_of_replicas: "0" diff --git a/public/plugin.ts b/public/plugin.ts deleted file mode 100644 index aea8f4660..000000000 --- a/public/plugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '../../../src/core/public'; -import { - observabilityID, - observabilityPluginOrder, - observabilityTitle, -} from '../common/constants/shared'; -import PPLService from './services/requests/ppl'; -import DSLService from './services/requests/dsl'; -import SavedObjects from './services/saved_objects/event_analytics/saved_objects'; -import { AppPluginStartDependencies, ObservabilitySetup, ObservabilityStart } from './types'; - -export class ObservabilityPlugin implements Plugin { - - public setup(core: CoreSetup): ObservabilitySetup { - - core.application.register({ - id: observabilityID, - title: observabilityTitle, - category: { - id: 'opensearch', - label: 'OpenSearch Plugins', - order: 2000, - }, - order: observabilityPluginOrder, - async mount(params: AppMountParameters) { - const { Observability } = await import('./components/index'); - const [ coreStart, depsStart ] = await core.getStartServices(); - const pplService = new PPLService(coreStart.http); - const dslService = new DSLService(coreStart.http); - const savedObjects = new SavedObjects(coreStart.http); - return Observability( - coreStart, - depsStart as AppPluginStartDependencies, - params, - pplService, - dslService, - savedObjects - ); - }, - }); - - // Return methods that should be available to other plugins - return {}; - } - public start(core: CoreStart): ObservabilityStart { - return {}; - } - public stop() {} -}