├── .config ├── .cprc.json ├── .eslintrc ├── .prettierrc.js ├── Dockerfile ├── README.md ├── jest-setup.js ├── jest.config.js ├── jest │ ├── mocks │ │ ├── fileMock.js │ │ └── react-inlinesvg.tsx │ └── utils.js ├── tsconfig.json ├── types │ └── custom.d.ts └── webpack │ ├── constants.ts │ ├── utils.ts │ └── webpack.config.ts ├── .cprc.json ├── .eslintrc ├── .eslintrc.js ├── .github ├── actions │ └── build │ │ └── action.yml └── workflows │ ├── bundle-stats.yml │ ├── bundle-types.yml │ ├── ci.yml │ ├── is-compatible.yml │ ├── publish-technical-documentation-next.yml │ ├── release.yml │ ├── update-main.yml │ └── update-make-docs.yml ├── .gitignore ├── .levignore.js ├── .nvmrc ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cspell.config.json ├── cypress.json ├── devenv ├── docker-compose.e2e.yaml ├── docker-compose.yaml └── tempo.yaml ├── docs ├── Makefile ├── README.md ├── docs.mk ├── make-docs ├── sources │ ├── _index.md │ ├── access │ │ └── index.md │ ├── choose-a-view │ │ └── index.md │ ├── concepts │ │ └── _index.md │ ├── determine-use-case │ │ └── index.md │ ├── explore-traces-homescreen.png │ ├── get-started │ │ └── _index.md │ ├── images │ │ ├── explore-traces-errors-add-filters-flow.png │ │ ├── explore-traces-errors-metric-flow.png │ │ ├── explore-traces-errors-rcause-menu.png │ │ ├── explore-traces-errors-root-cause.png │ │ ├── explore-traces-exemplar-v2.4.png │ │ ├── explore-traces-exemplars-trace-v2.4.png │ │ ├── explore-traces-filters.png │ │ ├── explore-traces-select-signal-errors.gif │ │ └── explore-traces-select-signal.png │ ├── investigate │ │ ├── _index.md │ │ ├── add-filters.md │ │ ├── analyze-tracing-data.md │ │ ├── choose-red-metric.md │ │ ├── choose-span-data.md │ │ └── view-exemplars.md │ └── ui-reference │ │ └── _index.md └── variables.mk ├── e2e ├── components.spec.ts ├── fixtures │ └── explore.ts └── navigation.spec.ts ├── jest-setup.js ├── jest.config.js ├── package.json ├── playwright.config.ts ├── provisioning ├── README.md ├── datasources │ └── default.yaml └── plugins │ └── app.yaml ├── src ├── README.md ├── components │ ├── App │ │ └── App.tsx │ ├── AppConfig │ │ └── AppConfig.tsx │ ├── Explore │ │ ├── ByFrameRepeater.test.ts │ │ ├── ByFrameRepeater.tsx │ │ ├── GroupBySelector.tsx │ │ ├── LayoutSwitcher.test.tsx │ │ ├── LayoutSwitcher.tsx │ │ ├── Search.tsx │ │ ├── StreamingIndicator.tsx │ │ ├── TraceQLIssueDetector.tsx │ │ ├── TracesByService │ │ │ ├── DurationComparisonControl.tsx │ │ │ ├── MiniREDPanel.tsx │ │ │ ├── REDPanel.tsx │ │ │ ├── Tabs │ │ │ │ ├── Breakdown │ │ │ │ │ ├── AttributesBreakdownScene.tsx │ │ │ │ │ ├── AttributesDescription.tsx │ │ │ │ │ └── BreakdownScene.tsx │ │ │ │ ├── Comparison │ │ │ │ │ ├── AttributesComparisonScene.tsx │ │ │ │ │ └── ComparisonScene.tsx │ │ │ │ ├── Spans │ │ │ │ │ ├── SpanListColumnsSelector.test.tsx │ │ │ │ │ ├── SpanListColumnsSelector.tsx │ │ │ │ │ ├── SpanListScene.tsx │ │ │ │ │ └── SpansScene.tsx │ │ │ │ ├── Structure │ │ │ │ │ └── StructureScene.tsx │ │ │ │ └── TabsBarScene.tsx │ │ │ ├── TraceDrawerScene.tsx │ │ │ ├── TracesByServiceScene.test.tsx │ │ │ └── TracesByServiceScene.tsx │ │ ├── actions │ │ │ ├── AddToFiltersAction.test.tsx │ │ │ ├── AddToFiltersAction.tsx │ │ │ ├── AddToInvestigationButton.tsx │ │ │ ├── InspectAttributeAction.tsx │ │ │ ├── ShareExplorationAction.test.tsx │ │ │ └── ShareExplorationAction.tsx │ │ ├── behaviors │ │ │ └── syncYaxis.tsx │ │ ├── layouts │ │ │ ├── HighestDifferencePanel.tsx │ │ │ ├── allComparison.ts │ │ │ ├── attributeBreakdown.ts │ │ │ └── attributeComparison.ts │ │ ├── panels │ │ │ ├── PanelMenu.tsx │ │ │ ├── TraceViewPanelScene.tsx │ │ │ ├── barsPanel.ts │ │ │ ├── histogram.ts │ │ │ └── linesPanel.ts │ │ └── queries │ │ │ ├── StepQueryRunner.ts │ │ │ ├── comparisonQuery.ts │ │ │ ├── generateMetricsQuery.test.ts │ │ │ ├── generateMetricsQuery.ts │ │ │ ├── histogram.ts │ │ │ └── queries.test.ts │ ├── Home │ │ ├── AttributePanel.tsx │ │ ├── AttributePanelRow.test.tsx │ │ ├── AttributePanelRow.tsx │ │ ├── AttributePanelRows.test.tsx │ │ ├── AttributePanelRows.tsx │ │ ├── AttributePanelScene.tsx │ │ ├── ErroredServicesRows.tsx │ │ ├── HeaderScene.tsx │ │ ├── SlowestServicesRows.tsx │ │ └── SlowestTracesRows.tsx │ ├── Routes │ │ ├── Routes.tsx │ │ └── index.tsx │ └── states │ │ ├── EmptyState │ │ ├── EmptyState.tsx │ │ ├── EmptyStateScene.tsx │ │ ├── GrotNotFound.tsx │ │ ├── img │ │ │ ├── grot-404-dark.svg │ │ │ └── grot-404-light.svg │ │ └── useMousePosition.ts │ │ ├── ErrorState │ │ └── ErrorStateScene.tsx │ │ └── LoadingState │ │ └── LoadingStateScene.tsx ├── exposedComponents │ ├── EmbeddedTraceExploration │ │ └── EmbeddedTraceExploration.tsx │ ├── OpenInExploreTracesButton │ │ └── OpenInExploreTracesButton.tsx │ ├── index.tsx │ └── types.ts ├── img │ ├── errors-metric-flow.png │ ├── errors-root-cause.png │ ├── histogram-breakdown.png │ └── logo.svg ├── module.tsx ├── pages │ ├── Explore │ │ ├── AttributeFiltersVariable.tsx │ │ ├── PrimarySignalVariable.tsx │ │ ├── SmartDrawer.tsx │ │ ├── TraceExploration.tsx │ │ ├── TraceExplorationPage.tsx │ │ ├── index.tsx │ │ └── primary-signals.ts │ └── Home │ │ ├── Home.tsx │ │ ├── HomePage.tsx │ │ ├── bookmarks │ │ ├── BookmarkIcon.tsx │ │ ├── BookmarkItem.test.tsx │ │ ├── BookmarkItem.tsx │ │ ├── Bookmarks.test.tsx │ │ ├── Bookmarks.tsx │ │ ├── utils.test.ts │ │ └── utils.ts │ │ ├── utils.test.ts │ │ └── utils.ts ├── plugin.json ├── types.ts └── utils │ ├── analytics.ts │ ├── comparison.test.ts │ ├── comparison.ts │ ├── dates.test.ts │ ├── dates.ts │ ├── exemplars.ts │ ├── filters-renderer.test.ts │ ├── filters-renderer.ts │ ├── frames.ts │ ├── links.test.ts │ ├── links.ts │ ├── rockets.tsx │ ├── shared.ts │ ├── testIds.ts │ ├── trace-merge │ ├── merge.test.ts │ ├── merge.ts │ ├── test-responses │ │ └── service-struct.json │ ├── tree-node.ts │ └── utils.ts │ └── utils.ts ├── tsconfig-for-bundle-types.json ├── tsconfig.json ├── webpack.config.ts └── yarn.lock /.config/.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.5.0" 3 | } 4 | -------------------------------------------------------------------------------- /.config/.eslintrc: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config 6 | */ 7 | { 8 | "extends": ["@grafana/eslint-config"], 9 | "root": true, 10 | "rules": { 11 | "react/prop-types": "off" 12 | }, 13 | "overrides": [ 14 | { 15 | "plugins": ["deprecation"], 16 | "files": ["src/**/*.{ts,tsx}"], 17 | "rules": { 18 | "deprecation/deprecation": "warn" 19 | }, 20 | "parserOptions": { 21 | "project": "./tsconfig.json" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.config/.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | module.exports = { 8 | endOfLine: 'auto', 9 | printWidth: 120, 10 | trailingComma: 'es5', 11 | semi: true, 12 | jsxSingleQuote: false, 13 | singleQuote: true, 14 | useTabs: false, 15 | tabWidth: 2, 16 | }; 17 | -------------------------------------------------------------------------------- /.config/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG grafana_version=latest 2 | ARG grafana_image=grafana-enterprise 3 | 4 | FROM grafana/${grafana_image}:${grafana_version} 5 | 6 | # Make it as simple as possible to access the grafana instance for development purposes 7 | # Do NOT enable these settings in a public facing / production grafana instance 8 | ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" 9 | ENV GF_AUTH_ANONYMOUS_ENABLED "true" 10 | ENV GF_AUTH_BASIC_ENABLED "false" 11 | # Set development mode so plugins can be loaded without the need to sign 12 | ENV GF_DEFAULT_APP_MODE "development" 13 | 14 | # Inject livereload script into grafana index.html 15 | USER root 16 | RUN sed -i 's|||g' /usr/share/grafana/public/views/index.html 17 | -------------------------------------------------------------------------------- /.config/jest-setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | import '@testing-library/jest-dom'; 9 | 10 | // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom 11 | Object.defineProperty(global, 'matchMedia', { 12 | writable: true, 13 | value: jest.fn().mockImplementation((query) => ({ 14 | matches: false, 15 | media: query, 16 | onchange: null, 17 | addListener: jest.fn(), // deprecated 18 | removeListener: jest.fn(), // deprecated 19 | addEventListener: jest.fn(), 20 | removeEventListener: jest.fn(), 21 | dispatchEvent: jest.fn(), 22 | })), 23 | }); 24 | 25 | HTMLCanvasElement.prototype.getContext = () => {}; 26 | -------------------------------------------------------------------------------- /.config/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | const path = require('path'); 9 | const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils'); 10 | 11 | module.exports = { 12 | moduleNameMapper: { 13 | '\\.(css|scss|sass)$': 'identity-obj-proxy', 14 | 'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'), 15 | }, 16 | modulePaths: ['/src'], 17 | setupFilesAfterEnv: ['/jest-setup.js'], 18 | testEnvironment: 'jest-environment-jsdom', 19 | testMatch: [ 20 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 21 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 22 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 23 | ], 24 | transform: { 25 | '^.+\\.(t|j)sx?$': [ 26 | '@swc/jest', 27 | { 28 | sourceMaps: 'inline', 29 | jsc: { 30 | parser: { 31 | syntax: 'typescript', 32 | tsx: true, 33 | decorators: false, 34 | dynamicImport: true, 35 | }, 36 | }, 37 | }, 38 | ], 39 | }, 40 | // Jest will throw `Cannot use import statement outside module` if it tries to load an 41 | // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest 42 | transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], 43 | }; 44 | -------------------------------------------------------------------------------- /.config/jest/mocks/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /.config/jest/mocks/react-inlinesvg.tsx: -------------------------------------------------------------------------------- 1 | // Due to the grafana/ui Icon component making fetch requests to 2 | // `/public/img/icon/.svg` we need to mock react-inlinesvg to prevent 3 | // the failed fetch requests from displaying errors in console. 4 | 5 | import React from 'react'; 6 | 7 | type Callback = (...args: any[]) => void; 8 | 9 | export interface StorageItem { 10 | content: string; 11 | queue: Callback[]; 12 | status: string; 13 | } 14 | 15 | export const cacheStore: { [key: string]: StorageItem } = Object.create(null); 16 | 17 | const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/; 18 | 19 | const InlineSVG = ({ src }: { src: string }) => { 20 | // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`) 21 | const testId = src.replace(SVG_FILE_NAME_REGEX, '$2'); 22 | return ; 23 | }; 24 | 25 | export default InlineSVG; 26 | -------------------------------------------------------------------------------- /.config/jest/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | /* 8 | * This utility function is useful in combination with jest `transformIgnorePatterns` config 9 | * to transform specific packages (e.g.ES modules) in a projects node_modules folder. 10 | */ 11 | const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`; 12 | 13 | // Array of known nested grafana package dependencies that only bundle an ESM version 14 | const grafanaESModules = [ 15 | '.pnpm', // Support using pnpm symlinked packages 16 | '@grafana/schema', 17 | 'd3', 18 | 'd3-color', 19 | 'd3-force', 20 | 'd3-interpolate', 21 | 'd3-scale-chromatic', 22 | 'ol', 23 | 'react-colorful', 24 | 'rxjs', 25 | 'uuid', 26 | ]; 27 | 28 | module.exports = { 29 | nodeModulesToTransform, 30 | grafanaESModules, 31 | }; 32 | -------------------------------------------------------------------------------- /.config/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config 6 | */ 7 | { 8 | "compilerOptions": { 9 | "alwaysStrict": true, 10 | "declaration": false, 11 | "rootDir": "../src", 12 | "baseUrl": "../src", 13 | "typeRoots": ["../node_modules/@types"], 14 | "resolveJsonModule": true 15 | }, 16 | "ts-node": { 17 | "compilerOptions": { 18 | "module": "commonjs", 19 | "target": "es5", 20 | "esModuleInterop": true 21 | }, 22 | "transpileOnly": true 23 | }, 24 | "include": ["../src", "./types"], 25 | "extends": "@grafana/tsconfig" 26 | } 27 | -------------------------------------------------------------------------------- /.config/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | // Image declarations 2 | declare module '*.gif' { 3 | const src: string; 4 | export default src; 5 | } 6 | 7 | declare module '*.jpg' { 8 | const src: string; 9 | export default src; 10 | } 11 | 12 | declare module '*.jpeg' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.png' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.webp' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.svg' { 28 | const content: string; 29 | export default content; 30 | } 31 | 32 | // Font declarations 33 | declare module '*.woff'; 34 | declare module '*.woff2'; 35 | declare module '*.eot'; 36 | declare module '*.ttf'; 37 | declare module '*.otf'; 38 | -------------------------------------------------------------------------------- /.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /.config/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { glob } from 'glob'; 6 | import { SOURCE_DIR } from './constants'; 7 | 8 | export function isWSL() { 9 | if (process.platform !== 'linux') { 10 | return false; 11 | } 12 | 13 | if (os.release().toLowerCase().includes('microsoft')) { 14 | return true; 15 | } 16 | 17 | try { 18 | return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | export function getPackageJson() { 25 | return require(path.resolve(process.cwd(), 'package.json')); 26 | } 27 | 28 | export function getPluginJson() { 29 | return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); 30 | } 31 | 32 | export function hasReadme() { 33 | return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); 34 | } 35 | 36 | // Support bundling nested plugins by finding all plugin.json files in src directory 37 | // then checking for a sibling module.[jt]sx? file. 38 | export async function getEntries(): Promise> { 39 | const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); 40 | 41 | const plugins = await Promise.all( 42 | pluginsJson.map((pluginJson) => { 43 | const folder = path.dirname(pluginJson); 44 | return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); 45 | }) 46 | ); 47 | 48 | return plugins.reduce((result, modules) => { 49 | return modules.reduce((result, module) => { 50 | const pluginPath = path.dirname(module); 51 | const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); 52 | const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; 53 | 54 | result[entryName] = module; 55 | return result; 56 | }, result); 57 | }, {}); 58 | } 59 | -------------------------------------------------------------------------------- /.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "useReactRouterV6": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/.eslintrc" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['./.config/.eslintrc'], 3 | rules: { 4 | 'react-hooks/rules-of-hooks': 'off', // Temporarily disable the hooks rule 5 | 'react-hooks/exhaustive-deps': 'warn', 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: 'module', 10 | ecmaFeatures: { 11 | jsx: true, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/bundle-stats.yml: -------------------------------------------------------------------------------- 1 | name: Bundle Stats 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | push: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | actions: read 16 | 17 | jobs: 18 | compare: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - uses: grafana/plugin-actions/bundle-size@main 26 | -------------------------------------------------------------------------------- /.github/workflows/bundle-types.yml: -------------------------------------------------------------------------------- 1 | name: Bundle Types 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Run workflow on version tags, e.g. v1.0.0. 7 | 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # These permissions are needed to assume roles from Github's OIDC. 12 | permissions: 13 | contents: read 14 | id-token: write 15 | 16 | jobs: 17 | bundle-types: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | 24 | - uses: grafana/plugin-actions/bundle-types@main 25 | with: 26 | entry-point: ./src/exposedComponents/types.ts 27 | ts-config: ./tsconfig-for-bundle-types.json 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | persist-credentials: false 15 | - name: Call shared build action 16 | uses: ./.github/actions/build 17 | -------------------------------------------------------------------------------- /.github/workflows/is-compatible.yml: -------------------------------------------------------------------------------- 1 | name: Latest Grafana API compatibility check 2 | on: [pull_request] 3 | 4 | jobs: 5 | compatibilitycheck: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | persist-credentials: false 11 | - name: Setup Node.js environment 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: '20' 15 | cache: 'yarn' 16 | 17 | - name: Restore npm cache 18 | id: restore-npm-cache 19 | uses: actions/cache/restore@v4 20 | with: 21 | path: | 22 | node_modules 23 | key: ${{ runner.os }}-npm-${{ hashFiles('**/yarn.lock', '!node_modules/**/yarn.lock') }} 24 | restore-keys: ${{ runner.os }}-npm- 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Save npm cache 30 | id: save-npm-cache 31 | if: steps.restore-npm-cache.outputs.cache-hit != 'true' 32 | uses: actions/cache/save@v4 33 | with: 34 | path: | 35 | node_modules 36 | key: ${{ steps.restore-npm-cache.outputs.cache-primary-key }} 37 | - name: Build plugin 38 | run: npm run build 39 | - name: Compatibility check 40 | run: npx @grafana/levitate@latest is-compatible --path src/module.tsx --target @grafana/data,@grafana/ui,@grafana/runtime 41 | -------------------------------------------------------------------------------- /.github/workflows/publish-technical-documentation-next.yml: -------------------------------------------------------------------------------- 1 | name: publish-technical-documentation-next 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "docs/sources/**" 9 | workflow_dispatch: 10 | jobs: 11 | sync: 12 | if: github.repository == 'grafana/traces-drilldown' 13 | permissions: 14 | contents: read 15 | id-token: write 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | - uses: grafana/writers-toolkit/publish-technical-documentation@publish-technical-documentation/v1 22 | with: 23 | website_directory: content/docs/explore-traces/next 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub Action automates the process of building Grafana plugins. 2 | # (For more information, see https://github.com/grafana/plugin-actions/blob/main/build-plugin/README.md) 3 | name: Release 4 | 5 | on: 6 | push: 7 | tags: 8 | - 'v*' # Run workflow on version tags, e.g. v1.0.0. 9 | 10 | permissions: 11 | contents: write 12 | id-token: write 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | - id: get-secrets 22 | name: get secrets 23 | uses: grafana/shared-workflows/actions/get-vault-secrets@5d7e361bc7e0a183cde8afe9899fb7b596d2659b 24 | with: 25 | common_secrets: | 26 | GRAFANA_ACCESS_POLICY_TOKEN=plugins/sign-plugin-access-policy-token:token 27 | GCP_UPLOAD_ARTIFACTS_KEY=grafana/integration-artifacts-uploader-service-account:'credentials.json' 28 | - uses: grafana/plugin-actions/build-plugin@main 29 | id: build-release 30 | with: 31 | policy_token: ${{ env.GRAFANA_ACCESS_POLICY_TOKEN }} 32 | - name: Get plugin metadata 33 | id: metadata 34 | run: | 35 | sudo apt-get install jq 36 | 37 | export GRAFANA_PLUGIN_ID=$(cat src/plugin.json | jq -r .id) 38 | export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-latest.zip 39 | 40 | echo "plugin-id=${GRAFANA_PLUGIN_ID}" >> $GITHUB_OUTPUT 41 | echo "archive=${GRAFANA_PLUGIN_ARTIFACT}" >> $GITHUB_OUTPUT 42 | 43 | - id: 'auth' 44 | uses: 'google-github-actions/auth@v2' 45 | with: 46 | credentials_json: ${{ env.GCP_UPLOAD_ARTIFACTS_KEY }} 47 | 48 | - id: 'create-latest' 49 | run: cp "$SOURCE_ARCHIVE" "$TARGET_ARCHIVE" 50 | env: 51 | SOURCE_ARCHIVE: ${{ steps.build-release.outputs.archive }} 52 | TARGET_ARCHIVE: ${{ steps.metadata.outputs.archive }} 53 | 54 | - id: 'upload-to-gcs' 55 | name: 'Upload assets to latest' 56 | uses: 'google-github-actions/upload-cloud-storage@v1' 57 | with: 58 | path: ./ 59 | destination: 'integration-artifacts/grafana-exploretraces-app/' 60 | glob: '*.zip' 61 | parent: false 62 | -------------------------------------------------------------------------------- /.github/workflows/update-main.yml: -------------------------------------------------------------------------------- 1 | name: Build and release main to GCS 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: # Don't run the workflow if only docs files have been changed 9 | - 'docs/**' 10 | - '**.md' 11 | 12 | permissions: 13 | contents: write 14 | id-token: write 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | persist-credentials: false 23 | - id: get-secrets 24 | name: get secrets for build 25 | uses: grafana/shared-workflows/actions/get-vault-secrets@5d7e361bc7e0a183cde8afe9899fb7b596d2659b 26 | with: 27 | common_secrets: | 28 | GRAFANA_ACCESS_POLICY_TOKEN=plugins/sign-plugin-access-policy-token:token 29 | - name: Call shared build action 30 | uses: ./.github/actions/build 31 | with: 32 | policy_token: ${{ env.GRAFANA_ACCESS_POLICY_TOKEN }} 33 | upload: 34 | runs-on: ubuntu-latest 35 | needs: 36 | - build 37 | steps: 38 | - name: Download zip 39 | uses: actions/download-artifact@v4 40 | with: 41 | name: grafana-exploretraces-app-latest.zip 42 | - id: get-secrets 43 | name: get secrets for upload 44 | uses: grafana/shared-workflows/actions/get-vault-secrets@main 45 | with: 46 | common_secrets: | 47 | GCP_UPLOAD_ARTIFACTS_KEY=grafana/integration-artifacts-uploader-service-account:'credentials.json' 48 | - id: 'auth' 49 | uses: 'google-github-actions/auth@v2' 50 | with: 51 | credentials_json: ${{ env.GCP_UPLOAD_ARTIFACTS_KEY }} 52 | - id: 'upload-to-gcs' 53 | name: 'Upload assets to main' 54 | uses: 'google-github-actions/upload-cloud-storage@v1' 55 | with: 56 | path: ./grafana-exploretraces-app-latest.zip 57 | destination: 'integration-artifacts/grafana-exploretraces-app/' 58 | parent: false 59 | -------------------------------------------------------------------------------- /.github/workflows/update-make-docs.yml: -------------------------------------------------------------------------------- 1 | name: Update `make docs` procedure 2 | on: 3 | schedule: 4 | - cron: '0 7 * * 1-5' 5 | workflow_dispatch: 6 | jobs: 7 | main: 8 | if: github.repository == 'grafana/explore-traces' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | persist-credentials: false 14 | - uses: grafana/writers-toolkit/update-make-docs@update-make-docs/v1 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .pnpm-debug.log* 8 | 9 | node_modules/ 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # Compiled binary addons (https://nodejs.org/api/addons.html) 24 | dist/ 25 | artifacts/ 26 | work/ 27 | ci/ 28 | e2e-results/ 29 | **/cypress/videos 30 | **/cypress/report.json 31 | 32 | # Editor 33 | .idea 34 | 35 | .eslintcache 36 | .DS_Store 37 | 38 | # Playwright 39 | playwright-report/ 40 | playwright/ 41 | test-results/ 42 | -------------------------------------------------------------------------------- /.levignore.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | removals: [/getPluginLinkExtensions/, /PluginExtensionLinkConfig/], 3 | }; 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require('./.config/.prettierrc.js'), 4 | }; 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Grafana Traces Drilldown 2 | 3 | The Grafana Traces Drilldown app lets users navigate and visualize trace data stored in Tempo without complex queries. 4 | 5 | We love accepting contributions! 6 | If your change is minor, please feel free submit 7 | a [pull request](https://help.github.com/articles/about-pull-requests/). 8 | If your change is larger, or adds a feature, please file an issue beforehand so 9 | that we can discuss the change. You're welcome to file an implementation pull 10 | request immediately as well, although we generally lean towards discussing the 11 | change and then reviewing the implementation separately. 12 | 13 | ## Contribute to documentation 14 | 15 | Have a great new feature you want to contribute? Add docs for it! 16 | Find something missing in the docs? Update the docs! 17 | 18 | Use the [Writer's Toolkit](https://grafana.com/docs/writers-toolkit/writing-guide/contribute-documentation/) for information on creating good documentation. 19 | The toolkit also provides [document templates](https://github.com/grafana/writers-toolkit/tree/main/docs/static/templates) to help get started. 20 | 21 | When you create a PR for documentation, add the `type/doc` label to identify the PR as contributing documentation. 22 | 23 | To preview the documentation locally, run `make docs` from the root folder of the Traces Drilldown repository. This uses 24 | the `grafana/docs` image which internally uses Hugo to generate the static site. The site is available on `localhost:3002/docs/`. 25 | 26 | > **Note** The `make docs` command uses a lot of memory. If it is crashing, make sure to increase the memory allocated to Docker 27 | > and try again. 28 | -------------------------------------------------------------------------------- /cspell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": [ 3 | "coverage/**", 4 | "cypress/**", 5 | "dist/**", 6 | "go.sum", 7 | "mage_output_file.go", 8 | "node_modules/**", 9 | "provisioning/**/*.yaml", 10 | "src/dashboards/*.json", 11 | "**/testdata/**/*.json", 12 | "**/testdata/**/*.jsonc", 13 | "vendor/**", 14 | "cspell.config.json", 15 | "package.json", 16 | "yarn.lock", 17 | "docker-compose*.yaml", 18 | "docker-compose*.yml", 19 | "devenv" 20 | ], 21 | "ignoreRegExpList": ["import\\s*\\((.|[\r\n])*?\\)", "import\\s*.*\".*?\""], 22 | "words": [ 23 | "grafana", 24 | "datasource", 25 | "datasources", 26 | "data-testid", 27 | "checkoutservice", 28 | "barchart", 29 | "percentunit", 30 | "Yaxis", 31 | "analyse", 32 | "traceql", 33 | "Timerange", 34 | "spss", 35 | "xymark", 36 | "Dataframe", 37 | "Nanos", 38 | "inlinesvg", 39 | "GROUPBY", 40 | "exploretraces", 41 | "polystat", 42 | "volkovlabs", 43 | "Spanset", 44 | "tempopb", 45 | "querysharding", 46 | "queryless", 47 | "unroute", 48 | "gdev", 49 | "lokiexplore", 50 | "viewports", 51 | "Descendents", 52 | "overriden", 53 | "analysing", 54 | "Drilldown", 55 | "combobox", 56 | "Cvalue", 57 | "localblocks", 58 | "grafanacloud", 59 | "OLJCESPC", 60 | "contextualised" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "video": false 3 | } 4 | -------------------------------------------------------------------------------- /devenv/docker-compose.e2e.yaml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | services: 4 | grafana: 5 | container_name: 'grafana-explore-traces-e2e' 6 | build: 7 | context: ../.config 8 | args: 9 | grafana_image: ${GRAFANA_IMAGE:-grafana} 10 | grafana_version: ${GRAFANA_VERSION:-latest} 11 | ports: 12 | - 3001:3000/tcp 13 | volumes: 14 | - ../dist:/var/lib/grafana/plugins/grafana-explore-traces-e2e 15 | - ../provisioning:/etc/grafana/provisioning 16 | 17 | tempo: 18 | image: grafana/tempo:main-3449ef6 19 | command: [ "-config.file=/etc/tempo.yaml" ] 20 | volumes: 21 | - ./tempo.yaml:/etc/tempo.yaml 22 | ports: 23 | - "3200:3200" # tempo 24 | 25 | # A RabbitMQ queue used to send message between the requester and the server microservices. 26 | mythical-queue: 27 | image: rabbitmq:management 28 | restart: always 29 | ports: 30 | - "5672:5672" 31 | - "15672:15672" 32 | healthcheck: 33 | test: rabbitmq-diagnostics check_running 34 | interval: 5s 35 | timeout: 30s 36 | retries: 10 37 | 38 | # A postgres DB used to store data by the API server microservice. 39 | mythical-database: 40 | image: postgres:14.5 41 | restart: always 42 | environment: 43 | POSTGRES_PASSWORD: "mythical" 44 | ports: 45 | - "5432:5432" 46 | 47 | # A microservice that makes requests to the API server microservice. Requests are also pushed onto the mythical-queue. 48 | mythical-requester-A: 49 | image: grafana/intro-to-mltp:mythical-beasts-requester-0.3.1 50 | restart: always 51 | depends_on: 52 | mythical-queue: 53 | condition: service_healthy 54 | mythical-server-A: 55 | condition: service_started 56 | environment: 57 | - MYTHICAL_SERVER_HOST_PORT=mythical-server-A:4000 58 | - NAMESPACE=production 59 | - TRACING_COLLECTOR_HOST=tempo 60 | - TRACING_COLLECTOR_PORT=4317 61 | - OTEL_EXPORTER_OTLP_TRACES_INSECURE=true 62 | - OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.4,region=eu-east 63 | 64 | # The API server microservice. 65 | mythical-server-A: 66 | image: grafana/intro-to-mltp:mythical-beasts-server-0.3.1 67 | restart: always 68 | depends_on: 69 | - mythical-database 70 | environment: 71 | - NAMESPACE=production 72 | - TRACING_COLLECTOR_HOST=tempo 73 | - TRACING_COLLECTOR_PORT=4317 74 | - OTEL_EXPORTER_OTLP_TRACES_INSECURE=true 75 | - OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.5,region=eu-east 76 | 77 | # A microservice that consumes requests from the mythical-queue 78 | mythical-recorder: 79 | image: grafana/intro-to-mltp:mythical-beasts-recorder-latest 80 | restart: always 81 | depends_on: 82 | mythical-queue: 83 | condition: service_healthy 84 | environment: 85 | - NAMESPACE=production 86 | - TRACING_COLLECTOR_HOST=tempo 87 | - TRACING_COLLECTOR_PORT=4317 88 | - OTEL_EXPORTER_OTLP_TRACES_INSECURE=true 89 | - OTEL_RESOURCE_ATTRIBUTES=ip=1.2.3.5 90 | -------------------------------------------------------------------------------- /devenv/tempo.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | http_listen_port: 3200 3 | 4 | distributor: 5 | receivers: # this configuration will listen on all ports and protocols that tempo is capable of. 6 | otlp: 7 | protocols: 8 | http: 9 | grpc: 10 | opencensus: 11 | 12 | compactor: 13 | compaction: 14 | compaction_window: 1h # blocks in this time window will be compacted together 15 | max_block_bytes: 100_000_000 # maximum size of compacted blocks 16 | block_retention: 1h 17 | compacted_block_retention: 10m 18 | 19 | metrics_generator: 20 | traces_storage: 21 | path: /tmp/tempo/generator/traces 22 | registry: 23 | external_labels: 24 | source: tempo 25 | cluster: docker-compose 26 | storage: 27 | path: /tmp/tempo/generator/wal 28 | 29 | storage: 30 | trace: 31 | backend: local # backend configuration to use 32 | block: 33 | bloom_filter_false_positive: .05 # bloom filter false positive rate. lower values create larger filters but fewer false positives 34 | v2_index_downsample_bytes: 1000 # number of bytes per index record 35 | v2_encoding: zstd # block encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd, s2 36 | wal: 37 | path: /tmp/tempo/wal # where to store the wal locally 38 | v2_encoding: snappy # wal encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd, s2 39 | local: 40 | path: /tmp/tempo/blocks 41 | 42 | overrides: 43 | defaults: 44 | metrics_generator: 45 | processors: [local-blocks, span-metrics] 46 | 47 | stream_over_http_enabled: true 48 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | .DELETE_ON_ERROR: 3 | export SHELL := bash 4 | export SHELLOPTS := pipefail:errexit 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rule 7 | 8 | include docs.mk 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Traces Drilldown documentation 2 | 3 | This directory contains the source code for the Grafana Traces Drilldown documentation. 4 | 5 | Some key things to know about the Traces Drilldown documentation source: 6 | 7 | - The docs are written in markdown, specifically the CommonMark flavor of markdown. 8 | - The Grafana docs team uses [Hugo](https://gohugo.io/) to generate the documentation. 9 | - While you can view the documentation in GitHub, GitHub does not render the images or links correctly and cannot render the Hugo specific shortcodes. 10 | 11 | The Docs team has created a [Writers' Toolkit](https://grafana.com/docs/writers-toolkit/) that documents how we write documentation at Grafana Labs. 12 | The Writers' Toolkit contains information about how we structure documentation at Grafana, including [templates](https://github.com/grafana/writers-toolkit/tree/main/docs/static/templates) for different types of topics, information about Hugo shortcodes that extend markdown to add additional features, and information about linters and other tools that we use to write documentation. Writers' Toolkit also includes our [Style Guide](https://grafana.com/docs/writers-toolkit/write/style-guide/). 13 | 14 | ## Contributing 15 | 16 | The Traces Drilldown documentation is written using the CommonMark flavor of markdown, including some extended features. 17 | For more information about markdown, you can see the [CommonMark specification](https://spec.commonmark.org/), and a [quick reference guide](https://commonmark.org/help/) for CommonMark. 18 | 19 | If you have a GitHub account and you're just making a small fix, for example fixing a typo or updating an example, you can edit the topic in GitHub. 20 | 21 | 1. Find the topic in the explore-traces repo. 22 | 2. Click the pencil icon. 23 | 3. Enter your changes. 24 | 4. Click **Commit changes**. GitHub creates a pull request for you. 25 | 5. If this is your first contribution to this repository, you will need to sign the Contributor License Agreement (CLA) before your PR can be accepted. 26 | 27 | Note that in Hugo the structure of the documentation is based on the folder structure of the documentation repository. The URL structure is generated based on the folder structure and file names. 28 | -------------------------------------------------------------------------------- /docs/sources/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | cascade: 3 | FULL_PRODUCT_NAME: Grafana Traces Drilldown 4 | PRODUCT_NAME: Traces Drilldown 5 | description: Learn about traces and how you can investigate tracing data with Grafana Traces Drilldown to understand and troubleshoot 6 | your application and services. 7 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/ 8 | keywords: 9 | - Explore Traces 10 | - Traces Drilldown 11 | title: Traces Drilldown 12 | menuTitle: Traces Drilldown 13 | weight: 100 14 | refs: 15 | tempo-data-source: 16 | - pattern: /docs/grafana/ 17 | destination: /docs/grafana//datasources/tempo/ 18 | - pattern: /docs/grafana-cloud/ 19 | destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/ 20 | hero: 21 | title: Traces Drilldown 22 | level: 1 23 | width: 100 24 | height: 100 25 | description: Use Traces Drilldown to investigate and identify issues using tracing data. 26 | cards: 27 | title_class: pt-0 lh-1 28 | items: 29 | - title: Get started 30 | href: ./get-started/ 31 | description: How do you use tracing data to investigate an issue? Start here. 32 | height: 24 33 | - title: Access or install 34 | href: ./access/ 35 | description: Access or install Traces Drilldown. 36 | height: 24 37 | - title: Concepts 38 | href: ./concepts/ 39 | description: Learn the concepts you need to use tracing. 40 | height: 24 41 | - title: Investigate trends and spikes 42 | href: ./investigate/ 43 | description: Use your tracing data to identify issues and determine the root cause. 44 | height: 24 45 | - title: Changelog 46 | href: https://github.com/grafana/explore-traces/blob/main/CHANGELOG.md 47 | description: Learn about the updates, new features, and bugfixes in this version. 48 | height: 24 49 | --- 50 | 51 | # Traces Drilldown 52 | 53 | Distributed traces provide a way to monitor applications by tracking requests across services. 54 | Traces record the details of a request to help understand why an issue is or was happening. 55 | 56 | Grafana Traces Drilldown helps you visualize insights from your Tempo traces data. 57 | Using the app, you can: 58 | 59 | * Use Rate, Errors, and Duration (RED) metrics derived from traces to investigate issues 60 | * Uncover related issues and monitor changes over time 61 | * Browse automatic visualizations of your data based on its characteristics 62 | * Do all of this without writing TraceQL queries 63 | 64 | {{< docs/shared source="grafana" lookup="plugins/rename-note.md" version="" >}} 65 | 66 | To learn more, read: 67 | * [From multi-line queries to no-code investigations: meeting Grafana users where they are](https://grafana.com/blog/2024/10/22/from-multi-line-queries-to-no-code-investigations-meeting-grafana-users-where-they-are/) 68 | * [A queryless experience for exploring metrics, logs, traces, and profiles: Introducing the Explore apps suite for Grafana](https://grafana.com/blog/2024/09/24/queryless-metrics-logs-traces-profiles/). 69 | 70 | {{< youtube id="a3uB1C2oHA4" >}} 71 | 72 | ## Explore 73 | 74 | {{< card-grid key="cards" type="simple" >}} -------------------------------------------------------------------------------- /docs/sources/access/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Access or install Traces Drilldown. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/tempo/access/ 4 | keywords: 5 | - Install 6 | - Configure 7 | - Traces Drilldown 8 | menuTitle: Access or install 9 | weight: 150 10 | refs: 11 | tempo-data-source: 12 | - pattern: /docs/grafana/ 13 | destination: /docs/grafana//datasources/tempo/ 14 | - pattern: /docs/grafana-cloud/ 15 | destination: /docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/ 16 | --- 17 | 18 | # Access or install Traces Drilldown 19 | 20 | You can access Grafana Traces Drilldown using any of these: 21 | 22 | - [Grafana Cloud](#set-up-in-grafana-cloud): The easiest method, since no setup or installation is required. 23 | - Self-managed [Grafana](#set-up-in-self-managed-grafana) open source or Enterprise: You must install the Traces Drilldown plugin. 24 | 25 | Traces Drilldown requires Grafana Tempo 2.6 or later with [TraceQL metrics configured](https://grafana.com/docs/tempo//operations/traceql-metrics/). 26 | 27 | ## Set up in Grafana Cloud 28 | 29 | To use Traces Drilldown with Grafana Cloud, you need the following: 30 | 31 | - Grafana Cloud account 32 | - Grafana stack in Grafana Cloud receiving tracing data from your stack's default [Hosted Traces](https://grafana.com/docs/grafana-cloud/send-data/traces/) data source or a [Tempo data source](https://grafana.com/docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source/) 33 | 34 | ## Set up in self-managed Grafana 35 | 36 | To use Traces Drilldown with self-managed Grafana open source or Grafana Enterprise, you need: 37 | 38 | - Your own Grafana instance running 11.2 or later 39 | - Tempo 2.6 or later with [TraceQL metrics configured](https://grafana.com/docs/tempo//operations/traceql-metrics/) 40 | - Configured [Tempo data source](https://grafana.com/docs/grafana/latest/datasources/tempo/configure-tempo-data-source/) receiving tracing data 41 | 42 | Next, [access Traces Drilldown](#access-traces-drilldown). 43 | 44 | ### Install the Traces Drilldown plugin 45 | 46 | {{< admonition type="note" >}} 47 | If you are using Grafana v12 or later, Traces Drilldown is already installed, go to [Access Traces Drilldown](#access-traces-drilldown). 48 | To check, refer to [Access Traces Drilldown](#access-traces-drilldown). 49 | {{}} 50 | 51 | Traces Drilldown is distributed as a Grafana Plugin. 52 | You can find it in the official [Grafana Plugin Directory](https://grafana.com/grafana/plugins/grafana-exploretraces-app/). 53 | 54 | ### Install in your Grafana instance 55 | 56 | You can install Traces Drilldown in your Grafana instance using `grafana cli`: 57 | 58 | ```shell 59 | grafana cli --pluginUrl=https://storage.googleapis.com/integration-artifacts/grafana-exploretraces-app/grafana-exploretraces-app-latest.zip plugins install grafana-traces-app 60 | ``` 61 | 62 | Alternatively, follow these steps to install Traces Drilldown in Grafana: 63 | 64 | 1. In Grafana, go to **Administration** > **Plugins and data** > **Plugins**. 65 | 2. Search for "Traces Drilldown". 66 | 3. Select Traces Drilldown. 67 | 4. Click **Install**. 68 | 69 | The plugin is automatically activated after installation. 70 | 71 | ### Install in a Docker container 72 | 73 | To install the app in a Docker container, configure the following environment variable: 74 | 75 | ```shell 76 | GF_INSTALL_PLUGINS=https://storage.googleapis.com/integration-artifacts/grafana-exploretraces-app/grafana-exploretraces-app-latest.zip;grafana-traces-app 77 | ``` 78 | 79 | ## Access Traces Drilldown 80 | 81 | To access Traces Drilldown, use the following steps: 82 | 83 | 1. Open your Grafana stack in a web browser. 84 | 1. In the main menu, select **Drilldown** > **Traces**. -------------------------------------------------------------------------------- /docs/sources/concepts/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Learn about concepts basic to tracing. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/concepts/ 4 | keywords: 5 | - Traces Drilldown 6 | - Concepts 7 | title: Concepts 8 | menuTitle: Concepts 9 | weight: 200 10 | --- 11 | 12 | # Concepts 13 | 14 | Distributed traces provide a way to monitor applications by tracking requests across services. 15 | Traces record the details of a request to help understand why an issue is or was happening. 16 | 17 | Tracing is best used for analyzing the performance of your system, identifying bottlenecks, monitoring latency, and providing a complete picture of how requests are processed. 18 | 19 | To use the Grafana Traces Drilldown app, you should understand these concepts: 20 | - [Rate, error, and duration metrics](#rate-error-and-duration-metrics) 21 | - [Traces and spans](#traces-and-spans) 22 | 23 | ## Rate, error, and duration metrics 24 | 25 | The Traces Drilldown app lets you explore rate, error, and duration (RED) metrics generated from your traces by Tempo. 26 | 27 | | Useful for investigating | Metric | Meaning | 28 | |---|---|---| 29 | | Unusual spikes in activity | Rate | Number of requests per second | 30 | | Overall issues in your tracing ecosystem | Error | Number of those requests that are failing | 31 | | Response times and latency issues | Duration | Amount of time those requests take, represented as a histogram | 32 | 33 | For more information about the RED method, refer to [The RED Method: how to instrument your services](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/). 34 | 35 | ## Traces and spans 36 | 37 | A trace represents the journey of a request or an action as it moves through all the nodes of a distributed system, especially containerized applications or microservices architectures. 38 | This makes them the ideal observability signal for discovering bottlenecks and interconnection issues. 39 | 40 | Traces are composed of one or more spans. 41 | A span is a unit of work within a trace that has a start time relative to the beginning of the trace, a duration, and an operation name for the unit of work. 42 | It usually has a reference to a parent span in a trace, unless it’s the first span, also known as the root span. 43 | It frequently includes key/value attributes that are relevant to the span itself, for example, the HTTP method used in the request, as well as other metadata such as the service name, sub-span events, or links to other spans. 44 | 45 | For more information, refer to [Use traces to find solutions](https://grafana.com/docs/tempo//introduction/solutions-with-traces/) in the Tempo documentation. 46 | -------------------------------------------------------------------------------- /docs/sources/explore-traces-homescreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/explore-traces-homescreen.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-errors-add-filters-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-errors-add-filters-flow.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-errors-metric-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-errors-metric-flow.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-errors-rcause-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-errors-rcause-menu.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-errors-root-cause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-errors-root-cause.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-exemplar-v2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-exemplar-v2.4.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-exemplars-trace-v2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-exemplars-trace-v2.4.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-filters.png -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-select-signal-errors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-select-signal-errors.gif -------------------------------------------------------------------------------- /docs/sources/images/explore-traces-select-signal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/traces-drilldown/32d678b215ee7163320558da8a829e38b9dd15b3/docs/sources/images/explore-traces-select-signal.png -------------------------------------------------------------------------------- /docs/sources/investigate/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Investigate trends and spikes to identify issues. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/investigate/ 4 | keywords: 5 | - Traces Drilldown 6 | - Investigate 7 | title: Investigate trends and spikes 8 | menuTitle: Investigate trends and spikes 9 | weight: 600 10 | --- 11 | 12 | # Investigate trends and spikes 13 | 14 | Grafana Traces Drilldown provides powerful tools that help you identify and analyze problems in your applications and services. 15 | 16 | Using these steps, you can use the tracing data to investigate issues. 17 | 18 | 1. [Select **Root spans** or **All spans**](./choose-span-data/) to look at either the first span in a trace (the root span) or all span data. 19 | 1. [Choose the metric](./choose-red-metric/) you want to use: rates, errors, or duration. 20 | 1. [Analyze data](./analyze-tracing-data/) using **Breakdown**, **Comparison**, **Service structure** (available for rate), **Root cause errors** (available for errors), **Root cause latency** (available for duration), and **Traces** tabs. 21 | 1. [Add filters](./add-filters/) to refine the view of your data. 22 | 23 | You can use these steps in any order and move between them as many times as needed. 24 | Depending on what you find, you may start with root spans, delve into error data, and then select **All spans** to access all of the tracing data. 25 | 26 | {{< docs/play title="the Grafana Play site" url="https://play.grafana.org/a/grafana-exploretraces-app/explore" >}} 27 | -------------------------------------------------------------------------------- /docs/sources/investigate/add-filters.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Investigate trends and spikes to identify issues. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/investigate/add-filters/ 4 | keywords: 5 | - Traces Drilldown 6 | - Investigate 7 | refs: 8 | use-dashboards: 9 | - pattern: /docs/grafana/ 10 | destination: /docs/grafana//dashboards/use-dashboards/#set-dashboard-time-range 11 | - pattern: /docs/grafana-cloud/ 12 | destination: /docs/grafana-cloud/visualizations/dashboards/use-dashboards/ 13 | title: Add filters 14 | menuTitle: Add filters 15 | weight: 600 16 | --- 17 | 18 | # Refine your investigation using filters 19 | 20 | Use filters to refine your investigation. 21 | 22 | Filters are available on the **Breakdown** and **Comparison** views. 23 | Refer to [Analyze tracing data](../analyze-tracing-data) for how to use these views. 24 | 25 | ## Add filters 26 | 27 | Each time you add a filter, the condition appears in the list of filters at the top of the page. 28 | The list of filters expands as you investigate and explore your tracing data using Traces Drilldown. 29 | 30 | 1. Refine your investigation by adding filters. 31 | 1. Optional: Use the tabs underneath the metrics selection to provide insights into breakdowns, comparisons, latency, and other explorations. 32 | 1. Choose filters to hone in on the problem areas. Each filter that you select adds to the **Filter** bar at the top of the page. You can select filters on the **Comparison** and **Breakdown** tabs in the following ways: 33 | * Select **Add to filters**. 34 | * Use the **Filter** bar near the top. 35 | 36 | ![Change filters for your investigation](/media/docs/explore-traces/traces-drilldown-filters-ga-1.png) 37 | 38 | ### Example 39 | 40 | Let's say that you want to investigate a spike in errored root spans longer than 200ms. 41 | 42 | 1. Select **Root spans**. 43 | 1. Select the **Errored traces** tab. 44 | 1. In the Filter by labeled values, enter `span:duration`, select greater than (`>`) from the drop-down list, and then enter `200ms`. 45 | 1. Once the data updates, sort the **Errored traces** table by the **Duration** column. 46 | 47 | {{< video-embed src="/media/docs/explore-traces/traces-drilldown-errors-root-span-duration-filter.mp4" >}} 48 | 49 | ## Modify a filter 50 | 51 | Selecting an option for a filter automatically updates the displayed data. 52 | If there are no matches, the app displays a “No data for selected query” message. 53 | 54 | To modify an applied filter: 55 | 56 | 1. Select the filter to modify in the filter bar. 57 | 1. Select an option from the drop-down list. 58 | 59 | You can also click in the **Filter** bar to add filters using drop-down lists. 60 | 61 | ## Remove filters 62 | 63 | To remove a filter, select **Remove filter** (**X**) at the end of the filter you want to remove. 64 | 65 | ## Change the time range 66 | 67 | Use the time picker at the top right to modify the data shown in Traces Drilldown. 68 | 69 | You can select a time range of up to 24 hours in duration. 70 | By default, this time range can be any 24-hour period in your configured trace data retention period. 71 | The default retention period is 30 days. 72 | Your configuration may vary from these values. 73 | 74 | For more information about the time range picker, refer to [Use dashboards](ref:use-dashboards). 75 | 76 | -------------------------------------------------------------------------------- /docs/sources/investigate/choose-red-metric.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Choose a rate, error, or duration metric for your investigation. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/investigate/choose-red-metric/ 4 | keywords: 5 | - Traces Drilldown 6 | - Investigate 7 | title: Choose a metric for your investigation 8 | menuTitle: Choose a RED metric 9 | weight: 300 10 | --- 11 | 12 | # Choose a RED metric 13 | 14 | Traces Drilldown uses RED metrics generated from your tracing data to guide your investigation. 15 | In this context, RED metrics mean: 16 | 17 | * **Rates** show the rate of incoming spans per second. 18 | * **Errors** show spans that are failing. 19 | * **Duration** displays the amount of time those spans take; represented as a heat map that shows response time and latency. 20 | 21 | When you select a RED metric, the tabs underneath the metrics selection changes match the context. 22 | For example, selecting **Duration** displays **Root cause latency** and **Slow traces tabs**. 23 | Choosing **Errors** changes the tabs to **Root cause errors** and **Errored traces**. Rate provides **Service structure**, and **Traces** tabs. 24 | These tabs are used when you [analyze tracing data](../analyze-tracing-data). 25 | 26 | {{< video-embed src="/media/docs/explore-traces/traces-drilldown-select-metric-type.mp4" >}} 27 | 28 | To choose a RED metric: 29 | 30 | 1. Select a graph to select a **Spans** (rate), **Errors**, or **Duration** metric type. Notice that your selection changes the first drop-down list on the filter bar. 31 | 1. Optional: Select the signal you want to observe. **Root spans** is the default selection. 32 | 1. Look for spikes or trends in the data to help identify issues. 33 | 34 | {{< admonition type="tip" >}} 35 | If no data or limited data appears, refresh the page. Verify that you have selected the correct data source in the Data source drop-down as well as a valid time range. 36 | {{< /admonition >}} 37 | -------------------------------------------------------------------------------- /docs/sources/investigate/choose-span-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Use root span or full span data for your investigation. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/investigate/choose-span-data/ 4 | keywords: 5 | - Traces Drilldown 6 | - Investigate 7 | title: Choose root or full span data 8 | menuTitle: Choose span data 9 | weight: 200 10 | --- 11 | 12 | # Choose root or full span data 13 | 14 | Tracing data is highly structured and annotated and reflects events that happen in your services. 15 | You can choose the type of services you want to observe and think about. 16 | 17 | By default, Traces Drilldown displays information about root spans. 18 | You can change this by using the selector in the filter bar. 19 | 20 | * Use **Root spans** for trace‑level insights and faster performance (one span/trace). 21 | * Use **All spans** when you need to drill down into every operation within those traces. 22 | 23 | ## Query root spans only 24 | 25 | Using **Root spans**, you get exactly one span per trace (the root span or the first span in a trace) so you see one data point per trace in your results. 26 | 27 | When to use: 28 | - High‑level or service‑level investigations (e.g. error rate by root operation). 29 | - Fast filtering by trace‑wide metrics (e.g. trace duration, success vs. failure at the entry point). 30 | 31 | Benefits: 32 | - End-to-end view: Root spans represent the complete, end‑to‑end request or job. Querying just roots ensures you measure the full request lifecycle, exactly what your RED (Rate, Errors, Duration) metrics are built on. Duration and error‑rate histograms truly reflect user‑facing operations. 33 | - Speed: Only inspects the first span per trace. 34 | 35 | ![The Errors metric view showing Root spans selected](/media/docs/explore-traces/traces-drilldown-errors-root-spans.png) 36 | 37 | ## Query all spans 38 | 39 | With this option you query every matching span in every trace. 40 | 41 | When to use: 42 | - Deep‑dive troubleshooting where you need every operation in the call graph. 43 | - Filtering by child‑span attributes, for example, database calls and background jobs. 44 | 45 | Trade‑offs: 46 | - Skewed RED metrics: Unless used with an appropriate filter, aggregating duration or error rates across every span inflates counts and misrepresents true end‑to‑end performance. Your RED metrics become a mix of server, client, database, and internal spans. The average latency and error rates no longer align with user‑facing operations. 47 | - Performance: Scanning all spans is heavier, especially in wide or deep traces. 48 | - Result size: You may hit maximum spans per span-set limits if your traces are large. 49 | 50 | ![The Errors metric view showing All spans selected](/media/docs/explore-traces/traces-drilldown-errors-all-spans.png) -------------------------------------------------------------------------------- /docs/sources/investigate/view-exemplars.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: View exemplars to explore the links between metrics and spans. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/investigate/view-exemplars/ 4 | keywords: 5 | - Traces Drilldown 6 | - Investigate 7 | title: View exemplars 8 | menuTitle: View exemplars 9 | weight: 600 10 | --- 11 | 12 | # View exemplars 13 | 14 | Exemplars provide a link between the metrics and the traces themselves. 15 | 16 | An exemplar is a specific trace representative of measurement taken in a given time interval. While metrics excel at giving you an aggregated view of your system, traces give you a fine-grained view of a single request; exemplars are a way to link the two. 17 | 18 | Use exemplars to help isolate problems within your data distribution by pinpointing query traces exhibiting high latency within a time interval. 19 | After you localize the latency problem to a few exemplar traces, you can combine it with additional system based information or location properties to perform a root cause analysis faster, leading to quick resolutions to performance issues. 20 | 21 | For more information, refer to [Introduction to exemplars](/docs/grafana//fundamentals/exemplars/). 22 | 23 | ## Exemplars in Traces Drilldown 24 | 25 | In Traces Drilldown, exemplar data is represented by a small diamond next to the bar graphs. 26 | You can view the exemplar information by hovering the cursor over the small diamond. 27 | 28 | ![A small diamond next to the bar graph indicates that exemplar data is available.](/media/docs/explore-traces/explore-traces-exemplar-v2.4.png) 29 | 30 | Select **View trace** to open a slide-out trace panel. 31 | 32 | ![Selecting View trace reveals a slide-out panel with the full trace information.](/media/docs/explore-traces/explore-traces-exemplars-trace-v2.4.png) 33 | -------------------------------------------------------------------------------- /docs/sources/ui-reference/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Learn about the user interface for Traces Drilldown. 3 | canonical: https://grafana.com/docs/grafana/latest/explore/simplified-exploration/traces/ui-reference/ 4 | keywords: 5 | - Traces Drilldown 6 | - UI reference 7 | refs: 8 | use-dashboards-time: 9 | - pattern: /docs/grafana/ 10 | destination: /docs/grafana//dashboards/use-dashboards/#set-dashboard-time-range 11 | - pattern: /docs/grafana-cloud/ 12 | destination: /docs/grafana-cloud/visualizations/dashboards/use-dashboards/#set-dashboard-time-range 13 | title: Traces Drilldown UI reference 14 | menuTitle: UI reference 15 | weight: 600 16 | --- 17 | 18 | # Traces Drilldown UI reference 19 | 20 | Grafana Traces Drilldown helps you focus your tracing data exploration. 21 | Some of the screen sections are context sensitive and change depending upon the metric you've chosen. 22 | Refer to [Analyze tracing data](../investigate/analyze-tracing-data) for more information. 23 | 24 | ![Numbered sections of the Traces Drilldown app](/media/docs/explore-traces/traces-drilldown-screen-ui.png) 25 | 26 | 1. **Data source selection**: 27 | At the top left, you select the data source for your traces. In this example, the data source is set to `grafanacloud-traces`. 28 | 29 | 1. **Filters**: 30 | The filter bar helps you refine the data displayed. 31 | You can select the type of trace data, either **Root spans** or **All spans**. You can also add specific label values to narrow the scope of your investigation. 32 | 33 | 1. **Select metric type**: 34 | Choose between **Rate** (spans), **Errors**, or **Duration** metrics. In this example, the **Span rate** metric is selected, showing the number of spans per second. 35 | - The **Span rate** graph (top left) shows the rate of spans over time. 36 | - The **Errors** graph (top right) displays the error rate over time, with red bars indicating errors. 37 | - The **Duration** heatmap (bottom right) visualizes the distribution of span durations and can help identify latency patterns. 38 | 39 | 1. **Investigation-focused tabs**: 40 | Each metric type has its own set of tabs that help you explore your tracing data. These tabs differ depending on the metric type you've selected. 41 | For example, when you use Span rate, then the Investigation type tabs show **Breakdown**, **Service structure**, **Comparison**, and **Traces**. 42 | 43 | 1. **Add to filters**: 44 | Each attribute group includes an **Add to filters** option, so you can add your selections into the current investigation. 45 | 46 | 1. **Time range selector**: 47 | At the top right, you can adjust the time range for the displayed data using the time picker. In this example, the time range is set to the last 30 minutes. Refer to [Set dashboard time range](https://grafana.com/docs/grafana//dashboards/use-dashboards/#set-dashboard-time-range) for more information. 48 | 49 | ## Streaming query results 50 | 51 | When you first open Traces Drilldown, you may notice a green dot on the upper right corner of any of the metrics graphs. 52 | 53 | This green dot indicates that Traces Drilldown is displaying data that's still being received, or streamed. 54 | Streaming lets you view partial query results before the entire query completes. -------------------------------------------------------------------------------- /docs/variables.mk: -------------------------------------------------------------------------------- 1 | # List of projects to provide to the make-docs script. 2 | # Format is PROJECT[:[VERSION][:[REPOSITORY][:[DIRECTORY]]]] 3 | # The following PROJECTS value mounts content into the explore-logs project, at the "latest" version, which is the default if not explicitly set. 4 | # This results in the content being served at /docs/explore-traces/latest/. 5 | # The source of the content is the current repository which is determined by the name of the parent directory of the git root. 6 | # This overrides the default behavior of assuming the repository directory is the same as the project name. 7 | PROJECTS := explore-traces::$(notdir $(basename $(shell git rev-parse --show-toplevel))) 8 | -------------------------------------------------------------------------------- /e2e/components.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@grafana/plugin-e2e'; 2 | import { ExplorePage } from './fixtures/explore'; 3 | 4 | test.describe('components', () => { 5 | let explorePage: ExplorePage; 6 | 7 | test.beforeEach(async ({ page }) => { 8 | explorePage = new ExplorePage(page); 9 | await explorePage.gotoExplorePage(); 10 | await explorePage.assertNotLoading(); 11 | }); 12 | 13 | test.afterEach(async () => { 14 | await explorePage.unroute(); 15 | }); 16 | 17 | test('in header are visible', async ({ page }) => { 18 | await expect(page.getByText('Data source')).toBeVisible(); 19 | await expect(page.getByRole('button', { name: 'Need help' })).toBeVisible(); 20 | await expect(page.getByTestId('data-testid TimePicker Open Button')).toBeVisible(); 21 | await expect(page.getByTestId('data-testid RefreshPicker run button')).toBeVisible(); 22 | await expect(page.getByTestId('data-testid RefreshPicker interval button')).toBeVisible(); 23 | }); 24 | 25 | test('in filters bar are visible', async ({ page }) => { 26 | await expect(page.getByRole('radio', { name: 'Root spans' })).toBeVisible(); 27 | await expect(page.getByRole('radio', { name: 'All spans' })).toBeVisible(); 28 | await expect(page.getByRole('combobox', { name: 'Filter by label values' })).toBeVisible(); 29 | }); 30 | 31 | test('for RED metrics are visible', async ({ page }) => { 32 | await expect(page.getByText('Span rate')).toBeVisible(); 33 | await expect(page.getByTestId('data-testid Panel header ').locator('canvas')).toBeVisible(); 34 | await expect(page.getByTestId('data-testid Panel header Histogram by duration').locator('canvas')).toBeVisible(); 35 | // TODO: commenting out for now as it's passing fine and looks good when debugging the tests locally but failing in CI for some reason 36 | // await expect(page.getByTestId('data-testid Panel header Errors rate')).toBeVisible(); 37 | }); 38 | 39 | test('for tabs are visible', async ({ page }) => { 40 | await expect(page.getByTestId('data-testid Tab Breakdown')).toBeVisible(); 41 | await expect(page.getByTestId('data-testid Tab Service structure')).toBeVisible(); 42 | await expect(page.getByTestId('data-testid Tab Comparison')).toBeVisible(); 43 | await expect(page.getByTestId('data-testid Tab Traces')).toBeVisible(); 44 | }); 45 | 46 | test('for breakdown tab are visible', async ({ page }) => { 47 | await expect(page.getByText('Attributes are ordered by')).toBeVisible(); 48 | await expect(page.getByText('Scope')).toBeVisible(); 49 | await expect(page.getByRole('radio', { name: 'Resource', exact: true })).toBeVisible(); 50 | await expect(page.getByRole('radio', { name: 'Span', exact: true })).toBeVisible(); 51 | await expect(page.getByText('Group by')).toBeVisible(); 52 | await expect(page.getByLabel('service.name')).toBeVisible(); 53 | await expect( 54 | page 55 | .locator('div') 56 | .filter({ hasText: /^Other attributes$/ }) 57 | .nth(1) 58 | ).toBeVisible(); 59 | await expect(page.getByText('View', { exact: true })).toBeVisible(); 60 | await expect(page.getByLabel('Single')).toBeVisible(); 61 | await expect(page.getByLabel('Grid')).toBeVisible(); 62 | await expect(page.getByLabel('Rows')).toBeVisible(); 63 | await expect(page.getByPlaceholder('Search')).toBeVisible(); 64 | await expect(page.getByRole('heading', { name: 'mythical-requester' })).toBeVisible(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /e2e/fixtures/explore.ts: -------------------------------------------------------------------------------- 1 | import { Locator, Page } from '@playwright/test'; 2 | import pluginJson from '../../src/plugin.json'; 3 | import { expect } from '@grafana/plugin-e2e'; 4 | import { testIds } from '../../src/utils/testIds'; 5 | 6 | export class ExplorePage { 7 | constructor(public readonly page: Page) {} 8 | 9 | async gotoExplorePage() { 10 | await this.page.goto(`/a/${pluginJson.id}/explore`); 11 | } 12 | 13 | async unroute() { 14 | await this.page.unrouteAll({ behavior: 'ignoreErrors' }); 15 | } 16 | 17 | async assertNotLoading() { 18 | const loading = this.page.getByText('Loading'); 19 | await expect(loading).toHaveCount(0); 20 | } 21 | 22 | async assertMissingData() { 23 | await expect(this.page.getByTestId(testIds.emptyState)).not.toBeVisible(); 24 | await expect(this.page.getByTestId(testIds.errorState)).not.toBeVisible(); 25 | await expect(this.page.getByTestId(testIds.loadingState)).not.toBeVisible(); 26 | } 27 | 28 | async click(locator: Locator) { 29 | await expect(locator).toBeVisible(); 30 | await locator.scrollIntoViewIfNeeded(); 31 | await locator.click({ force: true }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /e2e/navigation.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@grafana/plugin-e2e'; 2 | import { ExplorePage } from './fixtures/explore'; 3 | 4 | test.describe('navigating app', () => { 5 | let explorePage: ExplorePage; 6 | 7 | test.beforeEach(async ({ page }) => { 8 | explorePage = new ExplorePage(page); 9 | await explorePage.gotoExplorePage(); 10 | await explorePage.assertNotLoading(); 11 | }); 12 | 13 | test.afterEach(async () => { 14 | await explorePage.unroute(); 15 | }); 16 | 17 | test('explore page should render successfully', async ({ page }) => { 18 | await expect(page.getByText('Data source')).toBeVisible(); 19 | await explorePage.assertMissingData(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | import { TextDecoder, TextEncoder } from 'util'; 2 | 3 | // Jest setup provided by Grafana scaffolding 4 | import './.config/jest-setup'; 5 | 6 | global.TextEncoder = TextEncoder; 7 | global.TextDecoder = TextDecoder; 8 | 9 | // mock the intersection observer and just say everything is in view 10 | const mockIntersectionObserver = jest.fn().mockImplementation((callback) => ({ 11 | observe: jest.fn().mockImplementation((elem) => { 12 | callback([{ target: elem, isIntersecting: true }]); 13 | }), 14 | unobserve: jest.fn(), 15 | disconnect: jest.fn(), 16 | })); 17 | global.IntersectionObserver = mockIntersectionObserver; 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // force timezone to UTC to allow tests to work regardless of local timezone 2 | // generally used by snapshots, but can affect specific tests 3 | process.env.TZ = 'UTC'; 4 | 5 | module.exports = { 6 | // Jest configuration provided by Grafana scaffolding 7 | ...require('./.config/jest.config'), 8 | moduleNameMapper: { 9 | '\\.(css|scss|sass)$': 'identity-obj-proxy', 10 | '\\.(svg|png|jpg|jpeg|gif)$': '/.config/jest/mocks/fileMock.js', // Mock static file imports 11 | resetMocks: true, 12 | clearMocks: true, 13 | resetModules: true, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOptions } from '@grafana/plugin-e2e'; 2 | import { defineConfig, devices } from '@playwright/test'; 3 | import { dirname } from 'node:path'; 4 | 5 | const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`; 6 | 7 | /** 8 | * See https://playwright.dev/docs/test-configuration. 9 | */ 10 | export default defineConfig({ 11 | testDir: './e2e', 12 | /* Run tests in files in parallel */ 13 | fullyParallel: true, 14 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 15 | forbidOnly: !!process.env.CI, 16 | /* Retry on CI only */ 17 | retries: process.env.CI ? 2 : 0, 18 | /* Opt out of parallel tests on CI. */ 19 | workers: process.env.CI ? 1 : undefined, 20 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 21 | reporter: 'html', 22 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 23 | use: { 24 | /* Base URL to use in actions like `await page.goto('/')`. */ 25 | baseURL: 'http://localhost:3001', 26 | 27 | // Record trace only when retrying a test for the first time. 28 | screenshot: 'only-on-failure', 29 | // Record video only when retrying a test for the first time. 30 | video: 'on-first-retry', 31 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 32 | trace: 'on-first-retry', 33 | 34 | // Turn on when debugging local tests 35 | // video: { 36 | // mode: 'on', 37 | // } 38 | }, 39 | expect: { timeout: 15000 }, 40 | 41 | /* Configure projects for major browsers */ 42 | projects: [ 43 | { 44 | name: 'chromium', 45 | use: { ...devices['Desktop Chrome'] }, 46 | }, 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /provisioning/README.md: -------------------------------------------------------------------------------- 1 | For more information see our documentation: 2 | 3 | - [Provision Grafana](https://grafana.com/docs/grafana/latest/administration/provisioning/) 4 | - [Provision a plugin](https://grafana.com/developers/plugin-tools/publish-a-plugin/provide-test-environment) 5 | -------------------------------------------------------------------------------- /provisioning/datasources/default.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: gdev-testdata 5 | isDefault: false 6 | type: testdata 7 | 8 | - name: Tempo 9 | type: tempo 10 | access: proxy 11 | orgId: 1 12 | url: http://tempo:3200 13 | basicAuth: false 14 | isDefault: true 15 | version: 1 16 | editable: false 17 | apiVersion: 1 18 | uid: tempo 19 | -------------------------------------------------------------------------------- /provisioning/plugins/app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | apps: 4 | - type: 'grafana-exploretraces-app' 5 | org_id: 1 6 | org_name: 'Grafana' 7 | disabled: false 8 | jsonData: 9 | apiUrl: http://default-url.com 10 | isApiKeySet: true 11 | secureJsonData: 12 | apiKey: secret-key 13 | -------------------------------------------------------------------------------- /src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppRootProps, PageLayoutType } from '@grafana/data'; 3 | import { AppRoutes } from '../Routes'; 4 | import { PluginPage } from '@grafana/runtime'; 5 | 6 | // This is used to be able to retrieve the root plugin props anywhere inside the app. 7 | const PluginPropsContext = React.createContext(null); 8 | 9 | class App extends React.PureComponent { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/components/Explore/LayoutSwitcher.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import { LayoutSwitcher, LayoutType, LayoutSwitcherState } from './LayoutSwitcher'; 4 | import { SelectableValue } from '@grafana/data'; 5 | import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../utils/analytics'; 6 | import { SceneObject } from '@grafana/scenes'; 7 | 8 | jest.mock('../../utils/analytics', () => ({ 9 | reportAppInteraction: jest.fn(), 10 | USER_EVENTS_ACTIONS: { analyse_traces: { layout_type_changed: 'layout_type_changed' } }, 11 | USER_EVENTS_PAGES: { analyse_traces: 'analyse_traces' }, 12 | })); 13 | 14 | const options: Array> = [ 15 | { label: 'Single', value: 'single' }, 16 | { label: 'Grid', value: 'grid' }, 17 | { label: 'Rows', value: 'rows' }, 18 | ]; 19 | 20 | const layouts = [ 21 | { Component: () =>
Single Layout
, state: {} }, 22 | { Component: () =>
Grid Layout
, state: {} }, 23 | { Component: () =>
Rows Layout
, state: {} }, 24 | ] as unknown as SceneObject[]; 25 | 26 | const initialState: LayoutSwitcherState = { 27 | active: 'single', 28 | layouts, 29 | options, 30 | }; 31 | 32 | describe('LayoutSwitcher', () => { 33 | let layoutSwitcher: LayoutSwitcher; 34 | 35 | beforeEach(() => { 36 | layoutSwitcher = new LayoutSwitcher(initialState); 37 | }); 38 | 39 | it('renders the Selector with correct options and active state', () => { 40 | render(); 41 | 42 | expect(screen.getByText('Single')).toBeInTheDocument(); 43 | expect(screen.getByText('Grid')).toBeInTheDocument(); 44 | expect(screen.getByText('Rows')).toBeInTheDocument(); 45 | }); 46 | 47 | it('changes layout on layout change and reports interaction', () => { 48 | render(); 49 | 50 | fireEvent.click(screen.getByText('Grid')); 51 | 52 | expect(layoutSwitcher.state.active).toBe('grid'); 53 | expect(reportAppInteraction).toHaveBeenCalledWith( 54 | USER_EVENTS_PAGES.analyse_traces, 55 | USER_EVENTS_ACTIONS.analyse_traces.layout_type_changed, 56 | { layout: 'grid' } 57 | ); 58 | }); 59 | 60 | it('calls the correct layout component based on the active state', () => { 61 | const mockOnLayoutChange = jest.fn(); 62 | class TestLayoutSwitcher extends LayoutSwitcher { 63 | public onLayoutChange = mockOnLayoutChange; 64 | } 65 | const model = new TestLayoutSwitcher(initialState) as unknown as LayoutSwitcher; 66 | render(); 67 | 68 | fireEvent.click(screen.getByText('Grid')); 69 | expect(mockOnLayoutChange).toHaveBeenCalledWith('grid'); 70 | }); 71 | 72 | it('renders the correct layout component based on the active state', () => { 73 | layoutSwitcher.setState({ active: 'grid' }); 74 | const { container } = render(); 75 | expect(container.firstChild).not.toBeNull(); 76 | expect(screen.getByText('Grid Layout')).toBeDefined(); 77 | }); 78 | 79 | it('returns null when the active layout option is invalid', () => { 80 | layoutSwitcher.setState({ active: 'invalid' as LayoutType }); 81 | const { container } = render(); 82 | expect(container.firstChild).toBeNull(); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/components/Explore/LayoutSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SelectableValue } from '@grafana/data'; 4 | import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; 5 | import { Field, RadioButtonGroup } from '@grafana/ui'; 6 | import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../utils/analytics'; 7 | 8 | export interface LayoutSwitcherState extends SceneObjectState { 9 | active: LayoutType; 10 | layouts: SceneObject[]; 11 | options: Array>; 12 | } 13 | 14 | export type LayoutType = 'single' | 'grid' | 'rows'; 15 | 16 | export class LayoutSwitcher extends SceneObjectBase { 17 | public Selector({ model }: { model: LayoutSwitcher }) { 18 | const { active, options } = model.useState(); 19 | 20 | return ( 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | public onLayoutChange = (active: LayoutType) => { 28 | this.setState({ active }); 29 | reportAppInteraction(USER_EVENTS_PAGES.analyse_traces, USER_EVENTS_ACTIONS.analyse_traces.layout_type_changed, { 30 | layout: active, 31 | }); 32 | }; 33 | 34 | public static Component = ({ model }: SceneComponentProps) => { 35 | const { layouts, options, active } = model.useState(); 36 | 37 | const index = options.findIndex((o) => o.value === active); 38 | if (index === -1) { 39 | return null; 40 | } 41 | 42 | const layout = layouts[index]; 43 | 44 | return ; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Explore/Search.tsx: -------------------------------------------------------------------------------- 1 | import { Field, Input, Icon, useStyles2 } from "@grafana/ui" 2 | import React from "react" 3 | import { GrafanaTheme2 } from '@grafana/data'; 4 | import { css } from "@emotion/css"; 5 | 6 | type Props = { 7 | searchQuery: string; 8 | onSearchQueryChange: (event: React.ChangeEvent) => void; 9 | } 10 | 11 | export const Search = (props: Props) => { 12 | const styles = useStyles2(getStyles); 13 | const { searchQuery, onSearchQueryChange } = props; 14 | 15 | return ( 16 | 17 | } 20 | value={searchQuery} 21 | onChange={onSearchQueryChange} 22 | id='searchFieldInput' 23 | /> 24 | 25 | ) 26 | } 27 | 28 | function getStyles(theme: GrafanaTheme2) { 29 | return { 30 | searchField: css({ 31 | marginBottom: theme.spacing(1), 32 | }), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Explore/StreamingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GrafanaTheme2 } from '@grafana/data'; 3 | import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; 4 | import { css } from '@emotion/css'; 5 | 6 | interface StreamingIndicatorProps { 7 | isStreaming: boolean; 8 | iconSize?: number; 9 | } 10 | 11 | export const StreamingIndicator = ({ 12 | isStreaming, 13 | iconSize = 14, 14 | }: StreamingIndicatorProps) => { 15 | const styles = useStyles2(getStyles, iconSize); 16 | 17 | if (!isStreaming) { 18 | return null; 19 | } 20 | 21 | return ( 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | const getStyles = (theme: GrafanaTheme2, iconSize: number) => { 29 | return { 30 | streamingIndicator: css({ 31 | width: `${iconSize}px`, 32 | height: `${iconSize}px`, 33 | backgroundColor: theme.colors.success.text, 34 | fill: theme.colors.success.text, 35 | borderRadius: '50%', 36 | display: 'inline-block', 37 | }), 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/Explore/TracesByService/DurationComparisonControl.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { SceneObjectBase, SceneComponentProps, SceneObjectState } from '@grafana/scenes'; 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { Button, useStyles2 } from '@grafana/ui'; 6 | import { css } from '@emotion/css'; 7 | import { getMetricValue, getTraceByServiceScene, shouldShowSelection } from 'utils/utils'; 8 | import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../../utils/analytics'; 9 | import { ComparisonSelection } from '../../../utils/shared'; 10 | 11 | export interface ComparisonControlState extends SceneObjectState { 12 | selection?: ComparisonSelection; 13 | } 14 | 15 | export class DurationComparisonControl extends SceneObjectBase { 16 | public constructor({ selection }: ComparisonControlState) { 17 | super({ selection }); 18 | } 19 | 20 | public startInvestigation = () => { 21 | const byServiceScene = getTraceByServiceScene(this); 22 | byServiceScene.setState({ selection: this.state.selection }); 23 | if (!shouldShowSelection(byServiceScene.state.actionView)) { 24 | byServiceScene.setActionView('comparison'); 25 | } 26 | 27 | reportAppInteraction(USER_EVENTS_PAGES.analyse_traces, USER_EVENTS_ACTIONS.analyse_traces.start_investigation, { 28 | selection: this.state.selection, 29 | metric: getMetricValue(this), 30 | }); 31 | }; 32 | 33 | public static Component = ({ model }: SceneComponentProps) => { 34 | const { selection } = getTraceByServiceScene(model).useState(); 35 | const styles = useStyles2(getStyles); 36 | 37 | const isDisabled = selection?.type === 'auto'; 38 | const tooltip = isDisabled 39 | ? 'Slowest traces are selected, navigate to the Comparison or Slow Traces tab for more details.' 40 | : undefined; 41 | 42 | return ( 43 |
44 | 55 |
56 | ); 57 | }; 58 | } 59 | 60 | function getStyles(theme: GrafanaTheme2) { 61 | return { 62 | wrapper: css({ 63 | display: 'flex', 64 | gap: '16px', 65 | alignItems: 'center', 66 | }), 67 | placeholder: css({ 68 | color: theme.colors.text.secondary, 69 | fontSize: theme.typography.bodySmall.fontSize, 70 | display: 'flex', 71 | gap: theme.spacing.x0_5, 72 | }), 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/components/Explore/TracesByService/Tabs/Breakdown/AttributesDescription.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React from 'react'; 3 | 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { useTheme2 } from '@grafana/ui'; 6 | 7 | type Tag = { 8 | label: string; 9 | color: string; 10 | }; 11 | 12 | type Props = { 13 | description: string; 14 | tags: Tag[]; 15 | }; 16 | 17 | export function AttributesDescription({ description, tags }: Props) { 18 | const theme = useTheme2(); 19 | const styles = getStyles(theme); 20 | 21 | return ( 22 |
23 |
{description}
24 | {tags.length > 0 && 25 | tags.map((tag) => ( 26 |
27 |
28 |
{tag.label}
29 |
30 | ))} 31 |
32 | ); 33 | } 34 | 35 | function getStyles(theme: GrafanaTheme2) { 36 | return { 37 | infoFlex: css({ 38 | display: 'flex', 39 | gap: theme.spacing(2), 40 | alignItems: 'center', 41 | padding: `${theme.spacing(1)} 0 ${theme.spacing(2)} 0`, 42 | }), 43 | tagsFlex: css({ 44 | display: 'flex', 45 | gap: theme.spacing(1), 46 | alignItems: 'center', 47 | }), 48 | tag: css({ 49 | display: 'inline-block', 50 | width: theme.spacing(2), 51 | height: theme.spacing(0.5), 52 | borderRadius: theme.spacing(0.5), 53 | }), 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Explore/TracesByService/Tabs/Breakdown/BreakdownScene.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | SceneComponentProps, 5 | SceneFlexItem, 6 | SceneObject, 7 | SceneObjectBase, 8 | SceneObjectState, 9 | VariableDependencyConfig, 10 | } from '@grafana/scenes'; 11 | import { AttributesBreakdownScene } from './AttributesBreakdownScene'; 12 | import { VAR_METRIC } from '../../../../../utils/shared'; 13 | 14 | interface BreakdownSceneState extends SceneObjectState { 15 | body?: SceneObject; 16 | } 17 | 18 | export class BreakdownScene extends SceneObjectBase { 19 | protected _variableDependency = new VariableDependencyConfig(this, { 20 | variableNames: [VAR_METRIC], 21 | }); 22 | 23 | constructor(state: Partial) { 24 | super({ ...state }); 25 | 26 | this.addActivationHandler(this._onActivate.bind(this)); 27 | } 28 | 29 | private _onActivate() { 30 | this.updateBody(); 31 | } 32 | 33 | private updateBody() { 34 | this.setState({ body: new AttributesBreakdownScene({}) }); 35 | } 36 | 37 | public static Component = ({ model }: SceneComponentProps) => { 38 | const { body } = model.useState(); 39 | return body && ; 40 | }; 41 | } 42 | 43 | export function buildBreakdownScene() { 44 | return new SceneFlexItem({ 45 | body: new BreakdownScene({}), 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Explore/TracesByService/Tabs/Comparison/ComparisonScene.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | SceneComponentProps, 5 | SceneFlexItem, 6 | SceneObject, 7 | SceneObjectBase, 8 | SceneObjectState, 9 | VariableDependencyConfig, 10 | } from '@grafana/scenes'; 11 | import { AttributesComparisonScene } from './AttributesComparisonScene'; 12 | import { MetricFunction, VAR_METRIC } from '../../../../../utils/shared'; 13 | import { getMetricVariable, getTraceByServiceScene } from '../../../../../utils/utils'; 14 | import { getDefaultSelectionForMetric } from '../../../../../utils/comparison'; 15 | 16 | interface ComparisonSceneState extends SceneObjectState { 17 | body?: SceneObject; 18 | } 19 | 20 | export class ComparisonScene extends SceneObjectBase { 21 | protected _variableDependency = new VariableDependencyConfig(this, { 22 | variableNames: [VAR_METRIC], 23 | }); 24 | 25 | constructor(state: Partial) { 26 | super({ ...state }); 27 | 28 | this.addActivationHandler(this._onActivate.bind(this)); 29 | } 30 | 31 | private _onActivate() { 32 | const metricVar = getMetricVariable(this); 33 | const metric = metricVar.getValue() as MetricFunction; 34 | 35 | const tracesByService = getTraceByServiceScene(this); 36 | if (!tracesByService.state.selection) { 37 | const selection = getDefaultSelectionForMetric(metric); 38 | if (selection) { 39 | tracesByService.setState({ selection }); 40 | } 41 | } 42 | 43 | this.updateBody(); 44 | } 45 | 46 | private updateBody() { 47 | this.setState({ body: new AttributesComparisonScene({}) }); 48 | } 49 | 50 | public static Component = ({ model }: SceneComponentProps) => { 51 | const { body } = model.useState(); 52 | return body && ; 53 | }; 54 | } 55 | 56 | export function buildComparisonScene() { 57 | return new SceneFlexItem({ 58 | body: new ComparisonScene({}), 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Explore/TracesByService/Tabs/Spans/SpanListColumnsSelector.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | import React from 'react'; 4 | import { SpanListColumnsSelector } from './SpanListColumnsSelector'; 5 | import { SelectableValue } from '@grafana/data'; 6 | 7 | describe('SpanListColumnsSelector', () => { 8 | const mockOptions: Array> = [ 9 | { label: 'Duration', value: 'duration' }, 10 | { label: 'Start Time', value: 'startTime' }, 11 | { label: 'Tags', value: 'tags' }, 12 | ]; 13 | 14 | const mockOnChange = jest.fn(); 15 | 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('should display "Add extra columns" label', () => { 21 | render(); 22 | 23 | expect(screen.getByText('Add extra columns')).toBeInTheDocument(); 24 | }); 25 | 26 | it('should show placeholder text when no value is selected', () => { 27 | render(); 28 | 29 | expect(screen.getByText('Select an attribute')).toBeInTheDocument(); 30 | }); 31 | 32 | it('should display pre-selected values', async () => { 33 | render(); 34 | 35 | expect(screen.getByText('Duration')).toBeInTheDocument(); 36 | expect(screen.getByText('Tags')).toBeInTheDocument(); 37 | }); 38 | 39 | it('should allow selecting multiple options', async () => { 40 | const user = userEvent.setup(); 41 | 42 | render(); 43 | 44 | // Open the combobox 45 | const combobox = screen.getByRole('combobox'); 46 | await user.click(combobox); 47 | 48 | // Select first option 49 | const durationOption = screen.getByText('Duration'); 50 | await user.click(durationOption); 51 | 52 | expect(mockOnChange).toHaveBeenCalledWith('duration'); 53 | 54 | // Select second option 55 | await user.click(combobox); 56 | const tagsOption = screen.getByText('Tags'); 57 | await user.click(tagsOption); 58 | 59 | expect(mockOnChange).toHaveBeenCalledWith('duration,tags'); 60 | }); 61 | 62 | it('should display all available options when clicking the combobox', async () => { 63 | const user = userEvent.setup(); 64 | 65 | render(); 66 | 67 | const combobox = screen.getByRole('combobox'); 68 | await user.click(combobox); 69 | 70 | mockOptions.forEach((option) => { 71 | if (option.label) { 72 | expect(screen.getByText(option.label)).toBeInTheDocument(); 73 | } 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/Explore/TracesByService/Tabs/Spans/SpanListColumnsSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | import { SelectableValue } from '@grafana/data'; 4 | import { Icon, Select, Field, useStyles2 } from '@grafana/ui'; 5 | import { VariableValue } from '@grafana/scenes'; 6 | import { css } from '@emotion/css'; 7 | 8 | const RECOMMENDED_ATTRIBUTES = [ 9 | 'span.http.method', 10 | 'span.http.request.method', 11 | 'span.http.route', 12 | 'span.http.path', 13 | 'span.http.status_code', 14 | 'span.http.response.status_code' 15 | ]; 16 | 17 | type Props = { 18 | options: Array>; 19 | onChange: (columns: string[]) => void; 20 | value?: VariableValue; 21 | }; 22 | 23 | const labelOrder = ['Recommended', 'Resource', 'Span', 'Other']; 24 | 25 | export function SpanListColumnsSelector({ options, value, onChange }: Props) { 26 | const styles = useStyles2(getStyles); 27 | 28 | const opt = useMemo( 29 | () => 30 | Object.values( 31 | options.reduce((acc, curr) => { 32 | if (curr.label) { 33 | const label = curr.label.slice(curr.label.indexOf('.') + 1); 34 | 35 | // use text until first dot as key 36 | if (RECOMMENDED_ATTRIBUTES.includes(curr.label)) { 37 | const group = acc['recommended'] ?? { label: 'Recommended', options: [] }; 38 | group.options.push({ ...curr, label }); 39 | acc['recommended'] = group; 40 | } else if (curr.label.startsWith('resource.')) { 41 | const group = acc['resource'] ?? { label: 'Resource', options: [] }; 42 | group.options.push({ ...curr, label }); 43 | acc['resource'] = group; 44 | } else { 45 | if (curr.label.startsWith('span.')) { 46 | const group = acc['span'] ?? { label: 'Span', options: [] }; 47 | group.options.push({ ...curr, label }); 48 | acc['span'] = group; 49 | } else { 50 | const group = acc['other'] ?? { label: 'Other', options: [] }; 51 | group.options.push(curr); 52 | acc['other'] = group; 53 | } 54 | } 55 | } 56 | return acc; 57 | }, {}) 58 | ).sort((a, b) => labelOrder.indexOf(a.label) - labelOrder.indexOf(b.label)), 59 | [options] 60 | ); 61 | 62 | return ( 63 |
64 | 65 |