├── .github └── workflows │ ├── integration-tests.yml │ └── npm-publish.yml ├── .gitignore ├── README.md ├── ksQlient ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ │ ├── integrationtests.js │ └── queryBuilderTests.js ├── docker-compose.yml ├── ksqldb │ ├── customErrors.js │ ├── ksqldb.js │ ├── queryBuilder.js │ └── validateInputs.js ├── package.json └── static │ ├── light.png │ └── name.png ├── ksqLight ├── .gitignore ├── README.md ├── example_containers │ ├── docker-compose.yml │ ├── jmx_exporter │ │ ├── jmx_prometheus_javaagent-0.17.0.jar │ │ └── ksqldb.yml │ └── prometheus │ │ └── prometheus.yml ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── index.html │ ├── main.js │ ├── manifest.json │ └── robots.txt ├── server │ ├── queryTypes.js │ └── server.js ├── src │ ├── App.js │ ├── components │ │ ├── Header.js │ │ ├── Homepage.js │ │ ├── LineChart.js │ │ ├── MetricCard.js │ │ ├── PermanentDrawer.js │ │ ├── QueryPage.js │ │ ├── SettingsSidebar.js │ │ └── Welcomepage.js │ ├── index.css │ ├── index.js │ ├── static │ │ ├── darkmode.gif │ │ ├── ksqLight2.png │ │ ├── ksqLight_name_2.png │ │ ├── ksqlight_icon.ico │ │ ├── ksqlight_icon.png │ │ ├── ksqlight_raleway.png │ │ ├── landing.gif │ │ ├── settings.gif │ │ └── sidebar.gif │ ├── style.scss │ └── utils │ │ └── utilityFunctions.js └── tailwind.config.js └── ksqljs └── README.md /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Integration-Tests 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the "master" branch 8 | push: 9 | branches: 10 | - ms/** 11 | - gb/** 12 | - jl/** 13 | - mx/** 14 | - ja/** 15 | pull_request: 16 | branches: 17 | - dev 18 | - master 19 | 20 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 21 | jobs: 22 | # This workflow contains a single job called "build" 23 | build: 24 | # The type of runner that the job will run on 25 | runs-on: ubuntu-latest 26 | timeout-minutes: 3 27 | 28 | # Steps represent a sequence of tasks that will be executed as part of the job 29 | steps: 30 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 31 | - uses: actions/checkout@v3 32 | 33 | - name: Start Containers 34 | run: docker-compose -f "./ksQlient/docker-compose.yml" up -d --build 35 | 36 | - name: Install node 37 | uses: actions/setup-node@v1 38 | with: 39 | node-version: 16.x 40 | 41 | - name: Install dependencies 42 | run: npm install --prefix "./ksQlient" 43 | 44 | - name: Sleep for 30 seconds 45 | run: sleep 30s 46 | shell: bash 47 | 48 | - name: Run tests 49 | run: npm run test --prefix "./ksQlient" 50 | 51 | - name: Stop containers 52 | if: always() 53 | run: docker-compose -f "./ksQlient/docker-compose.yml" down 54 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish-npm: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: 16 15 | registry-url: https://registry.npmjs.org/ 16 | - name: Publish 17 | working-directory: ./ksQlient 18 | run: npm publish --access public 19 | env: 20 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | */node_modules 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ksqlSuite 2 | 3 |
4 | 5 |
6 | logo 7 | logo 8 |
9 | 10 |

A Node.js client and a metric visualizer for ksqlDB

11 |
12 | 13 | ## About 14 | 15 | Need to run stream processing workloads on ksqlDB in Node.JS? Our lightweight **Node.js client** can help. 16 | 17 | Check out [ksQlient](./ksQlient/) 18 | 19 | Need to visualize ksqlDB query metrics to diagnose bottleneck issues? Try out our **metric visualizer**. 20 | 21 | Check out [ksqLight](./ksqLight/) 22 | -------------------------------------------------------------------------------- /ksQlient/.npmignore: -------------------------------------------------------------------------------- 1 | #Test Files 2 | /__tests__/ 3 | .yml 4 | /static/ 5 | -------------------------------------------------------------------------------- /ksQlient/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ksQlient/README.md: -------------------------------------------------------------------------------- 1 | # ksQlient (formerly ksqlDB-JS) 2 | 3 |
4 | 5 | GitHub stars 6 | GitHub issues 7 | GitHub last commit 8 | 9 |

A native Node.js client for ksqlDB

