├── EventHubUI.png ├── src └── main │ ├── java │ ├── eu │ │ └── adrianistan │ │ │ ├── EventHubProperty.kt │ │ │ ├── Main.java │ │ │ ├── EventHubMessage.kt │ │ │ ├── App.kt │ │ │ ├── MainController.kt │ │ │ └── NullCheckpointStore.kt │ └── module-info.java │ └── resources │ └── main.fxml ├── .gitignore ├── README.md ├── .github └── workflows │ └── maven.yml ├── pom.xml └── LICENSE /EventHubUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aarroyoc/eventhub-ui/main/EventHubUI.png -------------------------------------------------------------------------------- /src/main/java/eu/adrianistan/EventHubProperty.kt: -------------------------------------------------------------------------------- 1 | package eu.adrianistan 2 | 3 | data class EventHubProperty(val key: String, val value: String) -------------------------------------------------------------------------------- /src/main/java/eu/adrianistan/Main.java: -------------------------------------------------------------------------------- 1 | package eu.adrianistan; 2 | 3 | public class Main { 4 | public static void main(String[] args) { 5 | App.main(args); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/eu/adrianistan/EventHubMessage.kt: -------------------------------------------------------------------------------- 1 | package eu.adrianistan 2 | 3 | import java.time.Instant 4 | 5 | data class EventHubMessage( 6 | val partition: String, 7 | val sequence: Long, 8 | val body: String, 9 | val timestamp: Instant, 10 | val properties: List 11 | ) -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | module app { 2 | requires javafx.controls; 3 | requires javafx.fxml; 4 | requires transitive kotlin.stdlib; 5 | requires com.azure.messaging.eventhubs; 6 | requires com.google.gson; 7 | 8 | opens eu.adrianistan to javafx.fxml; 9 | exports eu.adrianistan; 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EventHub UI 2 | A graphical interface to watch your EventHub in real time. Useful to know what is inside your Azure EventHub with zero code. 3 | 4 | ![EventHub UI Screenshot](EventHubUI.png) 5 | 6 | ## Running 7 | 8 | EventHub UI is a Kotlin + JavaFX app, built using Maven. To run the app, download the code and then execute: 9 | 10 | $ mvn clean javafx:run 11 | 12 | ## TODO 13 | 14 | Some things missing: 15 | 16 | * Store credentials for quick connections 17 | * Send messages 18 | * Search messages -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 11 23 | - name: Build with Maven 24 | run: mvn -B package --file pom.xml 25 | -------------------------------------------------------------------------------- /src/main/java/eu/adrianistan/App.kt: -------------------------------------------------------------------------------- 1 | package eu.adrianistan 2 | 3 | import javafx.application.Application 4 | import javafx.fxml.FXMLLoader 5 | import javafx.scene.Parent 6 | import javafx.scene.Scene 7 | import javafx.stage.Stage 8 | import java.io.IOException 9 | import kotlin.jvm.JvmStatic 10 | 11 | /** 12 | * JavaFX App 13 | */ 14 | class App : Application() { 15 | override fun start(stage: Stage) { 16 | try { 17 | val root = FXMLLoader.load(javaClass.getResource("/main.fxml")) 18 | val scene = Scene(root) 19 | stage.title = "EventHub UI" 20 | stage.scene = scene 21 | stage.show() 22 | } catch (exp: IOException) { 23 | exp.printStackTrace() 24 | } 25 | } 26 | 27 | companion object { 28 | @JvmStatic 29 | fun main(args: Array) { 30 | launch(App::class.java) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/eu/adrianistan/MainController.kt: -------------------------------------------------------------------------------- 1 | package eu.adrianistan 2 | 3 | import javafx.fxml.FXML 4 | import javafx.collections.FXCollections 5 | import com.azure.messaging.eventhubs.EventProcessorClient 6 | import javafx.scene.control.cell.PropertyValueFactory 7 | import javafx.beans.value.ObservableValue 8 | import com.azure.messaging.eventhubs.EventProcessorClientBuilder 9 | import com.azure.messaging.eventhubs.EventHubClientBuilder 10 | import com.azure.messaging.eventhubs.models.ErrorContext 11 | import com.azure.messaging.eventhubs.models.EventContext 12 | import javafx.application.Platform 13 | import javafx.event.ActionEvent 14 | import javafx.scene.control.* 15 | import java.time.Instant 16 | import java.util.function.Consumer 17 | import kotlin.system.exitProcess 18 | import com.google.gson.GsonBuilder 19 | 20 | class MainController { 21 | @FXML 22 | var table: TableView? = null 23 | 24 | @FXML 25 | var partitionCol: TableColumn<*, *>? = null 26 | 27 | @FXML 28 | var sequenceCol: TableColumn<*, *>? = null 29 | 30 | @FXML 31 | var bodyCol: TableColumn<*, *>? = null 32 | 33 | @FXML 34 | var timestampCol: TableColumn<*, *>? = null 35 | 36 | @FXML 37 | var properties: TableView? = null 38 | 39 | @FXML 40 | var keyCol: TableColumn<*, *>? = null 41 | 42 | @FXML 43 | var valueCol: TableColumn<*, *>? = null 44 | 45 | @FXML 46 | var connectionString: TextField? = null 47 | 48 | @FXML 49 | var hubName: TextField? = null 50 | 51 | @FXML 52 | var connect: Button? = null 53 | 54 | @FXML 55 | var msg: TextArea? = null 56 | 57 | private val data = FXCollections.observableArrayList() 58 | private var eventProcessorClient: EventProcessorClient? = null 59 | 60 | fun initialize() { 61 | partitionCol!!.cellValueFactory = PropertyValueFactory("partition") 62 | sequenceCol!!.cellValueFactory = PropertyValueFactory("sequence") 63 | bodyCol!!.cellValueFactory = PropertyValueFactory("body") 64 | timestampCol!!.cellValueFactory = PropertyValueFactory("timestamp") 65 | 66 | keyCol!!.cellValueFactory = PropertyValueFactory("key") 67 | valueCol!!.cellValueFactory = PropertyValueFactory("value") 68 | 69 | table!!.items = data 70 | table!!.selectionModel.selectionMode = SelectionMode.SINGLE 71 | table!!.selectionModel.isCellSelectionEnabled = true 72 | table!!.selectionModel.selectedItemProperty() 73 | .addListener { obs: ObservableValue?, oldSelection: EventHubMessage?, newSelection: EventHubMessage? -> 74 | if (newSelection != null) { 75 | val gson = GsonBuilder().setPrettyPrinting().create() 76 | val data = gson.fromJson(newSelection.body, Object::class.java) 77 | val formatJson = gson.toJson(data) 78 | msg!!.text = formatJson 79 | properties!!.items = FXCollections.observableList(newSelection.properties) 80 | } 81 | } 82 | } 83 | 84 | @FXML 85 | fun handleDoConnect(event: ActionEvent?) { 86 | if (eventProcessorClient == null) { 87 | val eventProcessorClientBuilder = EventProcessorClientBuilder() 88 | .connectionString(connectionString!!.text, hubName!!.text) 89 | .consumerGroup(EventHubClientBuilder.DEFAULT_CONSUMER_GROUP_NAME) 90 | .processEvent(processEvent) 91 | .processError(processError) 92 | .checkpointStore(NullCheckpointStore()) 93 | eventProcessorClient = eventProcessorClientBuilder.buildEventProcessorClient() 94 | eventProcessorClient?.let { 95 | it.start() 96 | connect!!.text = "Close connection" 97 | } 98 | } else { 99 | eventProcessorClient!!.stop() 100 | eventProcessorClient = null 101 | connect!!.text = "Connect" 102 | } 103 | } 104 | 105 | @FXML 106 | fun handleDoQuit(event: ActionEvent?) { 107 | Platform.exit() 108 | exitProcess(0) 109 | } 110 | 111 | @FXML 112 | fun handleDoHelp(event: ActionEvent?) { 113 | val alert = Alert(Alert.AlertType.INFORMATION) 114 | alert.title = "About EventHub UI" 115 | alert.headerText = "EventHub UI 1.0.0" 116 | alert.contentText = "EventHub UI was made by Adrián Arroyo Calle (https://adrianistan.eu). EventHub is a trademark of Microsoft" 117 | alert.show() 118 | } 119 | 120 | val processEvent = Consumer { eventContext: EventContext -> 121 | System.out.printf("Message received: %s \n", eventContext.eventData.bodyAsString) 122 | data.add( 123 | EventHubMessage( 124 | eventContext.partitionContext.partitionId, 125 | eventContext.eventData.sequenceNumber, 126 | eventContext.eventData.bodyAsString, 127 | eventContext.eventData.enqueuedTime, 128 | eventContext.eventData.properties.map { 129 | EventHubProperty(it.key, it.value.toString()) 130 | } 131 | ) 132 | ) 133 | } 134 | val processError = Consumer { errorContext: ErrorContext -> 135 | System.out.printf( 136 | "Error occurred in partition processor for partition %s, %s.%n", 137 | errorContext.partitionContext.partitionId, 138 | errorContext.throwable 139 | ) 140 | } 141 | } -------------------------------------------------------------------------------- /src/main/java/eu/adrianistan/NullCheckpointStore.kt: -------------------------------------------------------------------------------- 1 | package eu.adrianistan 2 | 3 | import com.azure.messaging.eventhubs.CheckpointStore 4 | import com.azure.messaging.eventhubs.models.PartitionOwnership 5 | import java.util.concurrent.ConcurrentHashMap 6 | import com.azure.messaging.eventhubs.models.Checkpoint 7 | import com.azure.core.util.logging.ClientLogger 8 | import eu.adrianistan.NullCheckpointStore 9 | import reactor.core.publisher.Flux 10 | import java.lang.StringBuilder 11 | import java.util.Locale 12 | import com.azure.core.util.CoreUtils 13 | import java.util.UUID 14 | import reactor.core.publisher.Mono 15 | import java.lang.Void 16 | import java.lang.NullPointerException 17 | 18 | /* Taken from https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/eventhubs/azure-messaging-eventhubs/src/samples/java/com/azure/messaging/eventhubs/SampleCheckpointStore.java */ /* Code under the MIT License */ 19 | /** 20 | * A simple in-memory implementation of a [CheckpointStore]. This implementation keeps track of partition 21 | * ownership details including checkpointing information in-memory. Using this implementation will only facilitate 22 | * checkpointing and load balancing of Event Processors running within this process. 23 | */ 24 | class NullCheckpointStore : CheckpointStore { 25 | private val partitionOwnershipMap: MutableMap = ConcurrentHashMap() 26 | private val checkpointsMap: MutableMap = ConcurrentHashMap() 27 | private val logger = ClientLogger(NullCheckpointStore::class.java) 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | override fun listOwnership( 33 | fullyQualifiedNamespace: String, eventHubName: String, 34 | consumerGroup: String 35 | ): Flux { 36 | logger.info("Listing partition ownership") 37 | val prefix = prefixBuilder(fullyQualifiedNamespace, eventHubName, consumerGroup, OWNERSHIP) 38 | return Flux.fromIterable(partitionOwnershipMap.keys) 39 | .filter { key: String -> key.startsWith(prefix) } 40 | .map { key: String -> partitionOwnershipMap[key] } 41 | } 42 | 43 | private fun prefixBuilder( 44 | fullyQualifiedNamespace: String, eventHubName: String, consumerGroup: String, 45 | type: String 46 | ): String { 47 | return StringBuilder() 48 | .append(fullyQualifiedNamespace) 49 | .append(SEPARATOR) 50 | .append(eventHubName) 51 | .append(SEPARATOR) 52 | .append(consumerGroup) 53 | .append(SEPARATOR) 54 | .append(type) 55 | .toString() 56 | .toLowerCase(Locale.ROOT) 57 | } 58 | 59 | /** 60 | * Returns a [Flux] of partition ownership details for successfully claimed partitions. If a partition is 61 | * already claimed by an instance or if the ETag in the request doesn't match the previously stored ETag, then 62 | * ownership claim is denied. 63 | * 64 | * @param requestedPartitionOwnerships List of partition ownerships this instance is requesting to own. 65 | * @return Successfully claimed partition ownerships. 66 | */ 67 | override fun claimOwnership(requestedPartitionOwnerships: List): Flux { 68 | if (CoreUtils.isNullOrEmpty(requestedPartitionOwnerships)) { 69 | return Flux.empty() 70 | } 71 | val firstEntry = requestedPartitionOwnerships[0] 72 | val prefix = prefixBuilder( 73 | firstEntry.fullyQualifiedNamespace, firstEntry.eventHubName, 74 | firstEntry.consumerGroup, OWNERSHIP 75 | ) 76 | return Flux.fromIterable(requestedPartitionOwnerships) 77 | .filter { partitionOwnership: PartitionOwnership -> 78 | (!partitionOwnershipMap.containsKey(partitionOwnership.partitionId) 79 | || (partitionOwnershipMap[partitionOwnership.partitionId]!!.eTag 80 | == partitionOwnership.eTag)) 81 | } 82 | .doOnNext { partitionOwnership: PartitionOwnership -> 83 | logger 84 | .info( 85 | "Ownership of partition {} claimed by {}", partitionOwnership.partitionId, 86 | partitionOwnership.ownerId 87 | ) 88 | } 89 | .map { partitionOwnership: PartitionOwnership -> 90 | partitionOwnership.setETag(UUID.randomUUID().toString()).lastModifiedTime = System.currentTimeMillis() 91 | partitionOwnershipMap[prefix + SEPARATOR + partitionOwnership.partitionId] = partitionOwnership 92 | partitionOwnership 93 | } 94 | } 95 | 96 | /** 97 | * {@inheritDoc} 98 | */ 99 | override fun listCheckpoints( 100 | fullyQualifiedNamespace: String, eventHubName: String, 101 | consumerGroup: String 102 | ): Flux { 103 | val prefix = prefixBuilder(fullyQualifiedNamespace, eventHubName, consumerGroup, CHECKPOINT) 104 | return Flux.fromIterable(checkpointsMap.keys) 105 | .filter { key: String -> key.startsWith(prefix) } 106 | .map { key: String -> checkpointsMap[key] } 107 | } 108 | 109 | /** 110 | * Updates the in-memory storage with the provided checkpoint information. 111 | * 112 | * @param checkpoint The checkpoint containing the information to be stored in-memory. 113 | * @return A [Mono] that completes when the checkpoint is updated. 114 | */ 115 | override fun updateCheckpoint(checkpoint: Checkpoint): Mono { 116 | if (checkpoint == null) { 117 | return Mono.error(logger.logExceptionAsError(NullPointerException("checkpoint cannot be null"))) 118 | } 119 | val prefix = prefixBuilder( 120 | checkpoint.fullyQualifiedNamespace, checkpoint.eventHubName, 121 | checkpoint.consumerGroup, CHECKPOINT 122 | ) 123 | checkpointsMap[prefix + SEPARATOR + checkpoint.partitionId] = checkpoint 124 | logger.info( 125 | "Updated checkpoint for partition {} with sequence number {}", checkpoint.partitionId, 126 | checkpoint.sequenceNumber 127 | ) 128 | return Mono.empty() 129 | } 130 | 131 | companion object { 132 | private const val OWNERSHIP = "ownership" 133 | private const val SEPARATOR = "/" 134 | private const val CHECKPOINT = "checkpoint" 135 | } 136 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | eu.adrianistan 5 | eventhub-ui 6 | 1.0.1 7 | 8 | UTF-8 9 | 11 10 | 11 11 | 1.5.30 12 | 16 13 | 14 | 15 | 16 | org.openjfx 17 | javafx-controls 18 | ${javafx.version} 19 | win 20 | 21 | 22 | org.openjfx 23 | javafx-fxml 24 | ${javafx.version} 25 | win 26 | 27 | 28 | org.openjfx 29 | javafx-controls 30 | ${javafx.version} 31 | mac 32 | 33 | 34 | org.openjfx 35 | javafx-fxml 36 | ${javafx.version} 37 | mac 38 | 39 | 40 | org.openjfx 41 | javafx-controls 42 | ${javafx.version} 43 | linux 44 | 45 | 46 | org.openjfx 47 | javafx-fxml 48 | ${javafx.version} 49 | linux 50 | 51 | 52 | com.azure 53 | azure-messaging-eventhubs 54 | 5.10.0 55 | 56 | 57 | org.jetbrains.kotlin 58 | kotlin-stdlib-jdk8 59 | ${kotlin.version} 60 | 61 | 62 | com.google.code.gson 63 | gson 64 | 2.8.8 65 | 66 | 67 | org.jetbrains.kotlin 68 | kotlin-test 69 | ${kotlin.version} 70 | test 71 | 72 | 73 | 74 | 75 | 76 | org.openjfx 77 | javafx-maven-plugin 78 | 0.0.5 79 | 80 | 1.0.0 81 | app 82 | launcher 83 | app/eu.adrianistan.Main 84 | 85 | 86 | 87 | org.jetbrains.kotlin 88 | kotlin-maven-plugin 89 | ${kotlin.version} 90 | 91 | 92 | compile 93 | process-sources 94 | 95 | compile 96 | 97 | 98 | 99 | src/main/java 100 | 101 | 102 | 103 | 104 | test-compile 105 | test-compile 106 | 107 | test-compile 108 | 109 | 110 | 111 | 112 | 11 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-compiler-plugin 118 | 3.8.0 119 | 120 | 11 121 | 122 | 123 | 124 | compile 125 | compile 126 | 127 | compile 128 | 129 | 130 | 131 | testCompile 132 | test-compile 133 | 134 | testCompile 135 | 136 | 137 | 138 | 139 | 140 | org.apache.maven.plugins 141 | maven-shade-plugin 142 | 3.2.0 143 | 144 | 145 | package 146 | 147 | shade 148 | 149 | 150 | true 151 | project-classifier 152 | shade\${project.artifactId}.jar 153 | 154 | 156 | eu.adrianistan.Main 157 | 158 | 159 | 160 | 161 | *:* 162 | 163 | META-INF/*.SF 164 | META-INF/*.DSA 165 | META-INF/*.RSA 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/main/resources/main.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 93 | 94 | 95 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 127 |