├── .prettierrc.js ├── img └── grafana-riemann-streams.gif ├── src ├── img │ ├── grafana-riemann-streams.png │ └── riemann.svg ├── module.ts ├── plugin.json ├── ConfigEditor.tsx ├── types.ts ├── QueryEditor.tsx └── DataSource.ts ├── tsconfig.json ├── .gitlab-ci.yml ├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── appveyor.yml ├── TODO.md ├── package.json ├── Development.md ├── .circleci └── config.yml ├── .github └── workflows │ └── main.yml ├── README.md └── LICENSE /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"), 3 | }; 4 | -------------------------------------------------------------------------------- /img/grafana-riemann-streams.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxm0dem/grafana-riemann-websocket-datasource/HEAD/img/grafana-riemann-streams.gif -------------------------------------------------------------------------------- /src/img/grafana-riemann-streams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxm0dem/grafana-riemann-websocket-datasource/HEAD/src/img/grafana-riemann-streams.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "rootDir": "./src", 6 | "baseUrl": "./src", 7 | "typeRoots": ["./node_modules/@types"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:14.15.0 2 | 3 | before_script: 4 | - yarn install --cache-folder .yarn 5 | - rm -rf node_modules/@grafana/data/node_modules 6 | 7 | test: 8 | stage: test 9 | cache: 10 | paths: 11 | - node_modules/ 12 | - .yarn 13 | script: 14 | - yarn dev 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*.{js,ts,tsx,scss}] 13 | quote_type = single 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { DataSource } from './DataSource'; 3 | import { ConfigEditor } from './ConfigEditor'; 4 | import { QueryEditor } from './QueryEditor'; 5 | import { MyQuery, MyDataSourceOptions } from './types'; 6 | 7 | export const plugin = new DataSourcePlugin(DataSource) 8 | .setConfigEditor(ConfigEditor) 9 | .setQueryEditor(QueryEditor); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | node_modules/ 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Compiled binary addons (https://nodejs.org/api/addons.html) 23 | dist/ 24 | artifacts/ 25 | work/ 26 | ci/ 27 | e2e-results/ 28 | 29 | # Editor 30 | .idea 31 | 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2022-09-21 Release v0.1.6 4 | 5 | * Improve variable handling by interpolating multiple selections as "regex" 6 | 7 | ## 2021-04-30 Release v0.1.5 8 | 9 | * Add support for events with numeric time (in seconds) 10 | This makes this plugin also compatible with Mirabelle 11 | https://github.com/mcorbin/mirabelle 12 | 13 | ## 2021-01-28 Release v0.1.4 14 | 15 | * Fix time scrolling in Grafana 7.4.x 16 | Make sure event time is converted to millis. 17 | * Fix link to riemann query test suite 18 | * Update installation instructions 19 | 20 | ## 2020-11-20 Release v0.1.3 21 | 22 | * Initial public release 23 | 24 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "10" 4 | 5 | # Local NPM Modules 6 | cache: 7 | - node_modules 8 | 9 | # Install scripts. (runs after repo cloning) 10 | install: 11 | # Get the latest stable version of Node.js or io.js 12 | - ps: Install-Product node $env:nodejs_version 13 | # install modules 14 | - npm install -g yarn --quiet 15 | - yarn install --pure-lockfile 16 | 17 | # Post-install test scripts. 18 | test_script: 19 | # Output useful info for debugging. 20 | - node --version 21 | - npm --version 22 | 23 | # Run the build 24 | build_script: 25 | - yarn dev # This will also run prettier! 26 | - yarn build # make sure both scripts work 27 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Re-use WS connections and 4 | - keep track of the ws 5 | the reference Id of the we must be the query itself 6 | the problem with this is that multiple queries, in same or different 7 | panels might share the same websocket. 8 | - when query changes, close and reopen the ws 9 | - when any other param changes, update the ws.onmessage() only 10 | * Reap WS connections when leaving dash 11 | * Send Alert that any of the limits are reached 12 | See https://developers.grafana.com/ui/latest/index.html?path=/docs/overlays-alert--basic 13 | * Automatic maxseries calculation maybe from Max Data Points calculated from screen size 14 | 15 | # Done 16 | 17 | * Add configurable fields to datasource or query so they appear in grafana naturally 18 | * Limit rate 19 | * Limit # of series 20 | * Configure Limit # of points (CircularDataFrame capacity) 21 | * Configure group by (currently fixed to service) 22 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ccin2p3-riemann-datasource", 3 | "version": "0.1.6", 4 | "description": "Subscribe to riemann.io websocket streams!", 5 | "scripts": { 6 | "build": "grafana-toolkit plugin:build", 7 | "test": "grafana-toolkit plugin:test", 8 | "dev": "grafana-toolkit plugin:dev", 9 | "sign": "grafana-toolkit plugin:sign", 10 | "watch": "grafana-toolkit plugin:dev --watch" 11 | }, 12 | "files": ["Development.md"], 13 | "author": "Fᴀʙɪᴇɴ Wᴇʀɴʟɪ", 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "@grafana/data": "latest", 17 | "@grafana/runtime": "latest", 18 | "@grafana/toolkit": "latest", 19 | "@grafana/ui": "latest", 20 | "@testing-library/jest-dom": "5.4.0", 21 | "@testing-library/react": "^10.0.2", 22 | "@types/lodash": "latest" 23 | }, 24 | "engines": { 25 | "node": ">=12 <15" 26 | }, 27 | "resolutions": { 28 | "rxjs": "6.6.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "datasource", 3 | "name": "Riemann streams", 4 | "id": "ccin2p3-riemann-datasource", 5 | "metrics": true, 6 | "streaming": true, 7 | "info": { 8 | "description": "Subscribe to riemann.io websocket streams!", 9 | "author": { 10 | "name": "Fᴀʙɪᴇɴ Wᴇʀɴʟɪ", 11 | "url": "https://[::1]/" 12 | }, 13 | "keywords": [ 14 | "riemann", 15 | "websocket", 16 | "streaming", 17 | "datasource", 18 | "clojure" 19 | ], 20 | "logos": { 21 | "small": "img/riemann.svg", 22 | "large": "img/riemann.svg" 23 | }, 24 | "links": [ 25 | { 26 | "name": "Website", 27 | "url": "https://github.com/faxm0dem/grafana-riemann-websocket-datasource" 28 | }, 29 | { 30 | "name": "License", 31 | "url": "https://github.com/faxm0dem/grafana-riemann-websocket-datasource" 32 | } 33 | ], 34 | "screenshots": [{"name": "(streams)", "path": "img/grafana-riemann-streams.png"}], 35 | "version": "%VERSION%", 36 | "updated": "%TODAY%" 37 | }, 38 | "dependencies": { 39 | "grafanaDependency": ">=7.0.0", 40 | "plugins": [] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ConfigEditor.tsx: -------------------------------------------------------------------------------- 1 | // vim: expandtab ts=2 2 | import React, { ChangeEvent, PureComponent } from 'react'; 3 | import { LegacyForms } from '@grafana/ui'; 4 | import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; 5 | import { MyDataSourceOptions } from './types'; 6 | 7 | const { FormField } = LegacyForms; 8 | 9 | interface Props extends DataSourcePluginOptionsEditorProps {} 10 | 11 | interface State {} 12 | 13 | export class ConfigEditor extends PureComponent { 14 | onBaseURLChange = (event: ChangeEvent) => { 15 | const { onOptionsChange, options } = this.props; 16 | const jsonData = { 17 | ...options.jsonData, 18 | baseUrl: event.target.value, 19 | }; 20 | onOptionsChange({ ...options, jsonData }); 21 | }; 22 | 23 | render() { 24 | const { options } = this.props; 25 | const { jsonData } = options; 26 | 27 | return ( 28 |
29 |
30 | 36 |
37 |
38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery, DataSourceJsonData } from '@grafana/data'; 2 | 3 | export interface MyQuery extends DataQuery { 4 | queryText?: string; 5 | maxPoints: number; 6 | maxSeries: number; 7 | maxFreq: number; 8 | groupBy?: string[]; 9 | stringFields: string[]; 10 | numberFields: string[]; 11 | webSocket?: WebSocket; 12 | } 13 | 14 | export const defaultQuery: Partial = { 15 | queryText: 'tagged "riemann"', 16 | maxPoints: 100, 17 | maxSeries: 10, 18 | maxFreq: 1.0, 19 | groupBy: ['host', 'service'], 20 | stringFields: ['state'], 21 | numberFields: ['metric'], 22 | }; 23 | 24 | export interface NumberHash { 25 | [details: string]: number; 26 | } 27 | 28 | export interface IwsList { 29 | [details: string]: WebSocket; 30 | } 31 | 32 | // source https://stackoverflow.com/a/11426309/2122722 33 | export const cons = { 34 | log: console.log, 35 | trace: function(...arg: any) {}, 36 | debug: function(...arg: any) {}, 37 | info: function(...arg: any) {}, 38 | warn: console.log, 39 | error: console.log, 40 | }; 41 | 42 | /** 43 | * These are options configured for each DataSource instance 44 | */ 45 | export interface MyDataSourceOptions extends DataSourceJsonData { 46 | baseUrl?: string; 47 | } 48 | 49 | /** 50 | * Value that is used in the backend, but never sent over HTTP to the frontend 51 | */ 52 | export interface MySecureJsonData { 53 | apiKey?: string; 54 | } 55 | -------------------------------------------------------------------------------- /Development.md: -------------------------------------------------------------------------------- 1 | # Grafana Riemann streaming datasource 2 | 3 | [![CircleCI](https://circleci.com/gh/faxm0dem/grafana-riemann-websocket-datasource/tree/master.svg?style=svg)](https://circleci.com/gh/faxm0dem/grafana-riemann-websocket-datasource/tree/master) 4 | 5 | ## Building the plugin 6 | 7 | For building this plugin, there currently is a bug in the toolchain that prevents the correct execution. You need to run `rm -rf node_modules/@grafana/data/node_modules` in order for it to work. See the [github issue](https://github.com/grafana/grafana/issues/28395#issuecomment-714715586) for more information on that subject. 8 | 9 | 1. Install dependencies 10 | ```BASH 11 | yarn install 12 | ``` 13 | 2. Build plugin in development mode or run in watch mode 14 | ```BASH 15 | yarn dev 16 | ``` 17 | or 18 | ```BASH 19 | yarn watch 20 | ``` 21 | 3. Build plugin in production mode 22 | ```BASH 23 | yarn build 24 | ``` 25 | 26 | ## Learn more 27 | - [Grafana developer guide](https://github.com/grafana/grafana/blob/master/contribute/developer-guide.md) 28 | - [Build a streaming data source plugin](https://grafana.com/docs/grafana/latest/developers/plugins/build-a-streaming-data-source-plugin/) 29 | - [Javascript websockets](https://javascript.info/websocket) 30 | - [Grafana dataframe documentation](https://grafana.com/docs/grafana/latest/developers/plugins/data-frames/) 31 | - [Build a data source plugin tutorial](https://grafana.com/tutorials/build-a-data-source-plugin) 32 | - [Grafana UI Library](https://developers.grafana.com/ui) - UI components to help you build interfaces using Grafana Design System 33 | 34 | ## Resources that helped me 35 | 36 | - [Stackoverflow dynamically assign props to object in ts](https://stackoverflow.com/questions/12710905/how-do-i-dynamically-assign-properties-to-an-object-in-typescript) 37 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | parameters: 4 | ssh-fingerprint: 5 | type: string 6 | default: ${GITHUB_SSH_FINGERPRINT} 7 | 8 | aliases: 9 | # Workflow filters 10 | - &filter-only-master 11 | branches: 12 | only: master 13 | - &filter-only-release 14 | branches: 15 | only: /^v[1-9]*[0-9]+\.[1-9]*[0-9]+\.x$/ 16 | 17 | workflows: 18 | plugin_workflow: 19 | jobs: 20 | - build 21 | 22 | executors: 23 | default_exec: # declares a reusable executor 24 | docker: 25 | - image: srclosson/grafana-plugin-ci-alpine:latest 26 | e2e_exec: 27 | docker: 28 | - image: srclosson/grafana-plugin-ci-e2e:latest 29 | 30 | jobs: 31 | build: 32 | executor: default_exec 33 | steps: 34 | - checkout 35 | - restore_cache: 36 | name: restore node_modules 37 | keys: 38 | - build-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} 39 | - run: 40 | name: Install dependencies 41 | command: | 42 | mkdir ci 43 | [ -f ~/project/node_modules/.bin/grafana-toolkit ] || yarn install --frozen-lockfile 44 | - run: 45 | name: Fix toolchain bug 46 | command: | 47 | rm -rf node_modules/@grafana/data/node_modules 48 | - save_cache: 49 | name: save node_modules 50 | paths: 51 | - ~/project/node_modules 52 | key: build-cache-{{ .Environment.CACHE_VERSION }}-{{ checksum "yarn.lock" }} 53 | - run: 54 | name: Build and test frontend 55 | command: ./node_modules/.bin/grafana-toolkit plugin:ci-build 56 | - run: 57 | name: Move results to ci folder 58 | command: ./node_modules/.bin/grafana-toolkit plugin:ci-build --finish 59 | - run: 60 | name: Package distribution 61 | command: | 62 | ./node_modules/.bin/grafana-toolkit plugin:ci-package 63 | - persist_to_workspace: 64 | root: . 65 | paths: 66 | - ci/jobs/package 67 | - ci/packages 68 | - ci/dist 69 | - ci/grafana-test-env 70 | - store_artifacts: 71 | path: ci 72 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Run workflow on version tags, e.g. v1.0.0. 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v2.1.2 17 | with: 18 | node-version: 12.x 19 | 20 | - name: Install dependencies 21 | run: yarn install 22 | 23 | - name: Build plugin 24 | run: yarn build 25 | 26 | - name: Sign plugin 27 | run: yarn run grafana-toolkit plugin:sign 28 | env: 29 | GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} # Requires a Grafana API key from Grafana.com. 30 | 31 | - name: Get plugin information 32 | run: | 33 | sudo apt-get install jq 34 | 35 | export GRAFANA_PLUGIN_ID=$(cat dist/plugin.json | jq -r .id) 36 | export GRAFANA_PLUGIN_VERSION=$(cat dist/plugin.json | jq -r .info.version) 37 | export GRAFANA_PLUGIN_TYPE=$(cat dist/plugin.json | jq -r .type) 38 | export GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip 39 | export GRAFANA_PLUGIN_ARTIFACT_CHECKSUM=${GRAFANA_PLUGIN_ARTIFACT}.md5 40 | 41 | # Output to $GITHUB_ENV to be able to use the variables in next steps. 42 | echo "GRAFANA_PLUGIN_ID=${GRAFANA_PLUGIN_ID}" >> $GITHUB_ENV 43 | echo "GRAFANA_PLUGIN_VERSION=${GRAFANA_PLUGIN_VERSION}" >> $GITHUB_ENV 44 | echo "GRAFANA_PLUGIN_TYPE=${GRAFANA_PLUGIN_TYPE}" >> $GITHUB_ENV 45 | echo "GRAFANA_PLUGIN_ARTIFACT=${GRAFANA_PLUGIN_ARTIFACT}" >> $GITHUB_ENV 46 | echo "GRAFANA_PLUGIN_ARTIFACT_CHECKSUM=${GRAFANA_PLUGIN_ARTIFACT_CHECKSUM}" >> $GITHUB_ENV 47 | 48 | - name: Package plugin 49 | run: | 50 | mv dist $GRAFANA_PLUGIN_ID 51 | zip $GRAFANA_PLUGIN_ARTIFACT $GRAFANA_PLUGIN_ID -r 52 | md5sum $GRAFANA_PLUGIN_ARTIFACT > $GRAFANA_PLUGIN_ARTIFACT_CHECKSUM 53 | 54 | - name: Create release 55 | id: create_release 56 | uses: actions/create-release@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | tag_name: ${{ github.ref }} 61 | release_name: Release ${{ github.ref }} 62 | draft: false 63 | prerelease: false 64 | 65 | - name: Add plugin to release 66 | id: upload-plugin-asset 67 | uses: actions/upload-release-asset@v1 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | upload_url: ${{ steps.create_release.outputs.upload_url }} 72 | asset_path: ./${{ env.GRAFANA_PLUGIN_ARTIFACT }} 73 | asset_name: ${{ env.GRAFANA_PLUGIN_ARTIFACT }} 74 | asset_content_type: application/zip 75 | 76 | - name: Add checksum to release 77 | id: upload-checksum-asset 78 | uses: actions/upload-release-asset@v1 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | with: 82 | upload_url: ${{ steps.create_release.outputs.upload_url }} 83 | asset_path: ./${{ env.GRAFANA_PLUGIN_ARTIFACT_CHECKSUM }} 84 | asset_name: ${{ env.GRAFANA_PLUGIN_ARTIFACT_CHECKSUM }} 85 | asset_content_type: text/plain 86 | 87 | - name: Get checksum 88 | run: | 89 | echo "GRAFANA_PLUGIN_CHECKSUM=$(cat ./${{ env.GRAFANA_PLUGIN_ARTIFACT_CHECKSUM }} | cut -d' ' -f1)" >> $GITHUB_ENV 90 | 91 | - name: Publish to Grafana.com 92 | run: | 93 | echo Publish your plugin to grafana.com/plugins by opening a PR to https://github.com/grafana/grafana-plugin-repository with the following entry: 94 | echo 95 | echo '{ "id": "${{ env.GRAFANA_PLUGIN_ID }}", "type": "${{ env.GRAFANA_PLUGIN_TYPE }}", "url": "https://github.com/${{ github.repository }}", "versions": [ { "version": "${{ env.GRAFANA_PLUGIN_VERSION }}", "commit": "${{ github.sha }}", "url": "https://github.com/${{ github.repository }}", "download": { "any": { "url": "${{ steps.upload-plugin-asset.outputs.browser_download_url }}", "md5": "${{ env.GRAFANA_PLUGIN_CHECKSUM }}" } } } ] }' | jq . 96 | -------------------------------------------------------------------------------- /src/img/riemann.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 67 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/QueryEditor.tsx: -------------------------------------------------------------------------------- 1 | // vim: expandtab ts=2 2 | import defaults from 'lodash/defaults'; 3 | 4 | import React, { ChangeEvent, PureComponent } from 'react'; 5 | import { InlineFieldRow, InlineField, Input } from '@grafana/ui'; 6 | import { QueryEditorProps } from '@grafana/data'; 7 | import { DataSource } from './DataSource'; 8 | import { defaultQuery, MyDataSourceOptions, MyQuery } from './types'; 9 | 10 | // This is a temporary work-around for a styling issue related to the new Input component. 11 | // For more information, refer to https://github.com/grafana/grafana/issues/26512. 12 | import {} from '@emotion/core'; 13 | 14 | type Props = QueryEditorProps; 15 | 16 | export class QueryEditor extends PureComponent { 17 | onQueryTextChange = (event: ChangeEvent) => { 18 | const { onChange, query } = this.props; 19 | onChange({ ...query, queryText: event.target.value }); 20 | }; 21 | onGroupByChange = (event: ChangeEvent) => { 22 | const { onChange, query } = this.props; 23 | onChange({ ...query, groupBy: event.target.value.split(',') }); 24 | }; 25 | onStringFieldsChange = (event: ChangeEvent) => { 26 | const { onChange, query } = this.props; 27 | onChange({ ...query, stringFields: event.target.value.split(',') }); 28 | }; 29 | onNumberFieldsChange = (event: ChangeEvent) => { 30 | const { onChange, query } = this.props; 31 | onChange({ ...query, numberFields: event.target.value.split(',') }); 32 | }; 33 | onMaxPointsChange = (event: ChangeEvent) => { 34 | const { onChange, query, onRunQuery } = this.props; 35 | onChange({ ...query, maxPoints: parseInt(event.target.value, 10) }); 36 | onRunQuery(); 37 | }; 38 | onMaxSeriesChange = (event: ChangeEvent) => { 39 | const { onChange, query, onRunQuery } = this.props; 40 | onChange({ ...query, maxSeries: parseInt(event.target.value, 10) }); 41 | onRunQuery(); 42 | }; 43 | onMaxFreqChange = (event: ChangeEvent) => { 44 | const { onChange, query, onRunQuery } = this.props; 45 | onChange({ ...query, maxFreq: parseFloat(event.target.value) }); 46 | onRunQuery(); 47 | }; 48 | 49 | render() { 50 | const query = defaults(this.props.query, defaultQuery); 51 | const { queryText, maxPoints, maxSeries, maxFreq, groupBy, stringFields, numberFields } = query; 52 | 53 | return ( 54 | <> 55 | 56 | 61 | 62 | 63 | 64 | 65 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | 88 | 89 | 90 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | 106 | 107 | 108 | 109 | 110 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/DataSource.ts: -------------------------------------------------------------------------------- 1 | // vim: expandtab ts=2 2 | 3 | import defaults from 'lodash/defaults'; 4 | // import './WebSocketOnMessage'; 5 | 6 | import { 7 | CircularDataFrame, 8 | DataQueryRequest, 9 | DataQueryResponse, 10 | DataSourceApi, 11 | DataSourceInstanceSettings, 12 | FieldType, 13 | LoadingState, 14 | } from '@grafana/data'; 15 | 16 | import { MyQuery, MyDataSourceOptions, defaultQuery, IwsList, NumberHash, cons } from './types'; 17 | 18 | import { Observable, merge } from 'rxjs'; 19 | 20 | import { getTemplateSrv } from '@grafana/runtime'; 21 | 22 | function getSeriesId(event: any, ...keys: string[]): string { 23 | let parsedEvent = JSON.parse(event.data); 24 | let fields: string[] = keys.map(function(key) { 25 | return parsedEvent[key]; 26 | }); 27 | let id: string = fields.join('-'); 28 | return id; 29 | } 30 | 31 | export class DataSource extends DataSourceApi { 32 | baseUrl: string; 33 | wsList: IwsList; 34 | 35 | constructor(instanceSettings: DataSourceInstanceSettings) { 36 | super(instanceSettings); 37 | this.baseUrl = instanceSettings.jsonData.baseUrl || 'ws://localhost:5556'; 38 | // Track pool of websockets 39 | this.wsList = {}; 40 | } 41 | 42 | query(options: DataQueryRequest): Observable { 43 | const streams = options.targets.map(target => { 44 | const query = defaults(target, defaultQuery); 45 | const queryText = getTemplateSrv().replace(query.queryText, options.scopedVars, 'pipe'); 46 | let ws: WebSocket; 47 | if (queryText in this.wsList) { 48 | cons.trace('Using existing ws for query', queryText); 49 | ws = this.wsList[queryText]; 50 | } else { 51 | ws = this.newRiemannWebSocket(queryText || ''); 52 | this.wsList[queryText] = ws; 53 | query.webSocket = ws; 54 | cons.trace('Creating new ws for query', queryText); 55 | } 56 | let series: CircularDataFrame[] = []; 57 | let seriesList: NumberHash = {}; 58 | let seriesLastUpdate: NumberHash = {}; 59 | let seriesIndex = 0; 60 | cons.info('Processing query ', queryText); 61 | return new Observable(subscriber => { 62 | ws.onmessage = function(event) { 63 | var parsedEvent = JSON.parse(event.data); 64 | var typeOfTime = typeof parsedEvent['time']; 65 | if (typeOfTime === 'number') { 66 | // Mirabelle sends time as (fractional) seconds 67 | parsedEvent['time'] *= 1000; 68 | // Riemann sends time as Isodate string 69 | } else if (typeOfTime === 'string') { 70 | parsedEvent['time'] = new Date(parsedEvent['time']).getTime(); 71 | // In any other case, use current time 72 | } else { 73 | parsedEvent['time'] = new Date().getTime(); 74 | } 75 | const seriesId = getSeriesId(event, ...query.groupBy); 76 | let frame: CircularDataFrame; 77 | if (seriesId in seriesList) { 78 | cons.debug(`we already know about series ${seriesId} having index ${seriesList[seriesId]}`); 79 | frame = series[seriesList[seriesId]]; // get series' frame 80 | } else { 81 | if (seriesIndex < query.maxSeries) { 82 | cons.debug('Adding series ', seriesId); 83 | seriesLastUpdate[seriesId] = new Date().getTime() - 1000.0 / query.maxFreq; 84 | seriesList[seriesId] = seriesIndex++; // increment index 85 | frame = new CircularDataFrame({ 86 | append: 'tail', 87 | capacity: query.maxPoints, 88 | }); 89 | frame.refId = query.refId; 90 | frame.name = seriesId; 91 | frame.addField({ name: 'time', type: FieldType.time }); 92 | query.numberFields.map(field => { 93 | frame.addField({ name: field, type: FieldType.number }); 94 | }); 95 | query.stringFields.map(field => { 96 | frame.addField({ name: field, type: FieldType.string }); 97 | }); 98 | series.push(frame); 99 | } else { 100 | cons.info('MaxSeries reached! Not adding series ', seriesId); 101 | return; 102 | } 103 | } 104 | const currentTime = new Date().getTime(); 105 | if (currentTime - seriesLastUpdate[seriesId] >= 1000.0 / query.maxFreq) { 106 | frame.add(parsedEvent); 107 | subscriber.next({ 108 | data: series, 109 | key: query.refId, 110 | state: LoadingState.Streaming, 111 | }); 112 | seriesLastUpdate[seriesId] = currentTime; 113 | } else { 114 | cons.trace('MaxFreq reached! Dropping new data for series ', seriesId); 115 | } 116 | }; 117 | }); 118 | }); 119 | 120 | return merge(...streams); 121 | } 122 | async testDatasource(): Promise { 123 | let ws = this.newRiemannWebSocket(''); 124 | let promise = new Promise(function(resolve, reject) { 125 | ws.onerror = function(event) { 126 | reject({ 127 | status: 'error', 128 | message: `WebSocket Error: ${JSON.stringify(event)}`, 129 | }); 130 | }; 131 | ws.onopen = function(event) { 132 | resolve({ 133 | status: 'success', 134 | message: `WebSocket Success: ${JSON.stringify(event)}`, 135 | }); 136 | }; 137 | }); 138 | promise.then( 139 | function(result) { 140 | return result; 141 | }, 142 | function(error) { 143 | return error; 144 | } 145 | ); 146 | return promise; 147 | } 148 | newRiemannWebSocket(queryText: string): WebSocket { 149 | const Uri = this.baseUrl.concat('/index?subscribe=true&query=', queryText); 150 | cons.info('Opening new WS: ', Uri); 151 | return new WebSocket(encodeURI(Uri)); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grafana Riemann streaming datasource 2 | 3 | [![CircleCI](https://circleci.com/gh/faxm0dem/grafana-riemann-websocket-datasource/tree/master.svg?style=svg)](https://circleci.com/gh/faxm0dem/grafana-riemann-websocket-datasource/tree/master) 4 | 5 | This Grafana plugin implements a streaming [Riemann](https://riemann.io/) datasource. 6 | 7 | ## Purpose 8 | 9 | This datasource connects to a riemann server using websockets and subscribes to a stream. 10 | 11 | ![Animation showing timeseries being streamed to Grafana](https://github.com/faxm0dem/grafana-riemann-websocket-datasource/blob/master/img/grafana-riemann-streams.gif) 12 | 13 | ## Installation 14 | 15 | ### Latest published on grafana.com 16 | 17 | ``` 18 | grafana-cli plugins install ccin2p3-riemann-datasource 19 | ``` 20 | 21 | Or use the releases link on github and download the `.zip`. 22 | Then just `unzip` it to your Grafana plugins folder or run the following: 23 | 24 | ``` 25 | version=0.1.5 26 | grafana-cli --pluginUrl ./ccin2p3-riemann-datasource-${version}.zip plugins install ccin2p3-riemann-datasource 27 | ``` 28 | 29 | ### Roll your own 30 | 31 | You can also build your own using `yarn build` and moving the `dist` folder to your grafana plugins 32 | directory under the name `ccin2p3-riemann-datasource`. 33 | 34 | ## Configuring your Riemann backend 35 | 36 | For instructions on how to install a riemann server, refer to its [web site](https://riemann.io). 37 | 38 | ### For the impatient 39 | 40 | Here's a minimal riemann configuration file if you want to test the grafana plugin: 41 | 42 | ``` 43 | ; this will let you check riemann startup and websocket connections 44 | (logging/init {:file "/var/log/riemann/riemann.log"}) 45 | ; this will enable internal riemann instrumentation events at 1s interval, so you don't need to generate events yourself 46 | (instrumentation {:interval 1 :enabled? true}) 47 | ; this will enable the insecure ws server on localhost:5556 48 | (ws-server {:host "127.0.0.1" :port 5556}) 49 | ; this is to actually index events 50 | (let [index (index)] (streams index)) 51 | ``` 52 | 53 | ### Details 54 | 55 | The only requirement for the plugin to work is to have a riemann instance ready with websockets enabled. This means you need a line in the form of: 56 | 57 | ```clojure 58 | (ws-server {:host "0.0.0.0" :port 5556}) 59 | ``` 60 | 61 | As Riemann doesn't support secure websockets yet, we strongly advise you to tunnel it through your favourite web proxy. In any case, if Grafana is serving pages through `https`, you'll have no choice but to do so due to browser security enforcements. The way to go is traditionally to let riemann bind to localhost unsecured, and have the proxy listen to the server's public interface. 62 | 63 | ```clojure 64 | (ws-server {:host "127.0.0.1" :port 5556}) 65 | ``` 66 | 67 | For your convenience, here's a working configuration for HAProxy 1.5.18 (make sure you replace your public IP address): 68 | 69 | ``` 70 | # 71 | defaults 72 | mode http 73 | log global 74 | option httplog 75 | option http-server-close 76 | option dontlognull 77 | option redispatch 78 | option contstats 79 | retries 3 80 | backlog 10000 81 | timeout client 25s 82 | timeout connect 5s 83 | timeout server 25s 84 | timeout tunnel 3600s 85 | timeout http-keep-alive 1s 86 | timeout http-request 15s 87 | timeout queue 30s 88 | timeout tarpit 60s 89 | default-server inter 3s rise 2 fall 3 90 | option forwardfor 91 | 92 | frontend ft_riemann 93 | bind :5556 name http ssl crt /etc/riemann/ssl.pem 94 | maxconn 10000 95 | default_backend bk_riemann 96 | 97 | backend bk_riemann 98 | balance roundrobin 99 | server websrv1 localhost:5556 maxconn 10000 weight 10 cookie websrv1 check 100 | ``` 101 | 102 | You'll also need an index, or else the datasource will never see any events. 103 | 104 | 105 | ## Datasource Configuration 106 | 107 | ### Base URL 108 | 109 | Base URL to the riemann server. 110 | 111 | #### Examples 112 | 113 | ``` 114 | wss://my-haproxy-frontend:5556 115 | ``` 116 | 117 | ``` 118 | ws://my-insecure-riemann:5556 119 | ``` 120 | 121 | ## Query Configuration 122 | 123 | ### Query Text 124 | 125 | This is the query text that will define the websocket subscription. 126 | The riemann query language doesn't have proper documentation yet, but there are lots of examples on its [website](https://riemann.io) 127 | and on the [test suite](https://github.com/riemann/riemann/blob/master/test/riemann/query_test.clj). 128 | 129 | If you use variable interpolation and use multiple selections, make sure to use the regexp match. 130 | 131 | #### Examples 132 | 133 | ``` 134 | tagged "collectd" and plugin = "load" 135 | tagged "riemann" 136 | metric and state = "ok" 137 | metric = 42 138 | ``` 139 | 140 | ``` 141 | service ~= "$service" 142 | # When multiple services are selected, this will expand to 143 | # service ~= "service1|service2|service3" 144 | ``` 145 | 146 | ### GroupBy 147 | 148 | Riemann will potentially send you a truckload of unrelated events, unless your query is specific enough. 149 | If you don't want those to end up in the same Grafana series, you have to decide which riemann fields or attributes uniquely identify 150 | your Grafana series. This is where `GroupBy` comes it. It will assign a unique name to each series based on the event's attributes. 151 | For instance if you use `GroupBy=host` all events sharing the same `host` riemann attribute will end up in the same Grafana series. So you'll get 152 | as many series as you have hosts. If you use `GroupBy=host,service` you'll get `numHosts * numSeries` series. 153 | 154 | How many series you'll get is however constrained by the parameter `MaxSeries`. 155 | 156 | ### Max* 157 | 158 | To prevent your browser to die on you, the developers of the riemann Grafana plugin kindly implemented the `MaxSeries`, `MaxDataPoints` and `MaxFreq` parameters. 159 | 160 | ### MaxSeries 161 | 162 | It will cause your browser to ignore events that don't match the *first* `MaxSeries` series. It doesn't mean it won't process them: once you subscribed to 163 | a riemann event stream, your browser will get hit by all events matching the query. But only the ones whose first `MaxSeries` `GroupBy` clause matches the series identifier 164 | will get drawn on screen. The others will be ignored. 165 | 166 | ### MaxDataPoints 167 | 168 | This parameter limits the number of data points per series that are kept in memory. So if you chose `MaxSeries=10` and `MaxDataPoints=1000` the Grafana panel in your browser will 169 | display at most `10000` points. Older points will be removed in favour of younger events in a FIFO fashion. 170 | 171 | ### MaxFreq 172 | 173 | This parameter limits the number of data points added to your *series* every second. If you choose `MaxFreq=1` and two riemann events are consumed in less than a second, the plugin will ignore the second event. It will still process the websocket event, but will forget about it immediately. Again, this is per *series* so if you have `MaxSeries=10,MaxFreq=10` you'll get at most 100 points per second to be drawn on screen. 174 | 175 | ### StringFields 176 | 177 | [Riemann events can contain many different attributes](https://riemann.io/concepts.html) along with `host`, `service`, `state` and `description`. This parameter 178 | lets you decide which will be made available to panels as Grafana fields. 179 | 180 | ### NumericFields 181 | 182 | Riemann events usually contain the `metric` field which stores the time series' value. But they also contail the `ttl` field which stores the event's expiration time. 183 | This parameter lets you provide a coma-separated list to specify which fields should be fetched and made available as numeric fields in Grafana. This defaults to `metric`. 184 | 185 | 186 | ## Caveats 187 | 188 | ### Websocket connections 189 | 190 | The datasource works by opening one websocket per query. It reuses those sockets when dashboards are reloaded, or queries modified. It does so by tracking the queries by their `QueryText`. This has the following consequences: 191 | 192 | 1. When creating two panels with the same query, or one panel with two identical queries, things might go wrong 193 | 2. When modifying a query's parameters (but not the text), you have to save the panel and reload it in order for changes to be taken into account (clicking Grafana's refresh button won't suffice) 194 | 195 | Also, the developer's haven't found a way (yet) to properly close the connections when leaving the dashboard. This means your websockets will remain open until you close the browser tab (or switch Grafana organization). 196 | 197 | So please follow these guidelines: 198 | 199 | 1. Never use the same query more than once in the same dashboard. If you want to get two different representations of the same data, use Grafana's ["reuse queries" functionality](https://github.com/grafana/grafana/pull/16660) instead 200 | 2. If you modify a query's parameters (*e.g.* `MaxFreq`) save, then reload the tab 201 | 3. If you don't need your realtime dashboard, close the tab for your riemann server's sake 202 | 203 | ### Mixed datasources 204 | 205 | Grafana offers the ability to add data from multiple datasources to a panel through the use of the `--Mixed--` datasource. Unfortunately this functionality doesn't work yet with streaming datasources. 206 | If you would like to use this functionality, please help us [increase this bug's visibility by adding a thumbs up](https://github.com/grafana/grafana/issues/28981) 207 | 208 | ## Learn more 209 | - [Riemann](https://riemann.io) 210 | - [Grafana documentation](https://grafana.com/docs/) 211 | - [Grafana Tutorials](https://grafana.com/tutorials/) - Grafana Tutorials are step-by-step guides that help you make the most of Grafana 212 | 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------