10 |
11 | 12 | ## About the Project 13 | 14 | ksQlient is a **lightweight Node.js client for ksqlDB**, a database for streaming applications leveraging Kafka infrastructure under the hood. 15 | 16 | With our client, you can deploy stream-processing workloads on ksqlDB from within JS applications using simple, declarative SQL statements. 17 | 18 | Sample use cases: 19 | 20 | 1. Build applications that respond immediately to events. 21 | 2. Craft materialized views over streams. 22 | 3. Receive real-time push updates, or pull current state on demand. 23 | 24 | ## Table of Contents 25 | 26 | - [About the project](#about) 27 | - [Getting Started](#getting-started) 28 | - [Usage](#usage) 29 | - [Features](#features) 30 | - [Developers](#developers) 31 | - [Contributions](#contributions) 32 | - [License](#license) 33 | 34 | ## Getting Started 35 | 36 | The client is available on Node package manager (npm) ([link](https://www.npmjs.com/package/ksqlient)) 37 | 38 | ```bash 39 | 40 | npm install ksqlient 41 | 42 | ``` 43 | 44 | ## Usage 45 | 46 | Create a client in the application file: 47 | 48 | ```javascript 49 | const ksqldb = require("ksqlient"); 50 | const client = new ksqldb({ ksqldbURL: "" }); 51 | ``` 52 | 53 | To run tests, initiate Docker containers included in yaml file: 54 | 55 | ```bash 56 | docker-compose up 57 | npm test 58 | ``` 59 | 60 | ## Features 61 | 62 | ### Create a pull query 63 | 64 | ```javascript 65 | client.pull("SELECT * FROM myTable;"); 66 | ``` 67 | 68 | ### Create a push query (persistent query that subscribes to a stream) 69 | 70 | ```javascript 71 | client.push("SELECT * FROM myTable EMIT CHANGES;", (data) => { 72 | console.log(data); 73 | }); 74 | ``` 75 | 76 | ### Terminate persistent query (e.g. push query) 77 | 78 | ```javascript 79 | client.terminate(queryId); 80 | ``` 81 | 82 | ### Insert rows of data into a stream 83 | 84 | ```javascript 85 | client.insertStream("myTable", [ 86 | { name: "jack", email: "123@mail.com", age: 25 }, 87 | { name: "john", email: "456@mail.com", age: 20 }, 88 | ]); 89 | ``` 90 | 91 | ### List streams/queries 92 | 93 | ```javascript 94 | client.ksql("LIST STREAMS;"); 95 | ``` 96 | 97 | ### Create table/streams 98 | 99 | ```javascript 100 | client.createStream( 101 | "testStream", 102 | (columnsType = ["name VARCHAR", "email varchar", "age INTEGER"]), 103 | (topic = "testTopic"), 104 | (value_format = "json"), 105 | (partitions = 1) 106 | ); 107 | ``` 108 | 109 | ### For custom SQL statements including complex joins use the .ksql method 110 | 111 | ```javascript 112 | client.ksql("DROP STREAM IF EXISTS testStream;"); 113 | ``` 114 | 115 | ### SQL Query builder 116 | 117 | Feel free to use the built-in query builder to parametrize any SQL query to avoid SQL injection. 118 | 119 | ```javascript 120 | const builder = new queryBuilder(); 121 | const query = "SELECT * FROM table WHERE id = ? AND size = ?"; 122 | const finishedQuery = builder.build(query, 123, "middle"); 123 | 124 | client.ksql(finishedQuery); 125 | ``` 126 | 127 | ### Create a table (materialized view) from a source stream 128 | 129 | ```javascript 130 | client.createTableAs( 131 | "testTable", 132 | "sourceStream", 133 | (selectArray = ["name", "LATEST_BY_OFFSET(age) AS recentAge"]), 134 | (propertiesObj = { topic: "newTestTopic" }), 135 | (conditionsObj = { WHERE: "age >= 21", GROUP_BY: "name" }) 136 | ); 137 | ``` 138 | 139 | ### Create a stream based on an existing stream 140 | 141 | ```javascript 142 | client.createStreamAs( 143 | "testStream", 144 | (selectColumns = ["name", "age"]), 145 | "sourceStream", 146 | (propertiesObj = { 147 | kafka_topic: "testTopic", 148 | value_format: "json", 149 | partitions: 1, 150 | }), 151 | (conditions = "age > 50") 152 | ); 153 | ``` 154 | 155 | ### Pull stream data between two timestamps 156 | 157 | ```javascript 158 | client.pullFromTo( 159 | "TESTSTREAM", 160 | "America/Los_Angeles", 161 | (from = ["2022-01-01", "00", "00", "00"]), 162 | (to = ["2022-01-01", "00", "00", "00"]) 163 | ); 164 | ``` 165 | 166 | ### Troubleshooting methods to inspect server metrics 167 | 168 | - inspectServerStatus 169 | - inspectQueryStatus 170 | - inspectClusterStatus 171 | 172 | ## Use Case 173 | 174 | We have built a [demo app](https://github.com/stabRabbitDemo/app) to showcase how ksQlient can be used to create a streaming application . 175 | 176 | ## Developers 177 | 178 | - Javan Ang - [GitHub](https://github.com/javanang) | [LinkedIn](https://www.linkedin.com/in/javanang/) 179 | - Michael Snyder - [GitHub](https://github.com/MichaelCSnyder) | [LinkedIn](https://www.linkedin.com/in/michaelcharlessnyder/) 180 | - Jonathan Luu - [GitHub](https://github.com/jonathanluu17) | [LinkedIn](https://www.linkedin.com/in/jonathanluu17/) 181 | - Matthew Xing - [GitHub](https://github.com/matthewxing1) | [LinkedIn](https://www.linkedin.com/in/matthew-xing/) 182 | - Gerry Bong - [GitHub](https://github.com/ggbong734) | [LinkedIn](https://www.linkedin.com/in/gerry-bong-71137420/) 183 | 184 | ## Contributions 185 | 186 | Contributions to the code, examples, documentation, etc. are very much appreciated. 187 | 188 | - Please report issues and bugs directly in this [GitHub project](https://github.com/oslabs-beta/ksqljs/issues). 189 | 190 | ## License 191 | 192 | This product is licensed under the MIT License - see the LICENSE.md file for details. 193 | 194 | This is an open source product. 195 | 196 | This product is accelerated by OS Labs. 197 | 198 | ksqlDB is licensed under the [Confluent Community License](https://github.com/confluentinc/ksql/blob/master/LICENSE). 199 | 200 | _Apache, Apache Kafka, Kafka, and associated open source project names are trademarks of the [Apache Software Foundation](https://www.apache.org/)_. 201 | -------------------------------------------------------------------------------- /ksQlient/__tests__/integrationtests.js: -------------------------------------------------------------------------------- 1 | const ksqldb = require('../ksqldb/ksqldb'); 2 | // Pre-requisite: start a docker container 3 | /* To add to README: Prior to running test with 'npm test', please start the ksqlDB 4 | server using the command 'docker-compose up'. This will spin up a ksqlDB server on 5 | 'http://localhost:8088'. If the command was run before, the created container might 6 | need to be removed first. 7 | */ 8 | 9 | // ** INTEGRATION TEST INSTRUCTIONS ** 10 | 11 | // Prior to running the test files, please ensure an instance of the ksqldb server is running 12 | // Steps to starting the ksqldb server can be found here: (https://ksqldb.io/quickstart.html) 13 | // Once the ksqlDB server is running, tests can be run with terminal line: (npm test) 14 | 15 | describe('--Integration Tests--', () => { 16 | describe('--Method Tests--', () => { 17 | beforeAll((done) => { 18 | client = new ksqldb({ ksqldbURL: 'http://localhost:8088' }); 19 | done(); 20 | }); 21 | 22 | afterAll(async () => { 23 | await client.ksql('DROP STREAM IF EXISTS TESTJESTSTREAM DELETE TOPIC;'); 24 | }) 25 | 26 | it('.createStream properly creates a stream', async () => { 27 | await client.ksql('DROP STREAM IF EXISTS TESTJESTSTREAM DELETE TOPIC;') 28 | const result = await client.createStream('TESTJESTSTREAM', ['name VARCHAR', 'email varchar', 'age INTEGER'], 'testJestTopic', 'json', 1); 29 | const streams = await client.ksql('LIST STREAMS;'); 30 | const allStreams = streams.streams; 31 | let streamExists = false; 32 | for (let i = 0; i < allStreams.length; i++) { 33 | if (allStreams[i].name === "TESTJESTSTREAM") { 34 | streamExists = true; 35 | break; 36 | } 37 | } 38 | expect(streamExists).toEqual(true); 39 | }) 40 | 41 | it('.push properly creates a push query', async () => { 42 | let pushActive = false; 43 | await client.push('SELECT * FROM TESTJESTSTREAM EMIT CHANGES LIMIT 1;', async (data) => { 44 | if (JSON.parse(data).queryId) { 45 | pushActive = true; 46 | } 47 | expect(pushActive).toEqual(true) 48 | }); 49 | }) 50 | 51 | it('.terminate properly terminates a push query', () => { 52 | client.push('SELECT * FROM TESTJESTSTREAM EMIT CHANGES LIMIT 3;', async (data) => { 53 | const terminateRes = await client.terminate(JSON.parse(data).queryId); 54 | expect(terminateRes.wasTerminated).toEqual(true); 55 | }) 56 | }) 57 | 58 | it('.insertStream properly inserts a row into a stream', async () => { 59 | 60 | const data = []; 61 | await client.push('SELECT * FROM TESTJESTSTREAM EMIT CHANGES;', async (chunk) => { 62 | data.push(JSON.parse(chunk)); 63 | if (data[1]) { 64 | client.terminate(data[0].queryId); 65 | expect(data[1]).toEqual(["stab-rabbit", "123@mail.com", 100]) 66 | } 67 | }); 68 | const response = await client.insertStream('TESTJESTSTREAM', [ 69 | { "name": "stab-rabbit", "email": "123@mail.com", "age": 100 } 70 | ]); 71 | }) 72 | 73 | it('.pull receives the correct data from a pull query', async () => { 74 | const pullData = await client.pull("SELECT * FROM TESTJESTSTREAM;"); 75 | expect(pullData[1]).toEqual(["stab-rabbit", "123@mail.com", 100]); 76 | }) 77 | 78 | it('.pullFromTo receives all the data', async () => { 79 | const pullData = await client.pull("SELECT * FROM TESTJESTSTREAM;"); 80 | const data = await client.pullFromTo('TESTJESTSTREAM', 'America/Los_Angeles', ['2022-01-01', '00', '00', '00']); 81 | const expectPullData = pullData[1]; 82 | const expectData = data[0].slice(0, 3); 83 | expect(expectPullData).toEqual(expectData); 84 | }) 85 | 86 | describe('--Materialized Streams and Tables--', () => { 87 | beforeAll(async () => { 88 | await client.ksql('DROP STREAM IF EXISTS testAsStream;') 89 | await client.ksql('DROP TABLE IF EXISTS testAsTable;'); 90 | await client.ksql('DROP STREAM IF EXISTS newTestStream DELETE TOPIC;'); 91 | await client.createStream('newTestStream', ['name VARCHAR', 'age INTEGER'], 'newTestTopic', 'json', 1); 92 | }); 93 | 94 | afterAll(async () => { 95 | await client.ksql('DROP STREAM IF EXISTS newTestStream DELETE TOPIC;'); 96 | }) 97 | 98 | describe('--Materialized Streams Tests--', () => { 99 | beforeAll(async () => { 100 | // await client.ksql('DROP STREAM IF EXISTS testAsStream;') 101 | // await client.ksql('DROP STREAM IF EXISTS newTestStream DELETE TOPIC;'); 102 | 103 | // await client.createStream('newTestStream', ['name VARCHAR', 'age INTEGER'], 'newTestTopic', 'json', 1); 104 | testAsQueryId = await client.createStreamAs('testAsStream', ['name', 'age'], 'newTestStream', { 105 | kafka_topic: 'newTestTopic', 106 | value_format: 'json', 107 | partitions: 1 108 | }, 'age > 50'); 109 | }) 110 | 111 | afterAll(async () => { 112 | await client.ksql('DROP STREAM IF EXISTS testAsStream;') 113 | // await client.ksql('DROP STREAM IF EXISTS newTestStream DELETE TOPIC;'); 114 | }) 115 | 116 | it('creates materialized stream', async () => { 117 | let streamFound = false; 118 | const { streams } = await client.ksql('LIST STREAMS;'); 119 | 120 | for (let i = 0; i < streams.length; i++) { 121 | if (streams[i].name, streams[i].name === 'TESTASSTREAM') { 122 | streamFound = true; 123 | break; 124 | } 125 | } 126 | expect(streamFound).toBe(true); 127 | }); 128 | }); 129 | 130 | 131 | describe('--Materialized Tables Tests--', () => { 132 | beforeAll(async () => { 133 | await client.createTableAs('testAsTable', 'newTestStream', ['name', 'LATEST_BY_OFFSET(age) AS recentAge'], { topic: 'newTestTopic' }, { WHERE: 'age >= 21', GROUP_BY: 'name' }); 134 | }); 135 | afterAll(async () => { 136 | await client.ksql('DROP TABLE IF EXISTS testAsTable;'); 137 | // await client.ksql('DROP TABLE IF EXISTS TABLEOFSTREAM DELETE TOPIC;') 138 | // await client.ksql('DROP STREAM IF EXISTS NEWTESTSTREAM DELETE TOPIC;') 139 | }) 140 | 141 | it('creates a materialized table view of a stream', async () => { 142 | const { tables } = await client.ksql('LIST TABLES;'); 143 | let tableFound = false; 144 | for (let i = 0; i < tables.length; i++) { 145 | if (tables[i].name === 'TESTASTABLE') { 146 | tableFound = true; 147 | break; 148 | } 149 | } 150 | expect(tableFound).toEqual(true); 151 | }) 152 | }) 153 | }) 154 | }) 155 | 156 | 157 | describe('--Health Tests--', () => { 158 | beforeAll((done) => { 159 | client = new ksqldb({ ksqldbURL: 'http://localhost:8088' }); 160 | done(); 161 | }); 162 | 163 | afterAll(async () => { 164 | await client.ksql('DROP STREAM IF EXISTS TESTSTREAM2;'); 165 | }) 166 | 167 | it('.inspectQueryStatus checks if a stream is created successfully', async () => { 168 | const streamName = 'TESTSTREAM2' 169 | const create = await client.ksql(`CREATE STREAM IF NOT EXISTS ${streamName} 170 | (name VARCHAR, 171 | email varchar, 172 | age INTEGER) 173 | WITH ( 174 | KAFKA_TOPIC = 'testJestTopic', 175 | VALUE_FORMAT = 'json', 176 | PARTITIONS = 1 177 | );`); 178 | const commandId = create ? create.commandId : `stream/${streamName}/create`; 179 | const status = await client.inspectQueryStatus(commandId); 180 | // response should be { status: 'SUCCESS', message: 'Stream created', queryId: null } 181 | expect(status.data).toEqual(expect.objectContaining({ 182 | status: expect.any(String), 183 | message: expect.any(String), 184 | queryId: null 185 | })); 186 | }) 187 | 188 | it('.inspectServerInfo returns the server info and status', async () => { 189 | const status = await client.inspectServerInfo(); 190 | // should return something like: { 191 | // KsqlServerInfo: { 192 | // version: '0.25.1', 193 | // kafkaClusterId: '0Yxd6N5OSKGDUalltPWvXg', 194 | // ksqlServiceId: 'default_', 195 | // serverStatus: 'RUNNING' 196 | // } 197 | // } 198 | expect(status.data).toEqual(expect.objectContaining({ 199 | KsqlServerInfo: expect.objectContaining({ 200 | version: expect.any(String), 201 | kafkaClusterId: expect.any(String), 202 | serverStatus: expect.any(String) 203 | }) 204 | })); 205 | }) 206 | 207 | it('.inspectServerHealth returns the server health', async () => { 208 | const status = await client.inspectServerHealth(); 209 | // should return something like: { 210 | // isHealthy: true, 211 | // details: { 212 | // metastore: { isHealthy: true }, 213 | // kafka: { isHealthy: true }, 214 | // commandRunner: { isHealthy: true } 215 | // } 216 | // } 217 | expect(status.data).toEqual(expect.objectContaining({ 218 | isHealthy: expect.any(Boolean), 219 | details: expect.objectContaining({ 220 | metastore: expect.anything(), 221 | kafka: expect.anything(), 222 | commandRunner: expect.anything() 223 | }) 224 | }) 225 | ); 226 | }) 227 | 228 | it('.inspectClusterStatus returns the cluster status', async () => { 229 | const status = await client.inspectClusterStatus(); 230 | // should return something like: { 231 | // clusterStatus: { 232 | // 'ksqldb-server:8088': { 233 | // hostAlive: true, 234 | // lastStatusUpdateMs: 1653164479237, 235 | // activeStandbyPerQuery: [Object], 236 | // hostStoreLags: [Object] 237 | // } 238 | // }} 239 | expect(status.data).toEqual(expect.objectContaining({ 240 | clusterStatus: expect.anything() 241 | }) 242 | ); 243 | }) 244 | 245 | it('.isValidProperty returns true if a server configuration property is not prohibited from setting', async () => { 246 | const status = await client.isValidProperty('test'); 247 | // should return true 248 | expect(status.data).toEqual(true); 249 | }) 250 | 251 | // it('isValidProperty returns an error if the server property is prohibited from setting', async () => { 252 | // const status = await client.isValidProperty('ksql.connect.url'); 253 | // // should return something like 254 | // // { 255 | // // "@type": "generic_error", 256 | // // "error_code": 40000, 257 | // // "message": "One or more properties overrides set locally are prohibited by the KSQL server (use UNSET to reset their default value): [ksql.service.id]" 258 | // // } 259 | // expect(status.data).toEqual(expect.objectContaining({ 260 | // type: expect.any(String), 261 | // error_code: expect.any(Number), 262 | // message: expect.any(String), 263 | // })); 264 | // }) 265 | }) 266 | }) 267 | -------------------------------------------------------------------------------- /ksQlient/__tests__/queryBuilderTests.js: -------------------------------------------------------------------------------- 1 | const queryBuilder = require('../ksqldb/queryBuilder.js'); 2 | const { QueryBuilderError, EmptyQueryError, NumParamsError, InappropriateStringParamError } = require('../ksqldb/customErrors.js'); 3 | 4 | 5 | describe('Unit tests for query builder class', () => { 6 | let builder; 7 | let query; 8 | 9 | beforeAll((done) => { 10 | builder = new queryBuilder(); 11 | query = 'SELECT * FROM table WHERE id = ? AND size = ?'; 12 | done(); 13 | }); 14 | 15 | describe('Normal use case', () => { 16 | it('properly adds params into a sql query', () => { 17 | const finishedQuery = builder.build(query, 123, "middle"); 18 | expect(finishedQuery).toEqual("SELECT * FROM table WHERE id = 123 AND size = 'middle'"); 19 | }); 20 | }); 21 | 22 | describe('Error testing', () => { 23 | it('throws an error if number of params is different from number of question marks', () => { 24 | expect(() => { builder.build(query, 123, "middle", "extra") }).toThrow(NumParamsError); 25 | }); 26 | 27 | it('throws an error if the query is empty', () => { 28 | const query = ''; 29 | expect(() => { builder.build(query, 123) }).toThrow(EmptyQueryError); 30 | }); 31 | 32 | it('throws an error if an object is passed in as a param', () => { 33 | expect(() => { builder.build(query, 123, { "middle": "size" }) }).toThrow(QueryBuilderError); 34 | }); 35 | }); 36 | 37 | describe('SQL injection', () => { 38 | it("prevents 'OR 1=1' SQL injection by escaping single quotations ", () => { 39 | // https://stackoverflow.com/questions/5139770/escape-character-in-sql-server 40 | const finishedQuery = builder.build(query, 123, "middle' OR 1=1"); 41 | expect(finishedQuery).toEqual("SELECT * FROM table WHERE id = 123 AND size = 'middle'' OR 1=1'"); 42 | }); 43 | 44 | it("prevents (middle' OR 'a'=a') SQL injection by escaping single quotations ", () => { 45 | const finishedQuery = builder.build(query, 123, "middle' OR 'a'='a",); 46 | expect(finishedQuery).toEqual("SELECT * FROM table WHERE id = 123 AND size = 'middle'' OR ''a''=''a'"); 47 | }); 48 | 49 | it("throws an error if user tries to add a semicolon into a string param not wrapped in quotes", () => { 50 | expect(() => { builder.build(query, ['123; DROP tables WHERE size = '], 'middle') }).toThrow(InappropriateStringParamError); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /ksQlient/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.2" 3 | 4 | services: 5 | zookeeper: 6 | image: confluentinc/cp-zookeeper:7.0.1 7 | hostname: zookeeper 8 | container_name: zookeeper 9 | ports: 10 | - "2181:2181" 11 | environment: 12 | ZOOKEEPER_CLIENT_PORT: 2181 13 | ZOOKEEPER_TICK_TIME: 2000 14 | 15 | broker: 16 | image: confluentinc/cp-kafka:7.0.1 17 | hostname: broker 18 | container_name: broker 19 | depends_on: 20 | - zookeeper 21 | ports: 22 | - "29092:29092" 23 | environment: 24 | KAFKA_BROKER_ID: 1 25 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" 26 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 27 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:9092,PLAINTEXT_HOST://localhost:29092 28 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 29 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 30 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 31 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 32 | 33 | ksqldb-server: 34 | image: confluentinc/ksqldb-server:0.25.1 35 | hostname: ksqldb-server 36 | container_name: ksqldb-server 37 | volumes: 38 | - type: bind 39 | source: ./ 40 | target: /home/appuser 41 | depends_on: 42 | - broker 43 | ports: 44 | - "8088:8088" 45 | - "1090:1099" 46 | environment: 47 | KSQL_LISTENERS: http://0.0.0.0:8088 48 | KSQL_BOOTSTRAP_SERVERS: broker:9092 49 | KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true" 50 | KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true" 51 | # KSQL_KSQL_OPTS: "-Djava.security.auth.login.config=/jaas_config.file" 52 | # KSQL_AUTHENTICATION_METHOD: BASIC 53 | # KSQL_AUTHENTICATION_REALM: KsqlServer-Props 54 | # KSQL_AUTHENTICATION_ROLES: admin,developer,user 55 | # KSQL_SSL_CLIENT_AUTHENTICATION: NONE 56 | # KSQL_SSL_TRUSTSTORE_LOCATION: ksqldb_server_config/kafka.server.truststore.jks 57 | # KSQL_SSL_TRUSTSTORE_PASSWORD: ${SSL_PASSWORD} 58 | # KSQL_SSL_KEYSTORE_LOCATION: ksqldb_server_config/kafka.server.keystore.jks 59 | # KSQL_SSL_KEYSTORE_PASSWORD: ${SSL_PASSWORD} 60 | # KSQL_SSL_KEY_PASSWORD: ${SSL_PASSWORD} 61 | KSQL_KSQL_HEARTBEAT_ENABLE: "true" 62 | KSQL_KSQL_LAG_REPORTING_ENABLE: "true" 63 | 64 | ksqldb-cli: 65 | image: confluentinc/ksqldb-cli:0.25.1 66 | container_name: ksqldb-cli 67 | depends_on: 68 | - broker 69 | - ksqldb-server 70 | entrypoint: /bin/sh 71 | tty: true -------------------------------------------------------------------------------- /ksQlient/ksqldb/customErrors.js: -------------------------------------------------------------------------------- 1 | /** This file contains custom Node.js errors that extends the built-in Error class 2 | * REF: https://rclayton.silvrback.com/custom-errors-in-node-js 3 | * REF: https://futurestud.io/tutorials/node-js-create-your-custom-error 4 | */ 5 | 6 | // used to wrap error received from ksqlDB server 7 | class ksqlDBError extends Error { 8 | constructor(error) { 9 | super(error.message) 10 | 11 | // Ensure the name of this error is the same as the class name 12 | this.name = this.constructor.name 13 | 14 | // capturing the stack trace keeps the reference to your error class 15 | Error.captureStackTrace(this, this.constructor); 16 | 17 | // you may also assign additional properties to your error 18 | //this.status = 404 19 | Object.keys(error).forEach(property => {this[property] = error[property]}); 20 | } 21 | } 22 | 23 | // for returning error related to use of queryBuilder class 24 | class QueryBuilderError extends Error { 25 | constructor(message) { 26 | super(message) 27 | this.name = this.constructor.name 28 | Error.captureStackTrace(this, this.constructor); 29 | } 30 | } 31 | 32 | class EmptyQueryError extends QueryBuilderError { 33 | constructor() { 34 | super('Query should not be empty, undefined, or null'); 35 | } 36 | } 37 | 38 | class NumParamsError extends QueryBuilderError { 39 | constructor(message) { 40 | super(message); 41 | } 42 | } 43 | 44 | class InappropriateStringParamError extends QueryBuilderError { 45 | constructor(message) { 46 | super(message); 47 | } 48 | } 49 | 50 | class invalidArgumentTypes extends Error { 51 | constructor(message) { 52 | super(message); 53 | 54 | this.name = this.constructor.name; 55 | // necessary? 56 | Error.captureStackTrace(this, this.constructor); 57 | 58 | } 59 | } 60 | 61 | module.exports = { 62 | ksqlDBError, 63 | QueryBuilderError, 64 | EmptyQueryError, 65 | NumParamsError, 66 | InappropriateStringParamError, 67 | invalidArgumentTypes 68 | }; 69 | -------------------------------------------------------------------------------- /ksQlient/ksqldb/ksqldb.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const https = require('node:https'); 3 | const http2 = require("http2"); 4 | const { ksqlDBError } = require("./customErrors.js"); 5 | const validateInputs = require('./validateInputs.js'); 6 | const queryBuilder = require('./queryBuilder.js'); 7 | const builder = new queryBuilder(); 8 | 9 | class ksqldb { 10 | /** 11 | * Constructor 12 | * @param {object} config 13 | * 14 | * Config object can have these properties 15 | * 16 | * ksqldbURL: Connection URL or address 17 | * 18 | * API: Username or API key for basic authentication 19 | * 20 | * secret: Password or secret for basic authentication 21 | * 22 | * httpsAgent: httpsAgent for setting TLS properties 23 | */ 24 | constructor(config) { 25 | this.ksqldbURL = config.ksqldbURL; 26 | this.API = config.API ? config.API : null; 27 | this.secret = config.secret ? config.secret : null; 28 | this.httpsAgentAxios = config.httpsAgent ? new https.Agent(config.httpsAgent) : null; 29 | this.httpsAgentHttp2 = config.httpsAgent ? config.httpsAgent : null; 30 | } 31 | 32 | /** 33 | * Executes a pull query and returns the results. 34 | * 35 | *

This method may be used to execute pull queries, and returns an array containing all the data 36 | * received. The first value of the array will be an object containing -> 37 | * queryId: a string that contains the id of the stream that the pull query is being executed upon. 38 | * columnNames: an array that contains the names of the columns in the format of strings. 39 | * columnTypes: an array that contains the names of the columnTypes in the format of strings. 40 | * 41 | * Any subsequent values of the array are arrays that contain the data received. 42 | * 43 | *

If user input is used to build the query, please use the queryBuilder method to protect against sql injection. 44 | * 45 | * @param {string} query sql statement of query to execute 46 | * @return {Promise} a promise that completes once the server response is received, and contains the query 47 | * result if successful. 48 | * 49 | * Example: [{object that contains the metadata}, [data], [data], ...}] 50 | */ 51 | pull = (query) => { 52 | validateInputs([query, 'string', 'query']); 53 | 54 | const validatedQuery = builder.build(query); 55 | 56 | return axios 57 | .post(this.ksqldbURL + "/query-stream", 58 | { 59 | sql: validatedQuery, 60 | }, 61 | { 62 | headers: 63 | this.API && this.secret ? 64 | { 65 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 66 | } 67 | : 68 | {}, 69 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 70 | }) 71 | .then((res) => res.data) 72 | .catch((error) => { 73 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 74 | }); 75 | } 76 | 77 | /** 78 | * Executes a push query, and returns the results one row at a time. 79 | * 80 | *

This method may be used to issue a push query against a stream, with the first piece of data being an object 81 | * containing queryId, the id of the push query that can be used to terminate the push query being executed. 82 | * Otherwise, the push query will continuously run until terminated, returning results one at a time. 83 | * 84 | *

If user input is used to build the query, please use the queryBuilder method to protect against sql injection. 85 | * 86 | * @param {string} query sql statement of query to execute 87 | * @param {function} cb a callback function that is ran against each piece of data returned. 88 | * @return {Promise} a promise that completes once the server response is received, and contains the query 89 | * result if successful. 90 | */ 91 | push(query, cb) { 92 | validateInputs([query, 'string', 'query', true], [cb, 'function', 'cb', true]); 93 | const validatedQuery = builder.build(query); 94 | 95 | return new Promise((resolve, reject) => { 96 | let sentQueryId = false; 97 | const session = http2.connect( 98 | this.ksqldbURL, 99 | this.httpsAgentHttp2 ? this.httpsAgentHttp2 : {} 100 | ); 101 | 102 | session.on("error", (err) => reject(err)); 103 | 104 | const req = session.request( 105 | this.secret && this.API ? 106 | { 107 | ":path": "/query-stream", 108 | ":method": "POST", 109 | "Authorization": this.API && this.secret ? `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}` : '', 110 | } 111 | : 112 | { 113 | ":path": "/query-stream", 114 | ":method": "POST", 115 | } 116 | ); 117 | 118 | const reqBody = { 119 | sql: validatedQuery, 120 | Accept: "application/json, application/vnd.ksqlapi.delimited.v1", 121 | }; 122 | 123 | req.write(JSON.stringify(reqBody), "utf8"); 124 | req.end(); 125 | req.setEncoding("utf8"); 126 | 127 | req.on("data", (chunk) => { 128 | // check for chunk containing errors 129 | if (JSON.parse(chunk)['@type']?.includes('error')) throw new ksqlDBError(JSON.parse(chunk)); 130 | // continue if chunk indicates a healthy response 131 | if (!sentQueryId) { 132 | sentQueryId = true; 133 | cb(chunk); 134 | resolve(JSON.parse(chunk)?.queryId) 135 | } 136 | else { 137 | cb(chunk); 138 | } 139 | }); 140 | 141 | req.on("end", () => session.close()); 142 | }) 143 | } 144 | 145 | /** 146 | * Executes a terminate query that ends a push query. 147 | * 148 | *

This method may be used to end an active push query, and returns an object signifying whether the push query was 149 | * terminated properly or not. 150 | * 151 | *

This method is sql injection protected with the use of queryBuilder. 152 | * 153 | * @param {string} queryId a string that is the id of the push query to be terminated. 154 | * @return {Promise} a promise that completes once the server response is received, and is an object that signifies 155 | * if the termination was successful. 156 | */ 157 | terminate(queryId) { 158 | validateInputs([queryId, 'string', 'queryId']); 159 | 160 | return axios.post(this.ksqldbURL + '/ksql', 161 | { 162 | ksql: `TERMINATE ${queryId};` 163 | }, 164 | { 165 | headers: 166 | this.API && this.secret ? 167 | { 168 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 169 | } 170 | : 171 | {}, 172 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 173 | }) 174 | .then(res => res.data[0]) 175 | .catch(error => { 176 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 177 | }); 178 | } 179 | 180 | /** 181 | * Executes a query and returns the result(s). 182 | * 183 | *

This method may be used to issue custom sql queries against ksqldb without constraints. 184 | * 185 | *

If user input is used to build the query, please use the queryBuilder method to protect against sql injection. 186 | * 187 | * @param {string} query statement of a query to execute. 188 | * @return {Promise} a promise that completes once the server response is received, and returns the requested data. 189 | */ 190 | ksql(query) { 191 | validateInputs([query, 'string', 'query']); 192 | 193 | const validatedQuery = builder.build(query); 194 | 195 | return axios.post(this.ksqldbURL + '/ksql', 196 | { 197 | ksql: validatedQuery 198 | }, 199 | { 200 | headers: 201 | this.API && this.secret ? 202 | { 203 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 204 | } 205 | : 206 | {}, 207 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 208 | }) 209 | .then(res => res.data[0]) 210 | .catch(error => { 211 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 212 | }); 213 | } 214 | 215 | /** 216 | * Executes a query to create a stream. 217 | * 218 | *

This method is used to create a stream. 219 | * 220 | *

This method is sql injection protected with the use of queryBuilder. 221 | * 222 | * @param {string} name the name of the stream to be created. 223 | * @param {array} columnsType an array that contains the name of the columns and the associated types e.g [name VARCHAR, age INTEGER, ...] 224 | * @param {string} topic the name of the topic the stream is listening to. The topic is created if it does not currently exist. 225 | * @param {string} value_format a string specifying the value format. 226 | * @param {integer} partitions the number of partitions the stream should have. 227 | * @param {integer} key the key of the string. 228 | * @return {Promise} a promise that completes once the server response is received, and returns a response object. 229 | */ 230 | createStream(name, columnsType, topic, value_format = 'json', partitions = 1, key) { 231 | validateInputs([name, 'string', 'name', true], [columnsType, 'array', 'columnsType', true], [topic, 'string', 'topic'], [partitions, 'number', 'partitions']); 232 | 233 | const columnsTypeString = columnsType.reduce((result, currentType) => result + ', ' + currentType); 234 | const query = `CREATE STREAM ${name} (${columnsTypeString}) WITH (kafka_topic='${topic}', value_format='${value_format}', partitions=${partitions});`; 235 | 236 | return axios.post(this.ksqldbURL + '/ksql', { ksql: query }, { 237 | headers: 238 | this.API && this.secret ? 239 | { 240 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 241 | } 242 | : 243 | {}, 244 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 245 | }) 246 | .catch(error => { 247 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 248 | }); 249 | } 250 | 251 | /** 252 | * 253 | * @param {string} streamName - the name of the stream to be created 254 | * @param {string[]} selectColumns - the columns from the underlying stream to be included in the new materialized stream 255 | * @param {string} sourceStream - the underlying stream from which the new materialized stream will be created 256 | * @param {object} propertiesObj - an object whose keys are property names and values are the associated values 257 | * @param {string} conditions - a string containing the conditional statement (i.e., the 'WHERE' statement) 258 | * @param {string} partitionBy - column by which data will be distributed 259 | * @returns {Promise} - a promise that completes once the server response is received, and returns a query ID 260 | */ 261 | createStreamAs = (streamName, selectColumns, sourceStream, propertiesObj, conditions, partitionBy) => { 262 | validateInputs([streamName, 'string', 'streamName', true], [selectColumns, 'array', 'selectColumns', true], [sourceStream, 'string', 'sourceStream', true], [propertiesObj, 'object', 'propertiesObj'], [conditions, 'string', 'conditions'], [partitionBy, 'string', 'partitionBy']); 263 | 264 | const propertiesArgs = []; 265 | const selectColStr = selectColumns.reduce((result, current) => result + ', ' + current); 266 | // begin with first consistent portion of query 267 | let builderQuery = 'CREATE STREAM ? '; 268 | 269 | // include properties in query if provided 270 | if (Object.keys(propertiesObj).length > 0) { 271 | builderQuery += 'WITH ('; 272 | for (const [key, value] of Object.entries(propertiesObj)) { 273 | const justStarted = builderQuery[builderQuery.length - 1] === '('; 274 | 275 | if (!justStarted) builderQuery += ', '; 276 | builderQuery += '? = ?'; 277 | propertiesArgs.push([key], value); 278 | }; 279 | builderQuery += ') '; 280 | } 281 | 282 | // continue building the query to be sent to the builder 283 | builderQuery += `AS SELECT ${selectColStr} FROM ? `; 284 | if (conditions.indexOf(';') === -1) builderQuery += `WHERE ${conditions} `; 285 | builderQuery += partitionBy || ''; 286 | builderQuery += 'EMIT CHANGES;' 287 | 288 | // utilize query with variables to build actual query 289 | const query = builder.build(builderQuery, [streamName], ...propertiesArgs, [sourceStream]); 290 | 291 | return axios.post(this.ksqldbURL + '/ksql', { ksql: query }, { 292 | headers: 293 | this.API && this.secret ? 294 | { 295 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 296 | } 297 | : 298 | {}, 299 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 300 | }) 301 | .then(res => res.data[0].commandStatus.queryId) 302 | .catch(error => { 303 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 304 | }); 305 | } 306 | 307 | //---------------------Create tables----------------- 308 | /** 309 | * Executes a query to create a table. 310 | * 311 | *

This method is used to create a table. 312 | * 313 | *

This method is sql injection protected with the use of queryBuilder. 314 | * 315 | * @param {string} name the name of the table to be created. 316 | * @param {array} columnsType an array that contains the name of the columns and the associated types e.g [name VARCHAR, age INTEGER, ...] 317 | * @param {string} topic name of the topic the table is listening to. The topic is created if it does not currently exist. 318 | * @param {string} value_format string specifying the value format. 319 | * @param {integer} partitions number of partitions the table should have. 320 | * @return {Promise} a promise that completes once the server response is received, and returns a response object. 321 | */ 322 | createTable = (name, columnsType, topic, value_format = 'json', partitions) => { 323 | validateInputs([name, 'string', 'name', true], [columnsType, 'array', 'columnsType', true], [topic, 'string', 'topic', true], [value_format, 'string', 'value_format', true], [partitions, 'number', 'partitions']); 324 | 325 | const columnsTypeString = columnsType.reduce((result, currentType) => result + ', ' + currentType); 326 | const query = `CREATE TABLE ${name} (${columnsTypeString}) WITH (kafka_topic='${topic}', value_format='${value_format}', partitions=${partitions});` 327 | 328 | axios.post(this.ksqldbURL + '/ksql', 329 | { 330 | ksql: query 331 | }, 332 | { 333 | headers: 334 | this.API && this.secret ? 335 | { 336 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 337 | } 338 | : 339 | {}, 340 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 341 | }) 342 | .catch(error => { 343 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 344 | }); 345 | } 346 | 347 | //---------------------Create tables as select----------------- 348 | /** 349 | * Execute a query to create a new materialized table view of an existing table or stream 350 | * 351 | *

This method is used to create a materialized table view 352 | * 353 | *

This method is sql injection protected with the use of queryBuilder. 354 | * 355 | * @param {string} tableName name of the table to be created 356 | * @param {string} source name of the source stream / table materialized view is based on 357 | * @param {array} selectArray an array that contains the values (strings, aggregate functions) of the columns for the materialized view table 358 | * @param {object} propertiesObj an object containing key value pairs for supported table properties e.g {topic: 'myTopic', value_format: 'json', partitions: '1'}. {} for default values 359 | * @param {object} conditionsObj an object containing key value pairs for supported query conditions e.g {WHERE: 'a is not null', GROUP_BY: 'profileID', HAVING: 'COUNT(a) > 5' } 360 | * @returns {Promise} a promise that completes once the server response is received, returning a response object 361 | */ 362 | createTableAs = (tableName, source, selectArray, propertiesObj, conditionsObj) => { 363 | validateInputs([tableName, 'string', 'tableName', true], [source, 'string', 'source', true], [selectArray, 'array', 'selectArray', true], [propertiesObj, 'object', 'propertiesObj'], [conditionsObj, 'object', 'conditionsObj']); 364 | 365 | let selectColStr = selectArray.reduce((result, current) => result + ', ' + current); 366 | 367 | // expect user to input properties object of format {topic: ... , value_format: ..., partitions: ...} 368 | // check for properties object, look for properties, if any are missing assign it a default value, if there's no property 369 | const defaultProps = { 370 | topic: tableName, 371 | value_format: 'json', 372 | partitions: 1 373 | }; 374 | Object.assign(defaultProps, propertiesObj); 375 | 376 | // if there's no properties Obj, assign them all default values 377 | 378 | // expect user to input a conditions object of format {WHERE: condition, GROUP_BY: condition, HAVING: condition}; 379 | // generate conditions string based on object 380 | // const builder = new queryBuilder(); 381 | 382 | let conditionQuery = ''; 383 | if (conditionsObj) { 384 | const conditionsArr = ['WHERE', 'GROUP_BY', 'HAVING']; 385 | const sqlClauses = []; 386 | 387 | let i = 0; 388 | while (conditionsArr.length) { 389 | if (conditionsObj[conditionsArr[0]]) { 390 | sqlClauses[i] = [conditionsArr[0].replace('_', ' ')]; // clause values are set as arrays for query builder 391 | sqlClauses[i + 1] = [' ' + conditionsObj[conditionsArr[0]] + ' ']; 392 | } 393 | else { 394 | sqlClauses[i] = ['']; 395 | sqlClauses[i + 1] = ['']; 396 | } 397 | i += 2; 398 | conditionsArr.shift() 399 | } 400 | conditionQuery = builder.build(`${sqlClauses[0][0]}${sqlClauses[1][0]}??${sqlClauses[4][0]}${sqlClauses[5][0]}`, sqlClauses[2], sqlClauses[3]); 401 | } 402 | 403 | // reformat for builder 404 | tableName = [tableName]; 405 | selectColStr = [selectColStr]; 406 | source = [source]; 407 | conditionQuery = [conditionQuery] 408 | 409 | 410 | const query = builder.build(`CREATE TABLE ? WITH (kafka_topic=?, value_format=?, partitions=?) AS SELECT ? FROM ? ${conditionQuery} EMIT CHANGES;`, tableName, defaultProps.topic, defaultProps.value_format, defaultProps.partitions, selectColStr, source) 411 | return axios.post(this.ksqldbURL + '/ksql', { ksql: query }, { 412 | headers: 413 | this.API && this.secret ? 414 | { 415 | "Authorization": `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}`, 416 | } 417 | : 418 | {}, 419 | httpsAgent: this.httpsAgentAxios ? this.httpsAgentAxios : null, 420 | }) 421 | .catch(error => console.log(error)); 422 | } 423 | 424 | /** 425 | * Inserts rows of data into a stream. 426 | * 427 | *

This method may be used to insert new rows of data into a stream. 428 | * 429 | *

This method is sql injection protected with the use of queryBuilder. 430 | * 431 | * @param {string} stream the name of the stream to insert data into. 432 | * @param {object} rows an array that contains data that is being inserted into the stream. 433 | * @return {Promise} this method returns a promise that resolves into an array describing the status of the row inserted. 434 | */ 435 | //---------------------Insert Rows Into Existing Streams----------------- 436 | insertStream = (stream, rows) => { 437 | validateInputs([stream, 'string', 'stream', true], [rows, 'array', 'rows', true]); 438 | 439 | return new Promise((resolve, reject) => { 440 | const msgOutput = []; 441 | 442 | const session = http2.connect( 443 | this.ksqldbURL, 444 | this.httpsAgentHttp2 ? this.httpsAgentHttp2 : {} 445 | ); 446 | 447 | session.on("error", (err) => reject(err)); 448 | 449 | const req = session.request( 450 | this.secret && this.API ? 451 | { 452 | ":path": "/inserts-stream", 453 | ":method": "POST", 454 | "Authorization": this.API && this.secret ? `Basic ${Buffer.from(this.API + ":" + this.secret, 'utf8').toString('base64')}` : '', 455 | } 456 | : 457 | { 458 | ":path": "/inserts-stream", 459 | ":method": "POST", 460 | } 461 | ); 462 | 463 | let reqBody = `{ "target": "${stream}" }`; 464 | 465 | for (let row of rows) { 466 | reqBody += `\n${JSON.stringify(row)}`; 467 | } 468 | 469 | req.write(reqBody, "utf8"); 470 | req.end(); 471 | req.setEncoding("utf8"); 472 | 473 | req.on("data", (chunk) => { 474 | // check for chunk containing errors 475 | if (JSON.parse(chunk)['@type']?.includes('error')) throw new ksqlDBError(JSON.parse(chunk)); 476 | // continue if chunk indicates a healthy response 477 | msgOutput.push(JSON.parse(chunk)); 478 | }); 479 | 480 | req.on("end", () => { 481 | session.close(); 482 | resolve(msgOutput); 483 | }); 484 | }) 485 | } 486 | 487 | /** 488 | * Pulls data between two different time points. 489 | * 490 | *

This method may be used to pull data from within two specific points in time. The first three 491 | * parameters are required, with the fourth parameter being optional. 492 | * 493 | *

This method is sql injection protected with the use of queryBuilder. 494 | * 495 | * @param {string} streamName the name of the stream to pull data from. 496 | * @param {string} timeZone desired timezone that the data should conform to. 497 | * @param {array} from array of the format ['2200-01-01', '16', '10', '20'], with the values being 498 | * date, hour, minute, and second respectively. 499 | * @param {array} to array of the format ['2000-01-01', '16', '10', '20'], with the values being 500 | * date, hour, minute, and second respectively. This defaults to ['2200-03-14', '00', '00', '00']. 501 | * @return {array} this method returns an array that contains arrays with the data, along with an extra value at 502 | * the end of the array that includes the time that the data was inserted into the ksqldb. 503 | */ 504 | pullFromTo = async (streamName, timezone = 'Greenwich', from = [undefined, '00', '00', '00'], to = ['2200-03-14', '00', '00', '00']) => { 505 | validateInputs([streamName, 'string', 'streamName', true], [timezone, 'string', 'timezone', true], [from, 'array', 'from', true], [to, 'array', 'to', true]); 506 | 507 | const userFrom = `${from[0]}T${from[1]}:${from[2]}:${from[3]}`; 508 | const userTo = `${to[0]}T${to[1]}:${to[2]}:${to[3]}`; 509 | const userFromUnix = new Date(userFrom).getTime(); 510 | const userToUnix = new Date(userTo).getTime(); 511 | const query = builder.build("SELECT *, CONVERT_TZ(FROM_UNIXTIME(ROWTIME), 'UTC', ?) AS DATE, ROWTIME FROM ?;", timezone, [streamName]); 512 | const data = await this.pull(query); 513 | data.shift(); 514 | const filtered = []; 515 | data.map((element) => { 516 | if (element[element.length - 1] >= userFromUnix && element[element.length - 1] <= userToUnix) { 517 | filtered.push(element.slice(0, element.length - 1)); 518 | } 519 | }) 520 | return filtered; 521 | } 522 | 523 | /** 524 | * Inspects a specific query and returns the results. 525 | * 526 | * @link https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-rest-api/status-endpoint/ 527 | * 528 | *

This method may be used to inspect the status of a query. 529 | * 530 | * @param {string} commandId this id is obtained when using the .ksql method (/ksql endpoint) to run CREATE, DROP, TERMINATE commands. 531 | * @return {Promise} this method returns a promise, that resolves to a JSON object that has the following two properties-> 532 | * 533 | * status (string): One of QUEUED, PARSING, EXECUTING, TERMINATED, SUCCESS, or ERROR. 534 | * 535 | * message (string): Detailed message regarding the status of the execution statement. 536 | */ 537 | inspectQueryStatus(commandId) { 538 | validateInputs([commandId, 'string', 'commandId', true]); 539 | 540 | return axios.get(this.ksqldbURL + `/status/${commandId}`) 541 | .then(response => response) 542 | .catch(error => { 543 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 544 | }); 545 | } 546 | 547 | /** 548 | * Inspects a ksqlDB server and returns the results. 549 | * 550 | * @link https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-rest-api/info-endpoint/ 551 | * 552 | *

This method is mainly used for troubleshooting. 553 | * 554 | * @return {Promise} this method returns a promise that resolves to an object containing the version, clusterId, and ksqlservice id. 555 | */ 556 | inspectServerInfo() { 557 | return axios.get(this.ksqldbURL + `/info`) 558 | .then(response => response) 559 | .catch(error => { 560 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 561 | }); 562 | } 563 | 564 | /** 565 | * Inspects the health status of a ksqlDB server. 566 | * 567 | * @link https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-rest-api/info-endpoint/ 568 | * 569 | *

This method may be used to give the health status of a ksqlDB server. 570 | * 571 | * @return {Promise} this method returns a promise that resolves to an object containing the metastore, kafka, and commandRunner info. 572 | */ 573 | inspectServerHealth() { 574 | return axios.get(this.ksqldbURL + `/healthcheck`) 575 | .then(response => response) 576 | .catch(error => { 577 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 578 | }); 579 | } 580 | 581 | /** 582 | * Inspects all servers in a ksqlDB cluster and returns the results. 583 | * 584 | * @link https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-rest-api/cluster-status-endpoint/ 585 | * 586 | *

This method may be used to get information about the status of all ksqlDB servers in a ksqlDB cluster, which can be useful 587 | * for troubleshooting. 588 | * 589 | * @return {Promise} this method returns a promise that resolves to an object containing information about the ksqlDB servers. 590 | */ 591 | inspectClusterStatus() { 592 | return axios.get(this.ksqldbURL + `/clusterStatus`) 593 | .then(response => response) 594 | .catch(error => { 595 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 596 | }); 597 | } 598 | 599 | /** 600 | * Terminates a ksqlDB cluster. 601 | * 602 | * @link https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-rest-api/terminate-endpoint/ 603 | * 604 | *

This method may be used to terminate a ksqlDB cluster. First, shut down all the servers except one. 605 | * 606 | * @param {string[]} topicsToDelete an array of topic names or regular expressions for topic names to delete. 607 | * @return {Promise} this method returns a promise that returns a response object. 608 | */ 609 | terminateCluster(topicsToDelete = []) { 610 | validateInputs([topicsToDelete, 'array', 'topicsToDelete', true]); 611 | 612 | return axios.post(this.ksqldbURL + `/ksql/terminate`, { 613 | "deleteTopicList": topicsToDelete 614 | }, { 615 | headers: { 616 | // 'application/json' is the modern content-type for JSON, but some 617 | // older servers may use 'text/json'. 618 | 'Accept': 'application/vnd.ksql.v1+json', 619 | 'Content-Type': 'application/vnd.ksql.v1+json' 620 | } 621 | }) 622 | .then(response => response) 623 | .catch(error => { 624 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 625 | }); 626 | } 627 | 628 | /** 629 | * Checks whether a ksqldb server property is allowed to be changed. 630 | * 631 | * @link https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-rest-api/is_valid_property-endpoint/ 632 | * 633 | *

This method may be used to check if a property in a ksqldb server is prohibited from being changed. 634 | * 635 | *

If the property is prohibited from setting, the following object will be returned: 636 | * { 637 | * "@type": "generic_error", 638 | * "error_code": 40000, 639 | * "message": "One or more properties overrides set locally are prohibited by the KSQL server (use UNSET to reset their default value): [ksql.service.id]" 640 | * } 641 | * 642 | * @param {string} propertyName - the name of the property to validate 643 | * @return {Promise} this method returns a promise that resolves to a boolean true if the property is allowed to be changed. 644 | */ 645 | isValidProperty(propertyName) { 646 | validateInputs([propertyName, 'string', 'propertyName', true]); 647 | 648 | return axios.get(this.ksqldbURL + `/is_valid_property/${propertyName}`) 649 | .then(response => response) 650 | .catch(error => { 651 | throw error.response?.data['@type'] ? new ksqlDBError(error.response.data) : error; 652 | }); 653 | } 654 | }; 655 | 656 | module.exports = ksqldb; 657 | -------------------------------------------------------------------------------- /ksQlient/ksqldb/queryBuilder.js: -------------------------------------------------------------------------------- 1 | const { QueryBuilderError, EmptyQueryError, NumParamsError, InappropriateStringParamError } = require('./customErrors.js'); 2 | 3 | class queryBuilder { 4 | constructor() { 5 | } 6 | 7 | build = (query, ...params) => { 8 | // check for empty query 9 | if (this._checkEmptyQuery(query)) throw new EmptyQueryError(); 10 | 11 | let output = this._bind(query, ...params); 12 | 13 | return output; 14 | } 15 | _bind = (query, ...params) => { 16 | const numParams = params.length; 17 | // count num of ? in query 18 | const numMark = query.split("?").length - 1; 19 | if (numParams > numMark) { throw new NumParamsError('more params than wildcards in query') }; 20 | if (numParams < numMark) { throw new NumParamsError('less params than wildcards in query') }; 21 | for (let i = 0; i < numParams; i++) { 22 | const newParam = this._replaceWith(params[i]); 23 | // if (newParam instanceof Error) return newParam; 24 | query = query.replace(/\?/, newParam); 25 | }; 26 | return query; 27 | } 28 | _replaceWith = (param) => { 29 | switch (typeof param) { 30 | case "number": 31 | return param; 32 | case "bigint": 33 | return param; 34 | case "boolean": 35 | return param; 36 | case "object": 37 | if (Array.isArray(param)) { 38 | if (param[0]?.includes(";")) { 39 | throw new InappropriateStringParamError("string params not wrapped in quotes should not include semi-colons"); 40 | } 41 | return `${param[0]?.replaceAll("'", "''")}` 42 | } 43 | throw new QueryBuilderError("object should not be passed in as query argument"); 44 | case "function": 45 | throw new QueryBuilderError("function should not be passed in as query argument"); 46 | case "string": 47 | // https://stackoverflow.com/questions/5139770/escape-character-in-sql-server 48 | // example of injection in Go: https://go.dev/play/p/4KoWROjK903 49 | return `'${param.replaceAll("'", "''")}'`; 50 | default: 51 | // code block 52 | } 53 | }; 54 | _checkEmptyQuery = (query) => { 55 | if (query === "" || query === undefined || query === null) { 56 | return true; 57 | } 58 | return false; 59 | } 60 | } 61 | module.exports = queryBuilder; 62 | -------------------------------------------------------------------------------- /ksQlient/ksqldb/validateInputs.js: -------------------------------------------------------------------------------- 1 | const { invalidArgumentTypes } = require("./customErrors"); 2 | 3 | const validateInputs = (...args) => { 4 | const invalidArguments = []; 5 | 6 | // iterate through args to verify allowed types are provided 7 | for (let i = 0; i < args.length; i++) { 8 | const [currentArg, intendedType, actualType, required] = [args[i][0], args[i][1], typeof args[i][0], args[i][3] || false]; 9 | 10 | if (intendedType === 'array') { 11 | if (!Array.isArray(currentArg)) invalidArguments.push(args[i]); 12 | } 13 | else if (required && actualType !== intendedType) invalidArguments.push(args[i]); 14 | else if (!required && currentArg !== undefined && currentArg !== null && actualType !== intendedType) invalidArguments.push(args[i]); 15 | } 16 | 17 | // craft error message if error needs to be thrown 18 | if (invalidArguments.length) { 19 | let errorMessage = ''; 20 | 21 | for (let i = 0; i < invalidArguments.length; i++) { 22 | errorMessage += `argument "${invalidArguments[i][2]}" must be of type ${invalidArguments[i][1]}`; 23 | if (i < invalidArguments.length - 1) errorMessage += ', ' 24 | } 25 | throw new invalidArgumentTypes(errorMessage); 26 | } 27 | } 28 | 29 | module.exports = validateInputs; -------------------------------------------------------------------------------- /ksQlient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ksqlient", 3 | "version": "1.0.1", 4 | "description": "Javascript KsqlDB client for Node.js", 5 | "main": "./ksqldb/ksqldb", 6 | "scripts": { 7 | "test": "jest --verbose --forceExit" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oslabs-beta/ksqlSuite.git" 12 | }, 13 | "keywords": [], 14 | "author": "Michael Synder, Matthew Xing, Jonathan Luu, Gerry Bong, Javan Ang", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/oslabs-beta/ksqlSuite/issues" 18 | }, 19 | "homepage": "https://github.com/oslabs-beta/ksqlSuite#readme", 20 | "dependencies": { 21 | "axios": "^0.27.2" 22 | }, 23 | "devDependencies": { 24 | "jest": "^28.1.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ksQlient/static/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksQlient/static/light.png -------------------------------------------------------------------------------- /ksQlient/static/name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksQlient/static/name.png -------------------------------------------------------------------------------- /ksqLight/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ksqLight/README.md: -------------------------------------------------------------------------------- 1 | # ksqLight 2 | 3 |

4 | logo 5 |
6 | 7 | # About 8 | 9 | ksqLight is an open-source tool for **monitoring your ksqlDB queries and messages in real-time**. Its interactive dashboard visualizes ksqlDB query and messages metrics, to help in diagnosing stream processing bottlenecks and identifying network issues. 10 | 11 | Under the hood, it uses Prometheus to pull the relevant metrics from ksqlDB server. 12 | 13 | # Features 14 | 15 | ![](./src/static/landing.gif) 16 | 17 | ## Metrics 18 | 19 | Quickly toggle between query, message, and error pages. 20 | 21 | ![](./src/static/sidebar.gif) 22 | 23 | ## Dark Mode 24 | 25 | ![](./src/static/darkmode.gif) 26 | 27 | ## Submit SQL queries to ksqlDB server (in progress) 28 | 29 | # Setup 30 | 31 | > **Note** 32 | > This app assumes you have access to a working ksqlDB server and a Prometheus instance scraping metrics from ksqlDB server via JMX exporter. If you need a reference on how to do that, please check out the `ksqLight/exampleContainers` folder. 33 | 34 | ## Clone repo, cd into it, then run: 35 | 36 | ``` 37 | 38 | npm run electron:serve 39 | 40 | ``` 41 | 42 | Upon startup, enter the Prometheus url into the pop-up (or you can enter it on the settings modal). 43 | 44 | The duration and refresh rate for the time-series charts can be configured on the settings modal. 45 | 46 | To submit SQL queries to ksqlDB server, the server url needs to be entered in settings. 47 | 48 | ![](./src/static/settings.gif) 49 | 50 | # Developers 51 | 52 | - Javan Ang - [GitHub](https://github.com/javanang) | [LinkedIn](https://www.linkedin.com/in/javanang/) 53 | - Michael Snyder - [GitHub](https://github.com/MichaelCSnyder) | [LinkedIn](https://www.linkedin.com/in/michaelcharlessnyder/) 54 | - Jonathan Luu - [GitHub](https://github.com/jonathanluu17) | [LinkedIn](https://www.linkedin.com/in/jonathanluu17/) 55 | - Matthew Xing - [GitHub](https://github.com/matthewxing1) | [LinkedIn](https://www.linkedin.com/in/matthew-xing/) 56 | - Gerry Bong - [GitHub](https://github.com/ggbong734) | [LinkedIn](https://www.linkedin.com/in/gerry-bong-71137420/) 57 | 58 | # License 59 | 60 | This product is licensed under the MIT License - see the LICENSE.md file for details. 61 | 62 | This is an open source product. 63 | 64 | This product is accelerated by OS Labs. 65 | 66 | ksqlDB is licensed under the [Confluent Community License](https://github.com/confluentinc/ksql/blob/master/LICENSE). 67 | 68 | _Apache, Apache Kafka, Kafka, and associated open source project names are trademarks of the [Apache Software Foundation](https://www.apache.org/)_. 69 | -------------------------------------------------------------------------------- /ksqLight/example_containers/docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3.2" 3 | 4 | services: 5 | # prometheus: 6 | # image: prom/prometheus 7 | # container_name: prometheus 8 | # ports: 9 | # - 9090:9090 10 | # - 8080:8080 11 | # volumes: 12 | # - ./prometheus/:/etc/prometheus/ 13 | zookeeper: 14 | image: confluentinc/cp-zookeeper:7.0.1 15 | hostname: zookeeper 16 | container_name: zookeeper 17 | ports: 18 | - "2181:2181" 19 | environment: 20 | ZOOKEEPER_CLIENT_PORT: 2181 21 | ZOOKEEPER_TICK_TIME: 2000 22 | 23 | broker: 24 | image: confluentinc/cp-kafka:7.0.1 25 | hostname: broker 26 | container_name: broker 27 | depends_on: 28 | - zookeeper 29 | ports: 30 | - "29092:29092" 31 | environment: 32 | KAFKA_BROKER_ID: 1 33 | KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" 34 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT 35 | KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:9092,PLAINTEXT_HOST://localhost:29092 36 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 37 | KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 38 | KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 39 | KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 40 | 41 | ksqldb-server: 42 | image: confluentinc/ksqldb-server:0.25.1 43 | hostname: ksqldb-server 44 | container_name: ksqldb-server 45 | volumes: 46 | - ./jmx_exporter:/usr/share/jmx_exporter/ 47 | - type: bind 48 | source: ./ 49 | target: /home/appuser 50 | depends_on: 51 | - broker 52 | ports: 53 | - "8088:8088" 54 | - "1090:1099" 55 | environment: 56 | KSQL_LISTENERS: http://0.0.0.0:8088 57 | KSQL_BOOTSTRAP_SERVERS: broker:9092 58 | KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true" 59 | KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true" 60 | # KSQL_KSQL_OPTS: "-Djava.security.auth.login.config=/jaas_config.file" 61 | # KSQL_AUTHENTICATION_METHOD: BASIC 62 | # KSQL_AUTHENTICATION_REALM: KsqlServer-Props 63 | # KSQL_AUTHENTICATION_ROLES: admin,developer,user 64 | # KSQL_SSL_CLIENT_AUTHENTICATION: NONE 65 | # KSQL_SSL_TRUSTSTORE_LOCATION: ksqldb_server_config/kafka.server.truststore.jks 66 | # KSQL_SSL_TRUSTSTORE_PASSWORD: ${SSL_PASSWORD} 67 | # KSQL_SSL_KEYSTORE_LOCATION: ksqldb_server_config/kafka.server.keystore.jks 68 | # KSQL_SSL_KEYSTORE_PASSWORD: ${SSL_PASSWORD} 69 | # KSQL_SSL_KEY_PASSWORD: ${SSL_PASSWORD} 70 | KSQL_KSQL_HEARTBEAT_ENABLE: "true" 71 | KSQL_KSQL_LAG_REPORTING_ENABLE: "true" 72 | KSQL_KSQL_PULL_METRICS_ENABLED: "true" 73 | KSQL_JMX_OPTS: > 74 | -Dcom.sun.management.jmxremote.authenticate=false 75 | -Dcom.sun.management.jmxremote.ssl=false 76 | -Djava.util.logging.config.file=logging.properties 77 | -javaagent:/usr/share/jmx_exporter/jmx_prometheus_javaagent-0.17.0.jar=7010:/usr/share/jmx_exporter/ksqldb.yml 78 | ksqldb-cli: 79 | image: confluentinc/ksqldb-cli:0.25.1 80 | container_name: ksqldb-cli 81 | depends_on: 82 | - broker 83 | - ksqldb-server 84 | entrypoint: /bin/sh 85 | tty: true 86 | prometheus: 87 | image: prom/prometheus 88 | container_name: prometheus 89 | ports: 90 | - 9090:9090 91 | - 8080:8080 92 | volumes: 93 | - ./prometheus/:/etc/prometheus/ 94 | # Possible JMX OPT alternative? 95 | # -Djava.rmi.server.hostname=localhost 96 | # -Dcom.sun.management.jmxremote 97 | # -Dcom.sun.management.jmxremote.port=1099 98 | # -Dcom.sun.management.jmxremote.authenticate=false 99 | # -Dcom.sun.management.jmxremote.ssl=false 100 | # -Dcom.sun.management.jmxremote.rmi.port=1099docke 101 | -------------------------------------------------------------------------------- /ksqLight/example_containers/jmx_exporter/jmx_prometheus_javaagent-0.17.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/example_containers/jmx_exporter/jmx_prometheus_javaagent-0.17.0.jar -------------------------------------------------------------------------------- /ksqLight/example_containers/jmx_exporter/ksqldb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | lowercaseOutputName: true 3 | lowercaseOutputLabelNames: true 4 | whitelistObjectNames: 5 | - "io.confluent.ksql.metrics:*" 6 | # The two lines below are used to pull the Kafka Client Producer & consumer metrics from KSQL Client. 7 | # If you care about Producer/Consumer metrics for KSQL, please uncomment 2 lines below. 8 | # Please note that this increases the scrape duration to about 1 second as it needs to parse a lot of data. 9 | - "kafka.consumer:*" 10 | - "kafka.producer:*" 11 | - "kafka.streams:*" 12 | blacklistObjectNames: 13 | - kafka.streams:type=kafka-metrics-count 14 | # This will ignore the admin client metrics from KSQL server and will blacklist certain metrics 15 | # that do not make sense for ingestion. 16 | - "kafka.admin.client:*" 17 | - "kafka.consumer:type=*,id=*" 18 | - "kafka.consumer:type=*,client-id=*" 19 | - "kafka.consumer:type=*,client-id=*,node-id=*" 20 | - "kafka.producer:type=*,id=*" 21 | - "kafka.producer:type=*,client-id=*" 22 | - "kafka.producer:type=*,client-id=*,node-id=*" 23 | - "kafka.streams:type=stream-processor-node-metrics,thread-id=*,task-id=*,processor-node-id=*" 24 | - "kafka.*:type=kafka-metrics-count,*" 25 | rules: 26 | # "io.confluent.ksql.metrics:type=producer-metrics,key=*,id=*" 27 | # "io.confluent.ksql.metrics:type=consumer-metrics,key=*,id=*" 28 | - pattern: io.confluent.ksql.metrics<>([^:]+) 29 | name: ksql_$1_$4 30 | labels: 31 | key: "$2" 32 | id: "$3" 33 | # "io.confluent.ksql.metrics:type=_confluent-ksql-ksql-engine-query-stats" 34 | # The below statement parses KSQL Cluster Name and adds a new label so that per cluster data is searchable. 35 | - pattern: io.confluent.ksql.metrics<>([^:]+) 36 | name: "ksql_ksql_engine_query_stats_$2" 37 | labels: 38 | ksql_cluster: $1 39 | # "io.confluent.ksql.metrics:type=ksql-queries,status=_confluent-ksql-_query_ 40 | # The below statement parses KSQL query specific status 41 | - pattern: "io.confluent.ksql.metrics<>(.+): (.+)" 42 | value: 1 43 | name: ksql_ksql_metrics_$1_$4 44 | labels: 45 | ksql_query: $3 46 | ksql_cluster: $2 47 | $4: $5 48 | # kafka.streams:type=stream-processor-node-metrics,processor-node-id=*,task-id=*,thread-id=* 49 | # kafka.streams:type=stream-record-cache-metrics,record-cache-id=*,task-id=*,thread-id=* 50 | # kafka.streams:type=stream-state-metrics,rocksdb-state-id=*,task-id=*,thread-id=* 51 | # kafka.streams:type=stream-state-metrics,rocksdb-state-id=*,task-id=*,thread-id=* 52 | # - pattern: "kafka.streams<>(.+):" 53 | # name: kafka_streams_$1_$6 54 | # type: GAUGE 55 | # labels: 56 | # thread_id: "$2" 57 | # task_id: "$3" 58 | # $4: "$5" 59 | # kafka.streams:type=stream-task-metrics,task-id=*,thread-id=* 60 | # - pattern: "kafka.streams<>(.+):" 61 | # name: kafka_streams_$1_$4 62 | # type: GAUGE 63 | # labels: 64 | # thread_id: "$2" 65 | # task_id: "$3" 66 | # kafka.streams:type=stream-metrics,client-id=* 67 | # - pattern: "kafka.streams<>(state|alive-stream-threads|commit-id|version|application-id): (.+)" 68 | # name: kafka_streams_stream_metrics 69 | # value: 1 70 | # type: UNTYPED 71 | # labels: 72 | # $1: "$2" 73 | # $3: "$4" 74 | # kafka.streams:type=stream-thread-metrics,thread-id=* 75 | # - pattern: "kafka.streams<>([^:]+)" 76 | # name: kafka_streams_$1_$4 77 | # type: GAUGE 78 | # labels: 79 | # $2: "$3" 80 | # "kafka.consumer:type=app-info,client-id=*" 81 | # "kafka.producer:type=app-info,client-id=*" 82 | # - pattern: "kafka.(.+)<>(.+): (.+)" 83 | # value: 1 84 | # name: kafka_$1_app_info 85 | # labels: 86 | # client_type: $1 87 | # client_id: $2 88 | # $3: $4 89 | # type: UNTYPED 90 | # "kafka.consumer:type=consumer-metrics,client-id=*, protocol=*, cipher=*" 91 | # "kafka.consumer:type=type=consumer-fetch-manager-metrics,client-id=*, topic=*, partition=*" 92 | # "kafka.producer:type=producer-metrics,client-id=*, protocol=*, cipher=*" 93 | # - pattern: "kafka.(.+)<>(.+):" 94 | # name: kafka_$1_$2_$9 95 | # type: GAUGE 96 | # labels: 97 | # client_type: $1 98 | # $3: "$4" 99 | # $5: "$6" 100 | # $7: "$8" 101 | # "kafka.consumer:type=consumer-node-metrics,client-id=*, node-id=*" 102 | # "kafka.consumer:type=consumer-fetch-manager-metrics,client-id=*, topic=*" 103 | # "kafka.producer:type=producer-node-metrics,client-id=*, node-id=*" 104 | # "kafka.producer:type=producer-topic-metrics,client-id=*, topic=*" 105 | # - pattern: "kafka.(.+)<>(.+):" 106 | # name: kafka_$1_$2_$7 107 | # type: GAUGE 108 | # labels: 109 | # client_type: $1 110 | # $3: "$4" 111 | # $5: "$6" 112 | # "kafka.consumer:type=consumer-fetch-manager-metrics,client-id=*" 113 | # "kafka.consumer:type=consumer-metrics,client-id=*" 114 | # "kafka.producer:type=producer-metrics,client-id=*" 115 | # - pattern: "kafka.(.+)<>(.+):" 116 | # name: kafka_$1_$2_$5 117 | # type: GAUGE 118 | # labels: 119 | # client_type: $1 120 | # $3: "$4" 121 | # - pattern: "kafka.(.+)<>(.+):" 122 | # name: kafka_$1_$2_$3 123 | # labels: 124 | # client_type: $1 -------------------------------------------------------------------------------- /ksqLight/example_containers/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 10s 3 | evaluation_interval: 10s 4 | scrape_configs: 5 | - job_name: "ksqldb" 6 | static_configs: 7 | - targets: 8 | - ksqldb-server:7010 -------------------------------------------------------------------------------- /ksqLight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ksqlight", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.6.9", 7 | "@electron/remote": "^2.0.8", 8 | "@emotion/react": "^11.9.3", 9 | "@emotion/styled": "^11.9.3", 10 | "@mui/icons-material": "^5.8.4", 11 | "@mui/material": "^5.9.0", 12 | "@testing-library/jest-dom": "^5.16.4", 13 | "@testing-library/react": "^13.3.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "axios": "^0.27.2", 16 | "chart.js": "^3.8.0", 17 | "chartjs-plugin-streaming": "^2.0.0", 18 | "clsx": "^1.2.1", 19 | "concurrently": "^7.2.2", 20 | "cors": "^2.8.5", 21 | "cross-env": "^7.0.3", 22 | "express": "^4.18.1", 23 | "express-graphql": "^0.12.0", 24 | "graphql": "^15.8.0", 25 | "livereload": "^0.9.3", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-router-dom": "^6.3.0", 29 | "react-scripts": "5.0.1", 30 | "sass": "^1.54.0", 31 | "wait-on": "^6.0.1", 32 | "web-vitals": "^2.1.4" 33 | }, 34 | "main": "public/main.js", 35 | "homepage": "./", 36 | "scripts": { 37 | "start": "concurrently -k \"cross-env BROWSER=none npm run reactStart\" \"npm run electron:start\" \"node server/server.js\"", 38 | "reactStart": "react-scripts start", 39 | "build": "react-scripts build", 40 | "test": "react-scripts test", 41 | "eject": "react-scripts eject", 42 | "electron:serve": "concurrently -k \"cross-env BROWSER=none npm run reactStart\" \"npm run electron:start\" \"node server/server.js\"", 43 | "electron:build": "", 44 | "electron:start": "wait-on tcp:3000 && electron .", 45 | "devServer": "nodemon server/server.js", 46 | "dist": "electron-builder" 47 | }, 48 | "eslintConfig": { 49 | "extends": [ 50 | "react-app", 51 | "react-app/jest" 52 | ] 53 | }, 54 | "browserslist": { 55 | "production": [ 56 | ">0.2%", 57 | "not dead", 58 | "not op_mini all" 59 | ], 60 | "development": [ 61 | "last 1 chrome version", 62 | "last 1 firefox version", 63 | "last 1 safari version" 64 | ] 65 | }, 66 | "devDependencies": { 67 | "autoprefixer": "^10.4.7", 68 | "chartjs-adapter-moment": "^1.0.0", 69 | "electron": "^20.0.0", 70 | "electron-builder": "^23.3.3", 71 | "moment": "^2.29.4", 72 | "nodemon": "^2.0.19", 73 | "postcss": "^8.4.14", 74 | "tailwindcss": "^3.1.4" 75 | }, 76 | "build": { 77 | "appId": "com.ksqlsuite.ksqlight", 78 | "productName": "ksqLight", 79 | "target": "NSIS", 80 | "directories": { 81 | "output": "build", 82 | "buildResources":"resources" 83 | }, 84 | "nsis": { 85 | "allowToChangeInstallationDirectory": true, 86 | "oneClick": false 87 | }, 88 | "extends": null 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ksqLight/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /ksqLight/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /ksqLight/public/main.js: -------------------------------------------------------------------------------- 1 | const { app,BrowserWindow } = require("electron"); 2 | 3 | require("@electron/remote/main").initialize(); 4 | 5 | function createWindow () { 6 | const win = new BrowserWindow({ 7 | width: 1600, 8 | height: 1000, 9 | webPreferences: { 10 | enableRemoteModule: true, 11 | } 12 | }) 13 | 14 | // open dev tools 15 | // win.webContents.openDevTools(); 16 | win.loadURL("http://localhost:3000"); 17 | } 18 | 19 | app.on('ready', createWindow); 20 | 21 | //Handling of Mac OS 22 | app.on("window-all-closed", function () { 23 | if(process.platform !== "darwin"){ 24 | app.quit(); 25 | } 26 | }) 27 | 28 | app.on("activate", function () { 29 | if(BrowserWindow.getAllWindows().length === 0){ 30 | createWindow(); 31 | } 32 | }) -------------------------------------------------------------------------------- /ksqLight/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } -------------------------------------------------------------------------------- /ksqLight/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ksqLight/server/queryTypes.js: -------------------------------------------------------------------------------- 1 | const queryTypes = { 2 | runningQueries: "ksql_ksql_engine_query_stats_running_queries", 3 | rebalancingQueries: "ksql_ksql_engine_query_stats_rebalancing_queries", 4 | pendingShutdownQueries: "ksql_ksql_engine_query_stats_pending_shutdown_queries", 5 | pendingErrorQueries: "ksql_ksql_engine_query_stats_pending_error_queries", 6 | numPersistentQueries: "ksql_ksql_engine_query_stats_num_persistent_queries", 7 | numIdleQueries: "ksql_ksql_engine_query_stats_num_idle_queries", 8 | numActiveQueries: "ksql_ksql_engine_query_stats_num_active_queries", 9 | notRunningQueries: "ksql_ksql_engine_query_stats_not_running_queries", 10 | messagesProducedPerSec: "ksql_ksql_engine_query_stats_messages_produced_per_sec", 11 | messagesConsumedTotal: "ksql_ksql_engine_query_stats_messages_consumed_total", 12 | messagesConsumedPerSec: "ksql_ksql_engine_query_stats_messages_consumed_per_sec", 13 | messagesConsumedMin: "ksql_ksql_engine_query_stats_messages_consumed_min", 14 | messagesConsumedMax: "ksql_ksql_engine_query_stats_messages_consumed_max", 15 | messagesConsumedAvg: "ksql_ksql_engine_query_stats_messages_consumed_avg", 16 | livenessIndicator: "ksql_ksql_engine_query_stats_liveness_indicator", 17 | errorRate: "ksql_ksql_engine_query_stats_error_rate", 18 | errorQueries: "ksql_ksql_engine_query_stats_error_queries", 19 | createdQueries: "ksql_ksql_engine_query_stats_created_queries", 20 | bytesConsumedTotal: "ksql_ksql_engine_query_stats_bytes_consumed_total", 21 | }; 22 | 23 | module.exports = queryTypes; 24 | -------------------------------------------------------------------------------- /ksqLight/server/server.js: -------------------------------------------------------------------------------- 1 | //---------------External Module Imports---------------- 2 | const express = require("express"); 3 | const axios = require("axios"); 4 | const { graphqlHTTP } = require("express-graphql"); 5 | const cors = require("cors"); 6 | const { 7 | GraphQLSchema, 8 | GraphQLObjectType, 9 | GraphQLString, 10 | GraphQLList, 11 | GraphQLInt, 12 | GraphQLNonNull, 13 | GraphQLBoolean, 14 | } = require("graphql"); 15 | 16 | //---------------Internal Module Imports---------------- 17 | const queryTypes = require("./queryTypes.js"); 18 | 19 | const app = express(); 20 | 21 | //---------------Global Middleware---------------- 22 | app.use(cors()); 23 | 24 | //---------------Custom Types---------------- 25 | const RealTimeType = new GraphQLObjectType({ 26 | name: "activeQueries2", 27 | description: 28 | "Object containing x and y properties with the respective data as values", 29 | fields: () => ({ 30 | x: { 31 | type: GraphQLInt, 32 | resolve: (parent, args, context) => parent[0], 33 | }, 34 | y: { 35 | type: GraphQLString, 36 | resolve: (parent, args, context) => parent[1], 37 | }, 38 | }), 39 | }); 40 | 41 | const ValidationType = new GraphQLObjectType({ 42 | name: "inputValidation", 43 | description: "Object indicating input validity status and error message", 44 | fields: () => ({ 45 | isValid: { type: GraphQLBoolean }, 46 | error: { type: GraphQLString }, 47 | }), 48 | }); 49 | 50 | //---------------Root Query Types---------------- 51 | const RootQueryType = new GraphQLObjectType({ 52 | name: "Query", 53 | description: "Root Query", 54 | fields: () => ({ 55 | ksqlDBMetrics: { 56 | type: new GraphQLList(RealTimeType), 57 | description: "ksqlDB Metric", 58 | args: { 59 | metric: { type: GraphQLNonNull(GraphQLString) }, 60 | start: { type: GraphQLNonNull(GraphQLInt) }, 61 | end: { type: GraphQLNonNull(GraphQLInt) }, 62 | resolution: { type: GraphQLNonNull(GraphQLInt) }, 63 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 64 | }, 65 | resolve: (parent, { start, end, resolution, metric, prometheusURL }) => { 66 | if (prometheusURL[prometheusURL.length - 1] === "/") 67 | prometheusURL = prometheusURL.slice(0, prometheusURL.length - 1); 68 | 69 | return axios 70 | .get( 71 | `${prometheusURL}/api/v1/query_range?step=${resolution}s&end=${end}&start=${start}&query=${queryTypes[metric]}` 72 | ) 73 | .then((res) => { 74 | return res.data.data.result[0].values; 75 | }) 76 | .catch((error) => error); 77 | }, 78 | }, 79 | livenessIndicator: { 80 | type: GraphQLBoolean, 81 | description: 82 | "Boolean value representing whether the ksqlDB server up and emitting metrics.", 83 | args: { 84 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 85 | }, 86 | resolve: async (parent, { prometheusURL }) => { 87 | try { 88 | return await axios 89 | .get( 90 | `${prometheusURL}/api/v1/query?query=ksql_ksql_engine_query_stats_liveness_indicator` 91 | ) 92 | .then((res) => res.data.data.result[0].value[1] === "1") 93 | .catch((error) => { 94 | throw error; 95 | }); 96 | } catch (error) { 97 | return error; 98 | } 99 | }, 100 | }, 101 | errorRate: { 102 | type: GraphQLInt, 103 | description: 104 | "The number of messages that were consumed but not processed.", 105 | args: { 106 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 107 | }, 108 | resolve: async (parent, { prometheusURL }) => { 109 | try { 110 | return await axios 111 | .get( 112 | `${prometheusURL}/api/v1/query?query=ksql_ksql_engine_query_stats_error_rate` 113 | ) 114 | .then((res) => Number(res.data.data.result[0].value[1])) 115 | .catch((error) => { 116 | throw error; 117 | }); 118 | } catch (error) { 119 | return error; 120 | } 121 | }, 122 | }, 123 | errorQueries: { 124 | type: GraphQLInt, 125 | description: "The count of queries in ERROR state.", 126 | args: { 127 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 128 | }, 129 | resolve: async (parent, { prometheusURL }) => { 130 | try { 131 | return await axios 132 | .get( 133 | `${prometheusURL}/api/v1/query?query=ksql_ksql_engine_query_stats_error_queries` 134 | ) 135 | .then((res) => Number(res.data.data.result[0].value[1])) 136 | .catch((error) => { 137 | throw error; 138 | }); 139 | } catch (error) { 140 | return error; 141 | } 142 | }, 143 | }, 144 | bytesConsumed: { 145 | type: GraphQLInt, 146 | description: "The total number of bytes consumed across all queries.", 147 | args: { 148 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 149 | }, 150 | resolve: async (parent, { prometheusURL }) => { 151 | try { 152 | return await axios 153 | .get( 154 | `${prometheusURL}/api/v1/query?query=ksql_ksql_engine_query_stats_bytes_consumed_total` 155 | ) 156 | .then((res) => Number(res.data.data.result[0].value[1])) 157 | .catch((error) => { 158 | throw error; 159 | }); 160 | } catch (error) { 161 | return error; 162 | } 163 | }, 164 | }, 165 | isValidPrometheusURL: { 166 | type: ValidationType, 167 | description: 168 | "Object representing whether provided Prometheus URL points to valid Prometheus server and any errors", 169 | args: { 170 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 171 | }, 172 | resolve: async (parent, { prometheusURL }) => { 173 | if (prometheusURL[prometheusURL.length - 1] === "/") 174 | prometheusURL = prometheusURL.slice(0, prometheusURL.length - 1); 175 | 176 | return axios 177 | .get(`${prometheusURL}/api/v1/status/buildinfo`) 178 | .then((res) => ({ 179 | isValid: true, 180 | error: null, 181 | })) 182 | .catch((error) => ({ 183 | isValid: false, 184 | error: error.message, 185 | })); 186 | }, 187 | }, 188 | isValidKsqlDBURL: { 189 | type: ValidationType, 190 | description: 191 | "Object representing whether provided ksqlDB URL points to valid Prometheus server and any errors", 192 | args: { 193 | ksqlDBURL: { type: GraphQLNonNull(GraphQLString) }, 194 | }, 195 | resolve: (parent, { ksqlDBURL }) => { 196 | if (ksqlDBURL[ksqlDBURL.length - 1] === "/") 197 | ksqlDBURL = ksqlDBURL.slice(0, ksqlDBURL.length - 1); 198 | 199 | return axios 200 | .get(`${ksqlDBURL}/clusterStatus`) 201 | .then((res) => ({ 202 | isValid: true, 203 | error: null, 204 | })) 205 | .catch((error) => ({ 206 | isValid: false, 207 | error: error.message, 208 | })); 209 | }, 210 | }, 211 | isValidDuration: { 212 | type: GraphQLBoolean, 213 | description: 214 | "Boolean representing whether Prometheus server accepts user duration.", 215 | args: { 216 | metric: { type: GraphQLNonNull(GraphQLString) }, 217 | start: { type: GraphQLNonNull(GraphQLInt) }, 218 | end: { type: GraphQLNonNull(GraphQLInt) }, 219 | resolution: { type: GraphQLNonNull(GraphQLInt) }, 220 | prometheusURL: { type: GraphQLNonNull(GraphQLString) }, 221 | }, 222 | resolve: (parent, { start, end, resolution, metric, prometheusURL }) => { 223 | if (prometheusURL[prometheusURL.length - 1] === "/") 224 | prometheusURL = prometheusURL.slice(0, prometheusURL.length - 1); 225 | 226 | return axios 227 | .get( 228 | `${prometheusURL}/api/v1/query_range?step=${resolution}s&end=${end}&start=${start}&query=${queryTypes[metric]}` 229 | ) 230 | .then((res) => true) 231 | .catch((error) => false); 232 | }, 233 | }, 234 | }), 235 | }); 236 | 237 | const schema = new GraphQLSchema({ 238 | query: RootQueryType, 239 | }); 240 | 241 | app.use( 242 | "/graphql", 243 | graphqlHTTP({ 244 | schema: schema, 245 | graphiql: true, 246 | }) 247 | ); 248 | 249 | app.listen(5001, () => console.log("Server Running...")); 250 | -------------------------------------------------------------------------------- /ksqLight/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useState } from "react"; 3 | import { BrowserRouter, Routes, Route } from "react-router-dom"; 4 | import { Header } from "./components/Header.js"; 5 | import { Homepage } from "./components/Homepage.js"; 6 | import { SettingsSidebar } from "./components/SettingsSidebar.js"; 7 | import { PermanentDrawer } from "./components/PermanentDrawer.js"; 8 | 9 | import { 10 | CssBaseline, 11 | ThemeProvider, 12 | createTheme, 13 | Box, 14 | Grid, 15 | } from "@mui/material"; 16 | import { MetricCard } from "./components/MetricCard.js"; 17 | import { Welcomepage } from "./components/Welcomepage.js"; 18 | 19 | // http://localhost:9090 20 | function App() { 21 | const [showSettings, setShowSettings] = useState(false); 22 | const [showQueries, setShowQueries] = useState(true); 23 | const [showMessages, setShowMessages] = useState(false); 24 | const [showErrors, setShowErrors] = useState(false); 25 | const [showQuery, setShowQuery] = useState(false); 26 | const [mode, setMode] = useState("light"); 27 | const [metricsState, setMetricsState] = useState({ 28 | prometheusURL: null, 29 | ksqlDBURL: "", 30 | duration: { 31 | days: 0, 32 | hours: 0, 33 | minutes: 10, 34 | }, 35 | refreshRate: 5, 36 | }); 37 | 38 | const theme = createTheme({ 39 | palette: { 40 | mainPage: { 41 | main: "#f3e5f5", 42 | }, 43 | mode, 44 | ...(mode === "light" 45 | ? { 46 | background: { 47 | default: "#fbfaff", 48 | }, 49 | chartColor: { 50 | background: "#FFFFFF", 51 | }, 52 | queryPage: { 53 | main: "rgb(78, 67, 118, .9)", 54 | }, 55 | cardColor: { 56 | background1: "#FFFFFF", 57 | background2: "#FFFFFF", 58 | background3: "#FFFFFF", 59 | background4: "#FFFFFF", 60 | iconColor1: "#04724D", 61 | iconColor2: "#FFC300", 62 | iconColor3: "#540C97", 63 | iconColor4: "#C48EF6", 64 | }, 65 | } 66 | : { 67 | chartColor: { 68 | background: "#1A1A1A", 69 | }, 70 | cardColor: { 71 | background1: "#1A1A1A", 72 | background2: "#1A1A1A", 73 | background3: "#1A1A1A", 74 | background4: "#1A1A1A", 75 | }, 76 | queryPage: { 77 | main: "rgb(78, 67, 118, .9)", 78 | }, 79 | }), 80 | }, 81 | }); 82 | 83 | const serverCards = [ 84 | "livenessIndicator", 85 | "bytesConsumed", 86 | "errorRate", 87 | "errorQueries", 88 | ].map((el, i) => { 89 | return ( 90 | 91 | 92 | 93 | ); 94 | }); 95 | 96 | return !metricsState.prometheusURL ? ( 97 | 98 | 102 | 103 | ) : ( 104 | 105 | 106 | 107 | 113 |
119 | 125 | 126 | 127 | 133 | 134 | 144 | 155 | {serverCards} 156 | 157 | 163 | 164 | 174 | } 175 | /> 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | ); 184 | } 185 | 186 | export default App; 187 | -------------------------------------------------------------------------------- /ksqLight/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Typography, createTheme, Toolbar, AppBar, IconButton } from "@mui/material"; 3 | import SettingsIcon from '@mui/icons-material/Settings'; 4 | import LightModeIcon from '@mui/icons-material/LightMode'; 5 | import DarkModeIcon from '@mui/icons-material/DarkMode'; 6 | import ArticleIcon from '@mui/icons-material/Article'; 7 | 8 | export const Header = ({ showSettings, setShowSettings, mode, setMode }) => { 9 | const ksqLightTheme = createTheme({ 10 | typography: { 11 | fontFamily: 'Raleway', 12 | fontSize: 9, 13 | } 14 | }) 15 | const navGithub = () => { 16 | window.open( 17 | "https://github.com/oslabs-beta/ksqljs/", "_blank"); 18 | } 19 | 20 | return ( 21 | theme.zIndex.drawer + 1 }}> 22 | 23 | Homepage 24 | ksqLight 25 | { navGithub() }} color="mainPage"> 26 | 27 | 28 | mode === 'light' ? setMode('dark') : setMode('light')}> 29 | {mode === 'light' ? : } 30 | 31 | setShowSettings(!showSettings)} color="mainPage"> 32 | 33 | 34 | 35 | 36 | ) 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /ksqLight/src/components/Homepage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid, CssBaseline, Box, CardContent } from "@mui/material"; 3 | import LineChart from "./LineChart.js"; 4 | import { QueryPage } from "./QueryPage.js"; 5 | 6 | export const Homepage = ({ 7 | showQueries, 8 | showMessages, 9 | showErrors, 10 | metricsState, 11 | showQuery, 12 | }) => { 13 | const queriesCharts = [ 14 | ["runningQueries", "Running Queries"], 15 | ["createdQueries", "Created Queries"], 16 | ["numPersistentQueries", "Persistent Queries"], 17 | ["numIdleQueries", "Idle Queries"], 18 | ["rebalancingQueries", "Rebalancing Queries"], 19 | ["numActiveQueries", "Active Queries"], 20 | ["notRunningQueries", "Not Running Queries"], 21 | ["pendingShutdownQueries", "Pending Shutdown Queries"], 22 | ]; 23 | 24 | const messagesCharts = [ 25 | ["messagesConsumedTotal", "Messages Consumed"], 26 | ["messagesProducedPerSec", "Messages Produced Per Second"], 27 | ["messagesConsumedPerSec", "Messages Consumed Per Second"], 28 | ["messagesConsumedMin", "Messages Consumed Min"], 29 | ["messagesConsumedMax", "Messages Consumed Max"], 30 | ["messagesConsumedAvg", "Messages Consumed Average"], 31 | ]; 32 | 33 | const errorCharts = [ 34 | ["errorRate", "Error Rate"], 35 | ["errorQueries", "Error Queries"], 36 | ["pendingErrorQueries", "Pending Error Queries"], 37 | ]; 38 | 39 | return ( 40 | 41 | 42 | 43 | {showQueries && 44 | queriesCharts.map(([query, description], index) => ( 45 | 52 | ))} 53 | {showMessages && 54 | messagesCharts.map(([query, description], index) => ( 55 | 62 | ))} 63 | {showErrors && 64 | errorCharts 65 | .map(([query, description], index) => ( 66 | 73 | )) 74 | .concat([ 75 | 76 | 77 | , 78 | ])} 79 | {showQuery && } 80 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /ksqLight/src/components/LineChart.js: -------------------------------------------------------------------------------- 1 | //-----------Import External Modules----------- 2 | import React, { useEffect } from "react"; 3 | import { Grid } from "@mui/material"; 4 | import { 5 | ApolloClient, 6 | InMemoryCache, 7 | ApolloProvider, 8 | gql, 9 | } from "@apollo/client"; 10 | import Chart from "chart.js/auto"; 11 | import ChartStreaming from "chartjs-plugin-streaming"; 12 | import "chartjs-adapter-moment"; 13 | import { CardContent } from "@mui/material"; 14 | 15 | //-----------Import Internal Modules----------- 16 | import { getUnixRange, getDuration } from "../utils/utilityFunctions.js"; 17 | 18 | Chart.register(ChartStreaming); 19 | 20 | const client = new ApolloClient({ 21 | uri: "http://localhost:5001/graphql", 22 | cache: new InMemoryCache(), 23 | }); 24 | 25 | export default function LineChart({ 26 | metric, 27 | description, 28 | metricsState, 29 | index, 30 | }) { 31 | useEffect(() => { 32 | let initialData; 33 | 34 | // define chart context 35 | const ctx = document.getElementById(metric).getContext("2d"); 36 | 37 | // make initial fetch of graph data 38 | const [unixStart, unixEnd] = getUnixRange( 39 | metricsState.duration.days, 40 | metricsState.duration.hours, 41 | metricsState.duration.minutes 42 | ); 43 | 44 | initialData = client 45 | .query({ 46 | query: gql` 47 | query fetchMetric { 48 | ksqlDBMetrics(prometheusURL: "${metricsState.prometheusURL}" metric: "${metric}", resolution: ${metricsState.refreshRate}, start: ${unixStart}, end: ${unixEnd}) { 49 | x, 50 | y 51 | } 52 | } 53 | `, 54 | }) 55 | .then((res) => { 56 | const data = res.data.ksqlDBMetrics.map((queryObj) => { 57 | return { 58 | x: new Date(queryObj.x * 1000), 59 | y: queryObj.y, 60 | }; 61 | }); 62 | return data; 63 | }) 64 | .catch((error) => console.log(error)); 65 | 66 | // define gradient for background 67 | const gradient = ctx.createLinearGradient(0, 0, 0, 400); 68 | // gradient.addColorStop(0, "rgba(255, 207, 112, 1)"); 69 | gradient.addColorStop( 70 | 0, 71 | index % 2 === 0 ? "rgba(255, 194, 67, 0.2)" : "rgba(78, 67, 118, 0.2)" 72 | ); 73 | gradient.addColorStop( 74 | 1, 75 | index % 2 === 0 ? "rgba(255, 194, 67, 0.2)" : "rgba(78, 67, 118, 0.2)" 76 | ); 77 | 78 | // define chart configuration 79 | const config = { 80 | type: "line", 81 | data: { 82 | datasets: [ 83 | { 84 | data: initialData, 85 | // data: [], // empty at the beginning, 86 | borderColor: 87 | index % 2 === 0 88 | ? "rgba(255, 194, 67, 1)" 89 | : "rgba(78, 67, 118, 1)", 90 | pointRadius: 0, 91 | hitRadius: 30, 92 | hoverRadius: 5, 93 | fill: true, 94 | backgroundColor: gradient, 95 | }, 96 | ], 97 | }, 98 | options: { 99 | responsive: true, 100 | elements: { 101 | line: { 102 | tension: 0.4, 103 | }, 104 | }, 105 | scales: { 106 | x: { 107 | type: "realtime", 108 | ticks: { 109 | // minTicksLimit: 24 110 | autoskip: true, 111 | autoSkipPadding: 30, 112 | maxRotation: 0, 113 | steps: 10, 114 | }, 115 | realtime: { 116 | duration: getDuration( 117 | metricsState.duration.days, 118 | metricsState.duration.hours, 119 | metricsState.duration.minutes 120 | ), // data in the past duration # of ms will be displayed 121 | refresh: metricsState.refreshRate * 1000, // onRefresh callback will be called every refresh # ms 122 | delay: 1000, // delay of 1000 ms, so upcoming values are known before plotting a line 123 | pause: false, // chart is not paused 124 | ttl: undefined, // data will be automatically deleted as it disappears off the chart 125 | frameRate: 30, // data points are drawn 30 times every second 126 | 127 | // a callback to update datasets 128 | onRefresh: (chart) => { 129 | const [unixStart, unixEnd] = getUnixRange( 130 | metricsState.duration.days, 131 | metricsState.duration.hours, 132 | metricsState.duration.minutes 133 | ); 134 | 135 | // query your data source and get the array of {x: timestamp, y: value} objects 136 | client 137 | .query({ 138 | query: gql` 139 | query testQuery { 140 | ksqlDBMetrics(prometheusURL: "${metricsState.prometheusURL}" metric: "${metric}", resolution: ${metricsState.refreshRate}, start: ${unixStart}, end: ${unixEnd}) { 141 | x, 142 | y 143 | } 144 | } 145 | `, 146 | }) 147 | .then((res) => { 148 | const data = res.data.ksqlDBMetrics.map((queryObj) => { 149 | return { 150 | x: new Date(queryObj.x * 1000), 151 | y: queryObj.y, 152 | }; 153 | }); 154 | chart.data.datasets[0].data = data; 155 | }) 156 | .catch((error) => console.log(error)); 157 | }, 158 | }, 159 | }, 160 | y: { 161 | beginAtZero: true, 162 | ticks: { 163 | // display: false, 164 | // color: "#999", 165 | stepSize: 5, 166 | }, 167 | }, 168 | }, 169 | plugins: { 170 | legend: { 171 | display: false, 172 | // position: "top", 173 | }, 174 | title: { 175 | fontFamily: "Raleway", 176 | // color: '#999', 177 | display: true, 178 | text: description, 179 | }, 180 | }, 181 | }, 182 | }; 183 | 184 | // instantiate new instance of a chart 185 | const realTimeChart = new Chart(ctx, config); 186 | 187 | // chart teardown on unmount 188 | return () => { 189 | realTimeChart.destroy(); 190 | }; 191 | }, [metricsState]); 192 | 193 | return ( 194 | 195 | 202 | 203 | 204 | 205 | ); 206 | } 207 | -------------------------------------------------------------------------------- /ksqLight/src/components/MetricCard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { ApolloClient, InMemoryCache, gql } from "@apollo/client"; 3 | import { Typography, CardContent, IconButton, Box } from "@mui/material"; 4 | import MonitorHeartIcon from "@mui/icons-material/MonitorHeart"; 5 | import ErrorIcon from "@mui/icons-material/Error"; 6 | import BugReportIcon from "@mui/icons-material/BugReport"; 7 | import DataThresholdingIcon from "@mui/icons-material/DataThresholding"; 8 | 9 | const client = new ApolloClient({ 10 | uri: "http://localhost:5001/graphql", 11 | cache: new InMemoryCache(), 12 | }); 13 | 14 | export const MetricCard = ({ type, index }) => { 15 | const [data, setData] = useState(null); 16 | 17 | const metricType = [ 18 | "Liveness Indicator", 19 | "Error Rate", 20 | "Error Queries", 21 | "Bytes Consumed", 22 | ]; 23 | const bgColor = [ 24 | "cardColor.background1", 25 | "cardColor.background2", 26 | "cardColor.background3", 27 | "cardColor.background4", 28 | ]; 29 | const textColor = [ 30 | "cardColor.textColor1", 31 | "cardColor.textColor2", 32 | "cardColor.textColor3", 33 | "cardColor.textColor4", 34 | ]; 35 | const bgImage = [ 36 | "cardColor.iconBg1", 37 | "cardColor.iconBg2", 38 | "cardColor.iconBg3", 39 | "cardColor.iconBg4", 40 | ]; 41 | const iconColor = [ 42 | "cardColor.iconColor1", 43 | "cardColor.iconColor2", 44 | "cardColor.iconColor3", 45 | "cardColor.iconColor4", 46 | ]; 47 | 48 | useEffect(() => { 49 | client 50 | .query({ 51 | query: gql` 52 | query testQuery { 53 | ${type}(prometheusURL: "http://localhost:9090/") 54 | } 55 | `, 56 | }) 57 | .then((res) => setData(res.data[type])) 58 | .catch((error) => console.log(error)); 59 | }, []); 60 | 61 | return ( 62 | 73 | 82 | 86 | {index === 0 && } 87 | {index === 1 && } 88 | {index === 2 && } 89 | {index === 3 && } 90 | 91 | 92 | 101 | {metricType[index]} 102 | 103 | 104 | 105 | 106 | {index === 0 && (data ? "Running" : "Down")} 107 | {index === 1 && (data !== null ? data : "N/A")} 108 | {index === 2 && (data !== null ? data : "N/A")} 109 | {index === 3 && (data !== null ? data : "N/A")} 110 | 111 | 112 | ); 113 | }; 114 | -------------------------------------------------------------------------------- /ksqLight/src/components/PermanentDrawer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { 4 | CssBaseline, 5 | List, 6 | ListItem, 7 | ListItemButton, 8 | ListItemIcon, 9 | ListItemText, 10 | Box, 11 | } from "@mui/material"; 12 | import StackedLineChartIcon from "@mui/icons-material/StackedLineChart"; 13 | import MailIcon from "@mui/icons-material/Mail"; 14 | 15 | export const PermanentDrawer = ({ 16 | setShowQueries, 17 | setShowMessages, 18 | setShowErrors, 19 | setShowQuery, 20 | }) => { 21 | let navigate = useNavigate(); 22 | 23 | const navQueryPage = () => { 24 | navigate("/queryPage"); 25 | }; 26 | 27 | const toggleCharts = (type) => { 28 | if (type === "queries") { 29 | setShowQueries(true); 30 | setShowMessages(false); 31 | setShowErrors(false); 32 | setShowQuery(false); 33 | } else if (type === "messages") { 34 | setShowQueries(false); 35 | setShowMessages(true); 36 | setShowErrors(false); 37 | setShowQuery(false); 38 | } else if (type === "errors") { 39 | setShowQueries(false); 40 | setShowMessages(false); 41 | setShowErrors(true); 42 | setShowQuery(false); 43 | } else if (type === "queryPage") { 44 | setShowQuery(true); 45 | setShowQueries(false); 46 | setShowMessages(false); 47 | setShowErrors(false); 48 | } 49 | }; 50 | 51 | return ( 52 | 61 | 62 | 63 | 64 | toggleCharts("queries")}> 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | toggleCharts("messages")}> 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | toggleCharts("errors")}> 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | toggleCharts("queryPage")}> 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /ksqLight/src/components/QueryPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Typography, Toolbar, TextField, Stack, Button } from "@mui/material"; 3 | import { Box } from "@mui/system"; 4 | 5 | export const QueryPage = ({ metricsState }) => { 6 | const [query, setQuery] = useState(null); 7 | 8 | const handleSubmit = (event) => { 9 | event.preventDefault(); 10 | }; 11 | 12 | return ( 13 | <> 14 | {metricsState.ksqlDBURL !== "" ? ( 15 | 24 |
25 | 34 | 42 | 43 |
44 | ) : ( 45 | 54 | 59 | Enter ksqlDB URL in settings menu to submit queries... 60 | 61 | 62 | )} 63 | 64 | // 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /ksqLight/src/components/SettingsSidebar.js: -------------------------------------------------------------------------------- 1 | //-----------Import External Modules----------- 2 | import React, { useEffect } from "react"; 3 | import { useState } from "react"; 4 | import { TextField, Typography, Drawer, IconButton, Grid, Button, Stack } from "@mui/material" 5 | import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; 6 | import { 7 | ApolloClient, 8 | InMemoryCache, 9 | ApolloProvider, 10 | gql, 11 | } from "@apollo/client"; 12 | 13 | //-----------Import Internal Modules----------- 14 | import {getUnixRange, getDuration, validateDuration} from "../utils/utilityFunctions.js"; 15 | 16 | const client = new ApolloClient({ 17 | uri: "http://localhost:5001/graphql", 18 | cache: new InMemoryCache(), 19 | }); 20 | 21 | export const SettingsSidebar = ({ showSettings, setShowSettings, metricsState, setMetricsState }) => { 22 | const [invalidPrometheusMessage, setInvalidPrometheusMessage] = useState(null); 23 | const [invalidKsqlDBMessage, setInvalidKsqlDBMessage] = useState(null); 24 | const [invalidDuration, setInvalidDuration] = useState(false); 25 | const [showSubmissionConfirmation, setShowSubmissionConfirmation] = useState(false); 26 | const [localMetricsState, setLocalMetricsState] = useState({ 27 | prometheusURL: metricsState.prometheusURL, 28 | ksqlDBURL: metricsState.ksqlDBURL, 29 | duration: metricsState.duration, 30 | refreshRate: metricsState.refreshRate 31 | }); 32 | 33 | const handleLocalMetrics = (event) => { 34 | switch(event.target.name) { 35 | case "prometheus-url": 36 | setLocalMetricsState({ 37 | ...localMetricsState, 38 | prometheusURL: event.target.value 39 | }); 40 | break; 41 | case "ksqldb-url": 42 | setLocalMetricsState({ 43 | ...localMetricsState, 44 | ksqlDBURL: event.target.value 45 | }); 46 | break; 47 | case "duration-days": 48 | setLocalMetricsState({ 49 | ...localMetricsState, 50 | duration: { 51 | ...localMetricsState.duration, 52 | days: event.target.value 53 | } 54 | }); 55 | break; 56 | case "duration-hours": 57 | setLocalMetricsState({ 58 | ...localMetricsState, 59 | duration: { 60 | ...localMetricsState.duration, 61 | hours: event.target.value 62 | } 63 | }); 64 | break; 65 | case "duration-minutes": 66 | setLocalMetricsState({ 67 | ...localMetricsState, 68 | duration: { 69 | ...localMetricsState.duration, 70 | minutes: event.target.value 71 | } 72 | }); 73 | break; 74 | case "refresh-rate": 75 | setLocalMetricsState({ 76 | ...localMetricsState, 77 | refreshRate: event.target.value 78 | }); 79 | break; 80 | default: 81 | break; 82 | } 83 | } 84 | 85 | const handleSubmit = async (event) => { 86 | event.preventDefault(); 87 | 88 | // Verify Prometheus URL 89 | try { 90 | // Validate Prometheus URL points to live server 91 | const {data: {isValidPrometheusURL: {isValid: prometheusValid, error: prometheusError}}} = await client.query({ 92 | query: gql` 93 | query validatePrometheusURL{ 94 | isValidPrometheusURL(prometheusURL: "${event.target[1].value}") { 95 | isValid, 96 | error 97 | } 98 | } 99 | ` 100 | }); 101 | 102 | if (prometheusError) { 103 | setInvalidPrometheusMessage(prometheusError); 104 | return; 105 | } else if (prometheusValid) { 106 | setInvalidPrometheusMessage(null); 107 | } 108 | 109 | // Validate ksqlDB URL points to live server (if provided) 110 | if (localMetricsState.ksqlDBURL) { 111 | const {data: {isValidKsqlDBURL: {isValid: ksqlDBValid, error: ksqlDBError}}} = await client.query({ 112 | query: gql` 113 | query isValidKsqlDBURL{ 114 | isValidKsqlDBURL(ksqlDBURL: "${event.target[3].value}") { 115 | isValid, 116 | error 117 | } 118 | } 119 | ` 120 | }); 121 | 122 | if (ksqlDBError) { 123 | setInvalidKsqlDBMessage(ksqlDBError); 124 | return; 125 | } else if (ksqlDBValid) { 126 | setInvalidKsqlDBMessage(null); 127 | } 128 | } else { 129 | setInvalidKsqlDBMessage(null); 130 | } 131 | 132 | // Validate Prometheus accepts user's duration input 133 | const duration = getDuration(localMetricsState.duration.days, localMetricsState.duration.hours, localMetricsState.duration.minutes); 134 | if (!validateDuration(duration, localMetricsState.refreshRate)) { 135 | setInvalidDuration(true); 136 | return; 137 | } else { 138 | setInvalidDuration(false); 139 | } 140 | 141 | // Update state with user's input values 142 | setMetricsState({ 143 | prometheusURL: localMetricsState.prometheusURL, 144 | ksqlDBURL: localMetricsState.ksqlDBURL, 145 | duration: { 146 | days: localMetricsState.duration.days, 147 | hours: localMetricsState.duration.hours, 148 | minutes: localMetricsState.duration.minutes 149 | }, 150 | refreshRate: localMetricsState.refreshRate 151 | }); 152 | setShowSubmissionConfirmation(true); 153 | setTimeout(() => setShowSubmissionConfirmation(false), 3000); 154 | } catch (error) { 155 | console.log(error); 156 | } 157 | } 158 | 159 | return( 160 | 161 |
162 |
163 | 164 | setShowSettings(!showSettings)}> 165 | 166 | 167 | Prometheus Connection 168 |
169 | {invalidPrometheusMessage ? ( 170 | <> 171 | 180 | {invalidPrometheusMessage} 181 | 182 | ) : ( 183 | 191 | )} 192 |
193 | ksqlDB Connection 194 |
195 | {invalidKsqlDBMessage ? ( 196 | <> 197 | 206 | {invalidKsqlDBMessage} 207 | 208 | ) : ( 209 | 217 | )} 218 |
219 | Duration 220 |
221 | { invalidDuration ? ( 222 | <> 223 | 224 | 225 | 234 | 235 | 236 | 245 | 246 | 247 | 256 | 257 | 258 | Duration must not include more than 11,000 data points 259 | 260 | ) : ( 261 | 262 | 263 | 271 | 272 | 273 | 281 | 282 | 283 | 291 | 292 | 293 | )} 294 |
295 | Refresh Rate 296 |
297 | 298 | 307 | 308 | 309 | 310 | 311 | 312 | {showSubmissionConfirmation && Settings Saved!} 313 |
314 |
315 |
316 |
317 | ) 318 | }; 319 | 320 | -------------------------------------------------------------------------------- /ksqLight/src/components/Welcomepage.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Typography, TextField, Box, Fade, Button, Stack } from "@mui/material"; 3 | import { ApolloClient, InMemoryCache, gql } from "@apollo/client"; 4 | 5 | const client = new ApolloClient({ 6 | uri: "http://localhost:5001/graphql", 7 | cache: new InMemoryCache(), 8 | }); 9 | 10 | export const Welcomepage = ({ setMetricsState, metricsState }) => { 11 | const [showTextBox, setShowTextBox] = useState(false); 12 | const [invalidPrometheusMessage, setInvalidPrometheusMessage] = 13 | useState(null); 14 | const [url, setUrl] = useState(""); 15 | 16 | const handleClick = () => { 17 | setShowTextBox(true); 18 | }; 19 | 20 | const handleSubmit = async (event) => { 21 | event.preventDefault(); 22 | 23 | try { 24 | const { 25 | data: { 26 | isValidPrometheusURL: { 27 | isValid: prometheusValid, 28 | error: prometheusError, 29 | }, 30 | }, 31 | } = await client.query({ 32 | query: gql` 33 | query validatePrometheusURL{ 34 | isValidPrometheusURL(prometheusURL: "${url}") { 35 | isValid, 36 | error 37 | } 38 | } 39 | `, 40 | }); 41 | if (prometheusError) { 42 | setInvalidPrometheusMessage(prometheusError); 43 | return; 44 | } else if (prometheusValid) { 45 | setInvalidPrometheusMessage(null); 46 | } 47 | setMetricsState({ 48 | ...metricsState, 49 | prometheusURL: url, 50 | }); 51 | } catch (error) { 52 | console.log(error); 53 | } 54 | }; 55 | 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 74 | 75 | 82 | ksqLight. 83 | 84 | 85 | 86 | {showTextBox ? ( 87 | <> 88 | {!invalidPrometheusMessage ? ( 89 |
90 | 91 | setUrl(e.target.value)} 102 | /> 103 | 111 | 112 |
113 | ) : ( 114 |
115 | 116 | setUrl(e.target.value)} 128 | /> 129 | 137 | 138 | 139 | {invalidPrometheusMessage} 140 | 141 |
142 | )} 143 | 144 | ) : ( 145 | 146 | 154 | 155 | )} 156 |
157 |
158 | ); 159 | }; 160 | -------------------------------------------------------------------------------- /ksqLight/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 8 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | 19 | .background-animate { 20 | background-size: 400%; 21 | 22 | -webkit-animation: AnimationName 3s ease infinite; 23 | -moz-animation: AnimationName 3s ease infinite; 24 | animation: AnimationName 3s ease infinite; 25 | } 26 | 27 | @keyframes AnimationName { 28 | 0%, 29 | 100% { 30 | background-position: 0% 50%; 31 | } 32 | 50% { 33 | background-position: 100% 50%; 34 | } 35 | } 36 | 37 | @layer components { 38 | .header-viewport { 39 | height: calc(100vh - 64px); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /ksqLight/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import './style.scss'; 5 | import App from './App'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | ); -------------------------------------------------------------------------------- /ksqLight/src/static/darkmode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/darkmode.gif -------------------------------------------------------------------------------- /ksqLight/src/static/ksqLight2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/ksqLight2.png -------------------------------------------------------------------------------- /ksqLight/src/static/ksqLight_name_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/ksqLight_name_2.png -------------------------------------------------------------------------------- /ksqLight/src/static/ksqlight_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/ksqlight_icon.ico -------------------------------------------------------------------------------- /ksqLight/src/static/ksqlight_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/ksqlight_icon.png -------------------------------------------------------------------------------- /ksqLight/src/static/ksqlight_raleway.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/ksqlight_raleway.png -------------------------------------------------------------------------------- /ksqLight/src/static/landing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/landing.gif -------------------------------------------------------------------------------- /ksqLight/src/static/settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/settings.gif -------------------------------------------------------------------------------- /ksqLight/src/static/sidebar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ksqlSuite/999b83890287cceff44169afd51af7c232cbd554/ksqLight/src/static/sidebar.gif -------------------------------------------------------------------------------- /ksqLight/src/style.scss: -------------------------------------------------------------------------------- 1 | 2 | .hero { 3 | display: inline-block; 4 | width: 100%; 5 | height: 100vh; 6 | position: relative; 7 | 8 | } 9 | .diagonal-hero-bg { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | background: #2b5876; /* fallback for old browsers */ 16 | background: -webkit-linear-gradient(to right, #4e4376, #2b5876); /* Chrome 10-25, Safari 5.1-6 */ 17 | background: linear-gradient(to right, #4e4376, #2b5876); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 18 | 19 | z-index: -1; 20 | } 21 | 22 | // creates randomized star sizes 23 | @function stars($n) { 24 | $value: '#{random(2000)}px #{random(2000)}px #767676'; 25 | @for $i from 2 through $n { 26 | $value: '#{$value} , #{random(2000)}px #{random(2000)}px #767676'; 27 | } 28 | @return unquote($value); 29 | } 30 | 31 | // store random stars 32 | $stars-small: stars(700); 33 | $stars-medium: stars(200); 34 | $stars-big: stars(100); 35 | 36 | 37 | .stars { 38 | z-index: -1; 39 | overflow: hidden; 40 | top: 0; 41 | left: 0; 42 | width: 100%; 43 | height: 95vh; 44 | transition: opacity 1s ease-in-out; 45 | } 46 | 47 | .stars > .small { 48 | width: 1px; 49 | height: 1px; 50 | background: transparent; 51 | box-shadow: $stars-small; 52 | animation: starsAnimation 50s linear infinite; 53 | 54 | &:after { 55 | content: " "; 56 | position: absolute; 57 | top: 2000px; 58 | width: 1px; 59 | height: 1px; 60 | background: transparent; 61 | box-shadow: $stars-small; 62 | } 63 | } 64 | 65 | .stars > .medium { 66 | width: 2px; 67 | height: 2px; 68 | background: transparent; 69 | box-shadow: $stars-medium; 70 | animation: starsAnimation 100s linear infinite; 71 | 72 | &:after { 73 | content: " "; 74 | position: absolute; 75 | top: 2000px; 76 | width: 2px; 77 | height: 2px; 78 | background: transparent; 79 | box-shadow: $stars-medium; 80 | } 81 | } 82 | 83 | .stars > .big { 84 | width: 3px; 85 | height: 3px; 86 | background: transparent; 87 | box-shadow: $stars-big; 88 | border-radius: 100%; 89 | animation: starsAnimation 150s linear infinite; 90 | 91 | &:after { 92 | content: " "; 93 | position: absolute; 94 | top: 2000px; 95 | width: 3px; 96 | height: 3px; 97 | background: transparent; 98 | box-shadow: $stars-big; 99 | border-radius: 100%; 100 | } 101 | } 102 | 103 | // swap from/to values to reverse animation 104 | @keyframes starsAnimation { 105 | from { 106 | transform: translateY(-2000px); 107 | } 108 | to { 109 | transform: translateY(0px); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /ksqLight/src/utils/utilityFunctions.js: -------------------------------------------------------------------------------- 1 | const utilityFunctions = {}; 2 | 3 | utilityFunctions.getUnixRange = (days, hours, minutes) => { 4 | const unixTimeNow = Math.round(new Date().getTime() / 1000); 5 | const unixOffSet = minutes * 60 + hours * 60 * 60 + days * 60 * 60 * 24; 6 | return [unixTimeNow - unixOffSet, unixTimeNow] 7 | }; 8 | 9 | utilityFunctions.getDuration = (days, hours, minutes) => { 10 | return (minutes * 60 + hours * 60 * 60 + days * 60 * 60 * 24) * 1000; 11 | }; 12 | 13 | utilityFunctions.validateDuration = (duration, resolution) => { 14 | return (duration / 1000) / resolution < 11000; 15 | } 16 | 17 | module.exports = utilityFunctions; -------------------------------------------------------------------------------- /ksqLight/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | fontFamily:{ 9 | 'raleway': ['Raleway', 'sans-serif'] 10 | } 11 | }, 12 | }, 13 | plugins: [], 14 | } 15 | -------------------------------------------------------------------------------- /ksqljs/README.md: -------------------------------------------------------------------------------- 1 | # ksQlient (formerly ksqlDB-JS) 2 | 3 |
4 | logo 5 |
6 | 7 |
8 | 9 | GitHub stars 10 | GitHub issues 11 | GitHub last commit 12 | 13 |

A native Node.js client for ksqlDB

14 |
15 | 16 | ## About the Project 17 | 18 | ksQlient is a **lightweight Node.js client for ksqlDB**, a database for streaming applications leveraging Kafka infrastructure under the hood. 19 | 20 | With our client, you can deploy stream-processing workloads on ksqlDB from within JS applications using simple, declarative SQL statements. 21 | 22 | Sample use cases: 23 | 24 | 1. Build applications that respond immediately to events. 25 | 2. Craft materialized views over streams. 26 | 3. Receive real-time push updates, or pull current state on demand. 27 | 28 | ## Table of Contents 29 | 30 | - [About the project](#about) 31 | - [Getting Started](#getting-started) 32 | - [Usage](#usage) 33 | - [Features](#features) 34 | - [Developers](#developers) 35 | - [Contributions](#contributions) 36 | - [License](#license) 37 | 38 | ## Getting Started 39 | 40 | The client is available on Node package manager (npm) ([link](https://www.npmjs.com/package/ksqldb-js)) 41 | 42 | ```bash 43 | 44 | npm install ksqldb-js 45 | 46 | ``` 47 | 48 | ## Usage 49 | 50 | Create a client in the application file: 51 | 52 | ```javascript 53 | const ksqldb = require("ksqldb-js"); 54 | const client = new ksqldb({ ksqldbURL: "" }); 55 | ``` 56 | 57 | To run tests, initiate Docker containers included in yaml file: 58 | 59 | ```bash 60 | docker-compose up 61 | npm test 62 | ``` 63 | 64 | ## Features 65 | 66 | ### Create a pull query 67 | 68 | ```javascript 69 | client.pull("SELECT * FROM myTable;"); 70 | ``` 71 | 72 | ### Create a push query (persistent query that subscribes to a stream) 73 | 74 | ```javascript 75 | client.push("SELECT * FROM myTable EMIT CHANGES;", (data) => { 76 | console.log(data); 77 | }); 78 | ``` 79 | 80 | ### Terminate persistent query (e.g. push query) 81 | 82 | ```javascript 83 | client.terminate(queryId); 84 | ``` 85 | 86 | ### Insert rows of data into a stream 87 | 88 | ```javascript 89 | client.insertStream("myTable", [ 90 | { name: "jack", email: "123@mail.com", age: 25 }, 91 | { name: "john", email: "456@mail.com", age: 20 }, 92 | ]); 93 | ``` 94 | 95 | ### List streams/queries 96 | 97 | ```javascript 98 | client.ksql("LIST STREAMS;"); 99 | ``` 100 | 101 | ### Create table/streams 102 | 103 | ```javascript 104 | client.createStream( 105 | "testStream", 106 | (columnsType = ["name VARCHAR", "email varchar", "age INTEGER"]), 107 | (topic = "testTopic"), 108 | (value_format = "json"), 109 | (partitions = 1) 110 | ); 111 | ``` 112 | 113 | ### For custom SQL statements including complex joins use the .ksql method 114 | 115 | ```javascript 116 | client.ksql("DROP STREAM IF EXISTS testStream;"); 117 | ``` 118 | 119 | ### SQL Query builder 120 | 121 | The built-in query builder can be used to parameterize SQL queries to avoid SQL injection. 122 | 123 | Multiple parameters can be added using to the statement using the "?" placeholder. If the number of parameters does not match the number of "?", an error will be thrown. 124 | 125 | ```javascript 126 | const builder = new queryBuilder(); 127 | const query = "SELECT * FROM table WHERE id = ? AND size = ?"; 128 | const finishedQuery = builder.build(query, 123, "middle"); 129 | 130 | client.ksql(finishedQuery); 131 | ``` 132 | 133 | ### Create a table (materialized view) from a source stream 134 | 135 | ```javascript 136 | client.createTableAs( 137 | "testTable", 138 | "sourceStream", 139 | (selectArray = ["name", "LATEST_BY_OFFSET(age) AS recentAge"]), 140 | (propertiesObj = { topic: "newTestTopic" }), 141 | (conditionsObj = { WHERE: "age >= 21", GROUP_BY: "name" }) 142 | ); 143 | ``` 144 | 145 | ### Create a stream based on an existing stream 146 | 147 | ```javascript 148 | client.createStreamAs( 149 | "testStream", 150 | (selectColumns = ["name", "age"]), 151 | "sourceStream", 152 | (propertiesObj = { 153 | kafka_topic: "testTopic", 154 | value_format: "json", 155 | partitions: 1, 156 | }), 157 | (conditions = "age > 50") 158 | ); 159 | ``` 160 | 161 | ### Pull stream data between two timestamps 162 | 163 | ```javascript 164 | client.pullFromTo( 165 | "TESTSTREAM", 166 | "America/Los_Angeles", 167 | (from = ["2022-01-01", "00", "00", "00"]), 168 | (to = ["2022-01-01", "00", "00", "00"]) 169 | ); 170 | ``` 171 | 172 | ### Troubleshooting methods to inspect server metrics 173 | 174 | - inspectServerStatus 175 | - inspectQueryStatus 176 | - inspectClusterStatus 177 | 178 | ## Use Case 179 | 180 | We have built a [demo app](https://github.com/stabRabbitDemo/app) to showcase how ksQlient can be used to create a streaming application . 181 | 182 | ## Developers 183 | 184 | - Javan Ang - [GitHub](https://github.com/javanang) | [LinkedIn](https://www.linkedin.com/in/javanang/) 185 | - Michael Snyder - [GitHub](https://github.com/MichaelCSnyder) | [LinkedIn](https://www.linkedin.com/in/michaelcharlessnyder/) 186 | - Jonathan Luu - [GitHub](https://github.com/jonathanluu17) | [LinkedIn](https://www.linkedin.com/in/jonathanluu17/) 187 | - Matthew Xing - [GitHub](https://github.com/matthewxing1) | [LinkedIn](https://www.linkedin.com/in/matthew-xing/) 188 | - Gerry Bong - [GitHub](https://github.com/ggbong734) | [LinkedIn](https://www.linkedin.com/in/gerry-bong-71137420/) 189 | 190 | ## Contributions 191 | 192 | Contributions to the code, examples, documentation, etc. are very much appreciated. 193 | 194 | - Please report issues and bugs directly in this [GitHub project](https://github.com/oslabs-beta/ksqljs/issues). 195 | 196 | ## License 197 | 198 | This product is licensed under the MIT License - see the LICENSE.md file for details. 199 | 200 | This is an open source product. 201 | 202 | This product is accelerated by OS Labs. 203 | 204 | ksqlDB is licensed under the [Confluent Community License](https://github.com/confluentinc/ksql/blob/master/LICENSE). 205 | 206 | _Apache, Apache Kafka, Kafka, and associated open source project names are trademarks of the [Apache Software Foundation](https://www.apache.org/)_. 207 | --------------------------------------------------------------------------------