From 52f9f755e683ffbe5b6e2d55b22f835144e198c2 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 22 May 2018 17:31:57 +0200 Subject: [PATCH 01/63] Wipe out logisland-agent --- .../logisland/config/DefaultConfigValues.java | 1 - logisland-assembly/pom.xml | 4 - logisland-documentation/agent.rst | 115 -- .../component/RestComponentFactory.java | 40 - .../logisland-spark_2_1-engine/pom.xml | 10 +- .../spark/AbstractKafkaRecordStream.scala | 51 +- .../logisland/stream/spark/package.scala | 16 - .../spark/structured/StructuredStream.scala | 51 +- .../structured/handler/SQLAggregator.scala | 2 - .../util/spark/RestJobsApiClientSink.scala | 60 - .../RestStreamProcessingIntegrationTest.java | 118 -- .../logisland-agent/.swagger-codegen-ignore | 25 - .../logisland-agent/.swagger-codegen/VERSION | 1 - logisland-framework/logisland-agent/LICENSE | 201 ---- logisland-framework/logisland-agent/README.md | 36 - logisland-framework/logisland-agent/pom.xml | 344 ------ .../agent/rest/api/ApiException.java | 10 - .../agent/rest/api/ApiOriginFilter.java | 22 - .../agent/rest/api/ApiResponseMessage.java | 69 -- .../logisland/agent/rest/api/ConfigsApi.java | 54 - .../agent/rest/api/ConfigsApiService.java | 33 - .../logisland/agent/rest/api/DefaultApi.java | 51 - .../agent/rest/api/DefaultApiService.java | 31 - .../agent/rest/api/JacksonJsonProvider.java | 29 - .../logisland/agent/rest/api/JobsApi.java | 289 ----- .../agent/rest/api/JobsApiService.java | 64 -- .../logisland/agent/rest/api/MetricsApi.java | 53 - .../agent/rest/api/MetricsApiService.java | 32 - .../agent/rest/api/NotFoundException.java | 10 - .../agent/rest/api/ProcessorsApi.java | 53 - .../agent/rest/api/ProcessorsApiService.java | 32 - .../agent/rest/api/RFC3339DateFormat.java | 19 - .../logisland/agent/rest/api/StringUtil.java | 42 - .../logisland/agent/rest/api/TopicsApi.java | 216 ---- .../agent/rest/api/TopicsApiService.java | 53 - .../logisland/agent/rest/model/Engine.java | 149 --- .../logisland/agent/rest/model/Error.java | 116 -- .../logisland/agent/rest/model/Field.java | 238 ---- .../logisland/agent/rest/model/FieldType.java | 262 ----- .../logisland/agent/rest/model/Job.java | 224 ---- .../agent/rest/model/JobSummary.java | 223 ---- .../logisland/agent/rest/model/Metrics.java | 546 --------- .../logisland/agent/rest/model/Processor.java | 173 --- .../logisland/agent/rest/model/Property.java | 140 --- .../logisland/agent/rest/model/Record.java | 187 --- .../logisland/agent/rest/model/Stream.java | 208 ---- .../logisland/agent/rest/model/Topic.java | 400 ------- .../logisland/agent/LogislandAgentMain.java | 58 - .../logisland/agent/rest/RestService.java | 22 - .../logisland/agent/rest/Versions.java | 45 - .../logisland/agent/rest/api/Bootstrap.java | 55 - .../factories/ConfigsApiServiceFactory.java | 18 - .../factories/DefaultApiServiceFactory.java | 18 - .../api/factories/JobsApiServiceFactory.java | 18 - .../factories/MetricsApiServiceFactory.java | 18 - .../ProcessorsApiServiceFactory.java | 18 - .../factories/TopicsApiServiceFactory.java | 18 - .../rest/api/impl/ConfigsApiServiceImpl.java | 69 -- .../rest/api/impl/DefaultApiServiceImpl.java | 39 - .../rest/api/impl/JobsApiServiceImpl.java | 404 ------- .../rest/api/impl/MetricsApiServiceImpl.java | 197 ---- .../api/impl/ProcessorsApiServiceImpl.java | 48 - .../rest/api/impl/TopicsApiServiceImpl.java | 219 ---- .../agent/rest/client/ConfigsApiClient.java | 26 - .../agent/rest/client/JobsApiClient.java | 27 - .../rest/client/MockConfigsApiClient.java | 49 - .../agent/rest/client/MockJobsApiClient.java | 126 -- .../rest/client/MockTopicsApiClient.java | 99 -- .../rest/client/RestConfigsApiClient.java | 57 - .../agent/rest/client/RestJobsApiClient.java | 80 -- .../rest/client/RestTopicsApiClient.java | 78 -- .../agent/rest/client/TopicsApiClient.java | 25 - .../exceptions/RestClientException.java | 35 - .../agent/utils/YarnApplication.java | 120 -- .../agent/utils/YarnApplicationWrapper.java | 48 - .../avro/AvroCompatibilityChecker.java | 72 -- .../avro/AvroCompatibilityLevel.java | 51 - .../hurence/logisland/avro/AvroSchema.java | 29 - .../com/hurence/logisland/avro/AvroUtils.java | 40 - .../component/RestComponentFactory.java | 202 ---- .../kafka/registry/KafkaRegistry.java | 596 ---------- .../kafka/registry/KafkaRegistryConfig.java | 402 ------- .../KafkaRegistryRestApplication.java | 70 -- .../exceptions/IncompatibleException.java | 37 - .../registry/exceptions/InvalidException.java | 37 - .../exceptions/InvalidVersionException.java | 38 - .../exceptions/RegistryException.java | 38 - .../RegistryInitializationException.java | 38 - .../RegistryRequestForwardingException.java | 39 - .../exceptions/RegistryStoreException.java | 39 - .../exceptions/RegistryTimeoutException.java | 38 - .../exceptions/UnknownMasterException.java | 39 - .../serialization/RegistrySerializer.java | 136 --- .../kafka/serialization/Serializer.java | 56 - .../logisland/kafka/store/InMemoryStore.java | 80 -- .../hurence/logisland/kafka/store/JobKey.java | 109 -- .../logisland/kafka/store/JobValue.java | 147 --- .../logisland/kafka/store/KafkaStore.java | 457 -------- .../kafka/store/KafkaStoreReaderThread.java | 226 ---- .../kafka/store/KafkaStoreService.java | 216 ---- .../hurence/logisland/kafka/store/MD5.java | 74 -- .../logisland/kafka/store/NoopKey.java | 49 - .../logisland/kafka/store/RegistryKey.java | 85 -- .../kafka/store/RegistryKeyType.java | 42 - .../logisland/kafka/store/RegistryValue.java | 20 - .../hurence/logisland/kafka/store/Store.java | 50 - .../kafka/store/StoreUpdateHandler.java | 28 - .../logisland/kafka/store/TopicKey.java | 107 -- .../logisland/kafka/store/TopicValue.java | 147 --- .../exceptions/SerializationException.java | 39 - .../store/exceptions/StoreException.java | 39 - .../StoreInitializationException.java | 38 - .../exceptions/StoreTimeoutException.java | 30 - .../kafka/utils/KafkaOffsetUtils.java | 125 -- .../kafka/zookeeper/RegistryIdentity.java | 134 --- .../zookeeper/ZookeeperMasterElector.java | 188 --- .../src/main/resources/components.json | 43 - .../src/main/resources/log4j.properties | 20 - .../src/main/swagger/api-swagger.yaml | 1018 ----------------- .../src/main/swagger/templates/api.mustache | 66 -- .../swagger/templates/apiService.mustache | 39 - .../templates/apiServiceFactory.mustache | 18 - .../swagger/templates/apiServiceImpl.mustache | 38 - .../src/main/webapp/WEB-INF/web.xml | 63 - .../src/test/avro/extended_user.avsc | 16 - .../logisland-agent/src/test/avro/user.avsc | 11 - .../agent/utils/YarnApplicationTest.java | 41 - .../utils/YarnApplicationWrapperTest.java | 90 -- .../logisland/avro/AvroCompatibilityTest.java | 93 -- .../logisland/avro/example/ExtendedUser.java | 211 ---- .../hurence/logisland/avro/example/User.java | 157 --- .../kafka/registry/ClusterTestHarness.java | 191 ---- .../logisland/kafka/registry/RestApp.java | 92 -- .../kafka/registry/SSLClusterTestHarness.java | 80 -- .../kafka/store/KafkaRegistryTest.java | 53 - .../store/KafkaStoreReaderThreadTest.java | 74 -- .../kafka/store/KafkaStoreSSLAuthTest.java | 96 -- .../kafka/store/KafkaStoreSSLNoAuthTest.java | 29 - .../logisland/kafka/store/KafkaStoreTest.java | 308 ----- .../kafka/store/RegistryKeysTest.java | 149 --- .../logisland/kafka/store/StoreUtils.java | 117 -- .../kafka/store/StringMessageHandler.java | 31 - .../kafka/store/StringSerializer.java | 75 -- .../logisland/kafka/utils/TestUtils.java | 128 --- .../kafka/zookeeper/MasterElectorTest.java | 698 ----------- .../logisland-bootstrap/pom.xml | 4 - .../logisland/runner/SparkJobLauncher.java | 108 -- .../resources/bin/kafka-avro-console-consumer | 49 - .../resources/bin/kafka-avro-console-producer | 50 - .../resources/bin/logisland-agent-run-class | 101 -- .../main/resources/bin/logisland-agent-start | 17 - .../main/resources/bin/logisland-agent-stop | 18 - .../bin/logisland-agent-stop-service | 32 - .../resources/bin/logisland-launch-spark-job | 268 ----- .../src/main/resources/docs/agent.rst | 115 -- logisland-framework/pom.xml | 8 - pom.xml | 7 +- 157 files changed, 11 insertions(+), 16375 deletions(-) delete mode 100644 logisland-documentation/agent.rst delete mode 100644 logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/component/RestComponentFactory.java delete mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/spark/RestJobsApiClientSink.scala delete mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RestStreamProcessingIntegrationTest.java delete mode 100644 logisland-framework/logisland-agent/.swagger-codegen-ignore delete mode 100644 logisland-framework/logisland-agent/.swagger-codegen/VERSION delete mode 100644 logisland-framework/logisland-agent/LICENSE delete mode 100644 logisland-framework/logisland-agent/README.md delete mode 100644 logisland-framework/logisland-agent/pom.xml delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiException.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiOriginFilter.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiResponseMessage.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApi.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApiService.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApi.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApiService.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JacksonJsonProvider.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApi.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApiService.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApi.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApiService.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/NotFoundException.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApi.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApiService.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/RFC3339DateFormat.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/StringUtil.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApi.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApiService.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Engine.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Error.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Field.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/FieldType.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Job.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/JobSummary.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Metrics.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Processor.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Property.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Record.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Stream.java delete mode 100644 logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Topic.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/LogislandAgentMain.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/RestService.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/Versions.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/Bootstrap.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ConfigsApiServiceFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/DefaultApiServiceFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/JobsApiServiceFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/MetricsApiServiceFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ProcessorsApiServiceFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/TopicsApiServiceFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ConfigsApiServiceImpl.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/DefaultApiServiceImpl.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/JobsApiServiceImpl.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/MetricsApiServiceImpl.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ProcessorsApiServiceImpl.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/TopicsApiServiceImpl.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/ConfigsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/JobsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockConfigsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockJobsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockTopicsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestConfigsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestJobsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestTopicsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/TopicsApiClient.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/exceptions/RestClientException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplication.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplicationWrapper.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityChecker.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityLevel.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroSchema.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroUtils.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/component/RestComponentFactory.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistry.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryConfig.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryRestApplication.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/IncompatibleException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidVersionException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryInitializationException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryRequestForwardingException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryStoreException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryTimeoutException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/UnknownMasterException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/RegistrySerializer.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/Serializer.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/InMemoryStore.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobKey.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobValue.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStore.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThread.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreService.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/MD5.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/NoopKey.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKey.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKeyType.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryValue.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/Store.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/StoreUpdateHandler.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicKey.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicValue.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/SerializationException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreInitializationException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreTimeoutException.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/utils/KafkaOffsetUtils.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/RegistryIdentity.java delete mode 100644 logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/ZookeeperMasterElector.java delete mode 100644 logisland-framework/logisland-agent/src/main/resources/components.json delete mode 100644 logisland-framework/logisland-agent/src/main/resources/log4j.properties delete mode 100644 logisland-framework/logisland-agent/src/main/swagger/api-swagger.yaml delete mode 100644 logisland-framework/logisland-agent/src/main/swagger/templates/api.mustache delete mode 100644 logisland-framework/logisland-agent/src/main/swagger/templates/apiService.mustache delete mode 100644 logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceFactory.mustache delete mode 100644 logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceImpl.mustache delete mode 100644 logisland-framework/logisland-agent/src/main/webapp/WEB-INF/web.xml delete mode 100644 logisland-framework/logisland-agent/src/test/avro/extended_user.avsc delete mode 100644 logisland-framework/logisland-agent/src/test/avro/user.avsc delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationWrapperTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/AvroCompatibilityTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/ExtendedUser.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/User.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/ClusterTestHarness.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/RestApp.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/SSLClusterTestHarness.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaRegistryTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThreadTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLAuthTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLNoAuthTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/RegistryKeysTest.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StoreUtils.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringMessageHandler.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringSerializer.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/utils/TestUtils.java delete mode 100644 logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/zookeeper/MasterElectorTest.java delete mode 100644 logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/SparkJobLauncher.java delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-consumer delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-producer delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-run-class delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-start delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop-service delete mode 100755 logisland-framework/logisland-resources/src/main/resources/bin/logisland-launch-spark-job delete mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/agent.rst diff --git a/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java b/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java index f48a1f94e..969d59b8f 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java +++ b/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java @@ -25,7 +25,6 @@ public enum DefaultConfigValues { KAFKA_BROKERS("sandbox:9092"), ZK_QUORUM("sandbox:2181"), SOLR_CONNECTION("http://sandbox:8983/solr"), - LOGISLAND_AGENT_HOST("sandbox:8008"), MQTT_BROKER_URL("tcp://sandbox:1883"); diff --git a/logisland-assembly/pom.xml b/logisland-assembly/pom.xml index ccf45cca9..78c8ec7ef 100644 --- a/logisland-assembly/pom.xml +++ b/logisland-assembly/pom.xml @@ -226,10 +226,6 @@ com.hurence.logisland logisland-spark_2_1-engine_${scala.binary.version} - - com.hurence.logisland - logisland-agent - com.hurence.logisland logisland-connect-spark diff --git a/logisland-documentation/agent.rst b/logisland-documentation/agent.rst deleted file mode 100644 index f0c9159ac..000000000 --- a/logisland-documentation/agent.rst +++ /dev/null @@ -1,115 +0,0 @@ - -The agent -========= - - -![agent smith](http://img09.deviantart.net/b93e/i/2007/141/a/2/matrix_agent_smith__stencil_by_vegetablelambtartary.jpg) - -The Logisland Agent provides a serving layer for your metadata. - -- provides a RESTful interface for storing and retrieving Avro schemas. -- stores a versioned history of all schemas -- provides multiple compatibility settings -- allows evolution of schemas according to the configured compatibility setting. -- provides serializers that plug into Kafka clients that handle schema storage and retrieval for Kafka messages that are sent in the Avro format. - - - -Deployment ----------- -We recommend to use the provided docker compose script. -Be sure to set `127.0.0.1 sandbox` in your `/etc/hosts` file - -.. code-block:: sh - - # start a Docker container containing Kafka - cd $LOGISLAND_HOME - docker-compose -f conf/docker-compose.yml up -d - -Starting the Logisland Agent is simple once its dependencies are running. - -.. code-block:: sh - - # The default settings in logisland.properties work automatically with - # the default settings for local ZooKeeper and Kafka nodes. - bin/logisland-agent-start conf/logisland.properties - - -On production environment you'll need to export SPARK_HOME and HADOOP_CONF_DIR variables : - -.. code-block:: sh - - export SPARK_HOME=/opt/spark-2.1.0-bin-hadoop2.7 - export HADOOP_CONF_DIR=/usr/hdp/current/hadoop-client/conf/ - -Schema registry -_______________ - -The following assumes you have Kafka and an instance of the Logisland Agent running using the default settings. - -A quick list of REST call to schema registry - -.. code-block:: sh - - # Register a new version of a schema under the subject "Kafka-key" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/subjects/Kafka-key/versions - {"id":1} - - # Register a new version of a schema under the subject "Kafka-value" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/subjects/Kafka-value/versions - {"id":1} - - # List all subjects - curl -X GET http://localhost:8081/subjects - ["Kafka-value","Kafka-key"] - - # List all schema versions registered under the subject "Kafka-value" - curl -X GET http://localhost:8081/subjects/Kafka-value/versions - [1] - - # Fetch a schema by globally unique id 1 - curl -X GET http://localhost:8081/schemas/ids/1 - {"schema":"\"string\""} - - # Fetch version 1 of the schema registered under subject "Kafka-value" - curl -X GET http://localhost:8081/subjects/Kafka-value/versions/1 - {"subject":"Kafka-value","version":1,"id":1,"schema":"\"string\""} - - # Fetch the most recently registered schema under subject "Kafka-value" - curl -X GET http://localhost:8081/subjects/Kafka-value/versions/latest - {"subject":"Kafka-value","version":1,"id":1,"schema":"\"string\""} - - # Check whether a schema has been registered under subject "Kafka-key" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/subjects/Kafka-key - {"subject":"Kafka-key","version":1,"id":1,"schema":"\"string\""} - - # Test compatibility of a schema with the latest schema under subject "Kafka-value" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/compatibility/subjects/Kafka-value/versions/latest - {"is_compatible":true} - - # Get top level config - curl -X GET http://localhost:8081/config - {"compatibilityLevel":"BACKWARD"} - - # Update compatibility requirements globally - curl -X PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"compatibility": "NONE"}' \ - http://localhost:8081/config - {"compatibility":"NONE"} - - # Update compatibility requirements under the subject "Kafka-value" - curl -X PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"compatibility": "BACKWARD"}' \ - http://localhost:8081/config/Kafka-value - {"compatibility":"BACKWARD"} - - - diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/component/RestComponentFactory.java b/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/component/RestComponentFactory.java deleted file mode 100644 index d0f46f1d3..000000000 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/component/RestComponentFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.component; - - -import com.hurence.logisland.engine.EngineContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Optional; - -/** - * Fake class for build compatibility in hdp2.4 profile - * this class is implemented in logisland-agent which is not available in hdp2.4 profile - */ -public class RestComponentFactory { - - private static Logger logger = LoggerFactory.getLogger(RestComponentFactory.class); - public RestComponentFactory(String fakeArg) { - logger.error("you should not use this class with this maven profile"); - } - - public Optional getEngineContext(String jobName) { - logger.error("you should not use this class with this maven profile"); - return Optional.empty(); - } -} diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index d3b694152..75f33d54e 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -32,10 +32,6 @@ http://www.w3.org/2001/XMLSchema-instance "> com.hurence.logisland logisland-api - - com.hurence.logisland - logisland-agent - com.hurence.logisland logisland-utils @@ -120,6 +116,12 @@ http://www.w3.org/2001/XMLSchema-instance "> junit + + ch.qos.logback + logback-classic + test + + org.apache.kafka diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala index a246f65d5..8366cbb0f 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala @@ -20,14 +20,14 @@ import java.io.ByteArrayInputStream import java.util import java.util.Collections -import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor, RestComponentFactory} +import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext import com.hurence.logisland.record.Record import com.hurence.logisland.serializer._ -import com.hurence.logisland.stream.{AbstractRecordStream, StreamContext, StreamProperties} +import com.hurence.logisland.stream.StreamProperties._ +import com.hurence.logisland.stream.{AbstractRecordStream, StreamContext} import com.hurence.logisland.util.kafka.KafkaSink import com.hurence.logisland.util.spark._ -import com.hurence.logisland.validator.StandardValidators import kafka.admin.AdminUtils import kafka.utils.ZkUtils import org.apache.avro.Schema.Parser @@ -46,7 +46,6 @@ import org.apache.spark.streaming.{Seconds, StreamingContext} import org.slf4j.LoggerFactory import scala.collection.JavaConversions._ -import com.hurence.logisland.stream.StreamProperties._ @@ -62,10 +61,7 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark @transient protected var ssc: StreamingContext = null protected var streamContext: StreamContext = null protected var engineContext: EngineContext = null - protected var restApiSink: Broadcast[RestJobsApiClientSink] = null protected var controllerServiceLookupSink: Broadcast[ControllerServiceLookupSink] = null - protected var currentJobVersion: Int = 0 - protected var lastCheckCount: Int = 0 protected var needMetricsReset = false override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { @@ -84,8 +80,6 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark descriptors.add(KAFKA_METADATA_BROKER_LIST) descriptors.add(KAFKA_ZOOKEEPER_QUORUM) descriptors.add(KAFKA_MANUAL_OFFSET_RESET) - descriptors.add(LOGISLAND_AGENT_HOST) - descriptors.add(LOGISLAND_AGENT_PULL_THROTTLING) descriptors.add(KAFKA_BATCH_SIZE) descriptors.add(KAFKA_LINGER_MS) descriptors.add(KAFKA_ACKS) @@ -123,8 +117,6 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark val brokerList = streamContext.getPropertyValue(KAFKA_METADATA_BROKER_LIST).asString val zkQuorum = streamContext.getPropertyValue(KAFKA_ZOOKEEPER_QUORUM).asString - val agentQuorum = streamContext.getPropertyValue(LOGISLAND_AGENT_HOST).asString - val throttling = streamContext.getPropertyValue(LOGISLAND_AGENT_PULL_THROTTLING).asInteger() val kafkaBatchSize = streamContext.getPropertyValue(KAFKA_BATCH_SIZE).asString val kafkaLingerMs = streamContext.getPropertyValue(KAFKA_LINGER_MS).asString val kafkaAcks = streamContext.getPropertyValue(KAFKA_ACKS).asString @@ -144,7 +136,6 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG -> "1000") kafkaSink = ssc.sparkContext.broadcast(KafkaSink(kafkaSinkParams)) - restApiSink = ssc.sparkContext.broadcast(RestJobsApiClientSink(agentQuorum)) controllerServiceLookupSink = ssc.sparkContext.broadcast( ControllerServiceLookupSink(engineContext.getControllerServiceConfigurations) ) @@ -181,9 +172,6 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark Subscribe[Array[Byte], Array[Byte]](inputTopics, kafkaParams) ) - // store current configuration version - currentJobVersion = restApiSink.value.getJobApiClient.getJobVersion(appName) - // do the parallel processing val stream = if (streamContext.getPropertyValue(WINDOW_DURATION).isSet) { @@ -203,39 +191,6 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark if (!rdd.isEmpty()) { - /** - * check if conf needs to be refreshed - */ - if (lastCheckCount > throttling) { - lastCheckCount = 0 - val version = restApiSink.value.getJobApiClient.getJobVersion(appName) - if (currentJobVersion != version) { - logger.info("Job version change detected from {} to {}, proceeding to update", - currentJobVersion, - version) - - val componentFactory = new RestComponentFactory(agentQuorum) - val updatedEngineContext = componentFactory.getEngineContext(appName) - if (updatedEngineContext.isPresent) { - - // find the corresponding stream - val it = updatedEngineContext.get().getStreamContexts.iterator() - while (it.hasNext) { - val updatedStreamingContext = it.next() - - // if we found a streamContext with the same name from the factory - if (updatedStreamingContext.getName == this.streamContext.getName) { - logger.info("new conf for stream {}", updatedStreamingContext.getName) - this.streamContext = updatedStreamingContext - } - } - } - currentJobVersion = version - } - } - - lastCheckCount += 1 - val offsetRanges = process(rdd) // some time later, after outputs have completed diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala index d07879bd9..d9e622634 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala @@ -183,22 +183,6 @@ object StreamProperties { .defaultValue(EARLIEST_OFFSET.getValue) .build - val LOGISLAND_AGENT_HOST: PropertyDescriptor = new PropertyDescriptor.Builder() - .name("logisland.agent.host") - .description("the stream needs to know how to reach Agent REST api in order to live update its processors") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("sandbox:8081") - .build - - val LOGISLAND_AGENT_PULL_THROTTLING: PropertyDescriptor = new PropertyDescriptor.Builder() - .name("logisland.agent.pull.throttling") - .description("wait every x batch to pull agent for new conf") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("10") - .build - val KAFKA_BATCH_SIZE: PropertyDescriptor = new PropertyDescriptor.Builder() .name("kafka.batch.size") diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala index b968a4499..111165bd2 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala @@ -19,7 +19,7 @@ package com.hurence.logisland.stream.spark.structured import java.util import java.util.Collections -import com.hurence.logisland.component.{PropertyDescriptor, RestComponentFactory} +import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext import com.hurence.logisland.logging.StandardComponentLogger import com.hurence.logisland.stream.StreamProperties._ @@ -44,10 +44,7 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { @transient protected var ssc: StreamingContext = _ @transient protected var streamContext: StreamContext = _ protected var engineContext: EngineContext = _ - protected var restApiSink: Broadcast[RestJobsApiClientSink] = _ protected var controllerServiceLookupSink: Broadcast[ControllerServiceLookupSink] = _ - protected var currentJobVersion: Int = 0 - protected var lastCheckCount: Int = 0 protected var needMetricsReset = false @@ -64,8 +61,6 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { descriptors.add(WRITE_TOPICS_CLIENT_SERVICE) descriptors.add(WRITE_TOPICS_SERIALIZER) descriptors.add(WRITE_TOPICS_KEY_SERIALIZER) - descriptors.add(LOGISLAND_AGENT_HOST) - descriptors.add(LOGISLAND_AGENT_PULL_THROTTLING) Collections.unmodifiableList(descriptors) } @@ -85,15 +80,10 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { throw new IllegalStateException("stream not initialized") try { - // Thread.sleep(5000) - val agentQuorum = streamContext.getPropertyValue(LOGISLAND_AGENT_HOST).asString - val throttling = streamContext.getPropertyValue(LOGISLAND_AGENT_PULL_THROTTLING).asInteger() val pipelineMetricPrefix = streamContext.getIdentifier /*+ ".partition" + partitionId*/ + "." val pipelineTimerContext = UserMetricsSystem.timer(pipelineMetricPrefix + "Pipeline.processing_time_ms").time() - - restApiSink = ssc.sparkContext.broadcast(RestJobsApiClientSink(agentQuorum)) controllerServiceLookupSink = ssc.sparkContext.broadcast( ControllerServiceLookupSink(engineContext.getControllerServiceConfigurations) ) @@ -115,10 +105,6 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { val readDF = readStreamService.load(spark, controllerServiceLookupSink, streamContext) - // store current configuration version - currentJobVersion = restApiSink.value.getJobApiClient.getJobVersion(appName) - updateConfigFromAgent(agentQuorum, throttling) - // apply windowing /*val windowedDF:Dataset[Record] = if (streamContext.getPropertyValue(WINDOW_DURATION).isSet) { if (streamContext.getPropertyValue(SLIDE_DURATION).isSet) @@ -151,41 +137,6 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { } } - - private def updateConfigFromAgent(agentQuorum: String, throttling: Integer) = { - /** - * check if conf needs to be refreshed - */ - if (lastCheckCount > throttling) { - lastCheckCount = 0 - val version = restApiSink.value.getJobApiClient.getJobVersion(appName) - if (currentJobVersion != version) { - logger.info(s"Job version change detected from $currentJobVersion to $version, proceeding to update") - - val componentFactory = new RestComponentFactory(agentQuorum) - val updatedEngineContext = componentFactory.getEngineContext(appName) - if (updatedEngineContext.isPresent) { - - // find the corresponding stream - val it = updatedEngineContext.get().getStreamContexts.iterator() - while (it.hasNext) { - val updatedStreamingContext = it.next() - - // if we found a streamContext with the same name from the factory - if (updatedStreamingContext.getName == this.streamContext.getName) { - logger.info(s"new conf for stream ${updatedStreamingContext.getName}") - this.streamContext = updatedStreamingContext - } - } - } - currentJobVersion = version - } - } - - lastCheckCount += 1 - } - - } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala index 756ef3978..85aa52320 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala @@ -54,8 +54,6 @@ class SQLAggregator extends StructuredStreamHandler { descriptors.add(KAFKA_METADATA_BROKER_LIST) descriptors.add(KAFKA_ZOOKEEPER_QUORUM) descriptors.add(KAFKA_MANUAL_OFFSET_RESET) - descriptors.add(LOGISLAND_AGENT_HOST) - descriptors.add(LOGISLAND_AGENT_PULL_THROTTLING) descriptors.add(KAFKA_BATCH_SIZE) descriptors.add(KAFKA_LINGER_MS) descriptors.add(KAFKA_ACKS) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/spark/RestJobsApiClientSink.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/spark/RestJobsApiClientSink.scala deleted file mode 100644 index 695d1b400..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/spark/RestJobsApiClientSink.scala +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.util.spark - -import com.hurence.logisland.agent.rest.client.{JobsApiClient, RestJobsApiClient} -import org.slf4j.LoggerFactory - - -/** - * lazy instanciation of the RestJobsApiClient, usefull in spark streaming foreachRDD - * - */ -class RestJobsApiClientSink(createRestJobsApiClient: () => JobsApiClient) extends Serializable { - - lazy val jobsApiClient = createRestJobsApiClient() - private val logger = LoggerFactory.getLogger(classOf[RestJobsApiClientSink]) - - - def getJobApiClient: JobsApiClient = jobsApiClient -} - -object RestJobsApiClientSink { - private val logger = LoggerFactory.getLogger(classOf[RestJobsApiClientSink]) - - def apply(agentQuorum: String): RestJobsApiClientSink = { - val f = () => { - logger.info("creating REST API client with agentQuorum {}", agentQuorum) - new RestJobsApiClient(agentQuorum) - } - new RestJobsApiClientSink(f) - } -} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RestStreamProcessingIntegrationTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RestStreamProcessingIntegrationTest.java deleted file mode 100644 index 4a7b94c7e..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RestStreamProcessingIntegrationTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.engine; - -import com.hurence.logisland.agent.rest.client.MockConfigsApiClient; -import com.hurence.logisland.agent.rest.client.MockJobsApiClient; -import com.hurence.logisland.agent.rest.client.MockTopicsApiClient; -import com.hurence.logisland.component.RestComponentFactory; -import com.hurence.logisland.record.FieldType; -import com.hurence.logisland.record.Record; -import com.hurence.logisland.record.StandardRecord; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.*; - -import static org.junit.Assert.assertTrue; - -/** - * Empty Java class for source jar generation (need to publish on OSS sonatype) - */ -public class RestStreamProcessingIntegrationTest extends AbstractStreamProcessingIntegrationTest { - - - public static final String MAGIC_STRING = "the world is so big"; - - - private static Logger logger = LoggerFactory.getLogger(RestStreamProcessingIntegrationTest.class); - - - Optional getEngineContext() { - - String zkPort = String.valueOf(zkServer.port()); - String kafkaPort = String.valueOf(BROKERPORT); - RestComponentFactory componentFactory = - new RestComponentFactory( - new MockJobsApiClient(), - new MockTopicsApiClient(), - new MockConfigsApiClient(zkPort, kafkaPort)); - - return componentFactory.getEngineContext(MockJobsApiClient.MOCK_PROCESSING_JOB); - } - - - @Test - @Ignore - public void validateIntegration() throws NoSuchFieldException, IllegalAccessException, InterruptedException, IOException { - - final List records = new ArrayList<>(); - - Runnable testRunnable = () -> { - - - // send message - Record record = new StandardRecord("cisco"); - record.setId("firewall_record1"); - record.setField("method", FieldType.STRING, "GET"); - record.setField("ip_source", FieldType.STRING, "123.34.45.123"); - record.setField("ip_target", FieldType.STRING, "255.255.255.255"); - record.setField("url_scheme", FieldType.STRING, "http"); - record.setField("url_host", FieldType.STRING, "origin-www.20minutes.fr"); - record.setField("url_port", FieldType.STRING, "80"); - record.setField("url_path", FieldType.STRING, "/r15lgc-100KB.js"); - record.setField("request_size", FieldType.INT, 1399); - record.setField("response_size", FieldType.INT, 452); - record.setField("is_outside_office_hours", FieldType.BOOLEAN, false); - record.setField("is_host_blacklisted", FieldType.BOOLEAN, false); - record.setField("tags", FieldType.ARRAY, new ArrayList<>(Arrays.asList("spam", "filter", "mail"))); - - - try { - Thread.sleep(8000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - try { - sendRecord(INPUT_TOPIC, record); - } catch (IOException e) { - e.printStackTrace(); - } - - - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - records.addAll(readRecords(OUTPUT_TOPIC)); - }; - - Thread t = new Thread(testRunnable); - logger.info("starting validation thread {}", t.getId()); - t.start(); - - - Thread.sleep(15000); - assertTrue(records.size() == 1); - assertTrue(records.get(0).size() == 13); - assertTrue(records.get(0).getField("message").asString().equals(MAGIC_STRING)); - - } -} diff --git a/logisland-framework/logisland-agent/.swagger-codegen-ignore b/logisland-framework/logisland-agent/.swagger-codegen-ignore deleted file mode 100644 index 7d31ab7af..000000000 --- a/logisland-framework/logisland-agent/.swagger-codegen-ignore +++ /dev/null @@ -1,25 +0,0 @@ -# Swagger Codegen Ignore -# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - - -#**/com/hurence/logisland/agent/rest/api/impl/* -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/logisland-framework/logisland-agent/.swagger-codegen/VERSION b/logisland-framework/logisland-agent/.swagger-codegen/VERSION deleted file mode 100644 index 6b4d15773..000000000 --- a/logisland-framework/logisland-agent/.swagger-codegen/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.2.3 \ No newline at end of file diff --git a/logisland-framework/logisland-agent/LICENSE b/logisland-framework/logisland-agent/LICENSE deleted file mode 100644 index 8dada3eda..000000000 --- a/logisland-framework/logisland-agent/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/logisland-framework/logisland-agent/README.md b/logisland-framework/logisland-agent/README.md deleted file mode 100644 index 9d6f4ade7..000000000 --- a/logisland-framework/logisland-agent/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Swagger Jersey generated server - - -## Start the agent - - - cd $LOGISLAND_HOME - bin/logisland-agent-start conf/logisland.properties - - -## Overview -This server was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project. By using the -[OpenAPI-Spec](https://github.com/swagger-api/swagger-core/wiki) from a remote server, you can easily generate a server stub. This -is an example of building a swagger-enabled JAX-RS server. - -This example uses the [JAX-RS](https://jax-rs-spec.java.net/) framework. - -To run the server, please execute the following: - -``` -mvn clean package jetty:run -``` - -You can then view the swagger listing here: - -``` -http://localhost:8080/agent/api/v0.10.0/swagger.json -``` - -Note that if you have configured the `host` to be something other than localhost, the calls through -swagger-ui will be directed to that host and not localhost! - - -``` -swagger-codegen generate --group-id com.hurence.logisland --artifact-id logisland-agent --artifact-version 0.12.2 --api-package com.hurence.logisland.agent.rest.api --model-package com.hurence.logisland.agent.rest.model -o logisland-framework/logisland-agent -l jaxrs --template-dir logisland-framework/logisland-agent/src/main/raml/templates -i logisland-framework/logisland-agent/src/main/raml/api-swagger.yaml -``` diff --git a/logisland-framework/logisland-agent/pom.xml b/logisland-framework/logisland-agent/pom.xml deleted file mode 100644 index b6250965d..000000000 --- a/logisland-framework/logisland-agent/pom.xml +++ /dev/null @@ -1,344 +0,0 @@ - - - - 4.0.0 - - com.hurence.logisland - logisland-framework - 0.12.2 - - logisland-agent - jar - - 1.5.9 - 9.2.9.v20150224 - 3.0.11.Final - 2.5 - ${logisland.shade.packageName}.agent - - - - com.hurence.logisland - logisland-api - - - com.hurence.logisland - logisland-utils - - - com.hurence.logisland - logisland-common-processors-plugin - - - - org.slf4j - slf4j-api - - - ch.qos.logback - logback-classic - - - - - org.apache.kafka - kafka_${scala.binary.version} - - - org.apache.kafka - connect-api - ${kafka.version} - provided - - - org.apache.avro - avro - - - io.confluent - common-config - - - io.confluent - common-utils - - - io.confluent - rest-utils - - - - - com.fasterxml.jackson.core - jackson-core - 2.4.4 - - - com.fasterxml.jackson.core - jackson-databind - 2.4.4 - - - com.fasterxml.jackson.core - jackson-annotations - 2.4.4 - - - - org.apache.commons - commons-exec - - - commons-collections - commons-collections - - - - io.swagger - swagger-jersey2-jaxrs - compile - - - javax.servlet - servlet-api - provided - - - - - - - org.glassfish.jersey.core - jersey-client - - - org.glassfish.jersey.ext - jersey-proxy-client - - - org.glassfish.jersey.containers - jersey-container-servlet-core - - - org.glassfish.jersey.media - jersey-media-multipart - - - org.glassfish.jersey.media - jersey-media-json-jackson - - - - - org.easymock - easymock - RELEASE - test - - - org.powermock - powermock-module-junit4 - RELEASE - test - - - - org.apache.kafka - kafka_${scala.binary.version} - ${kafka.version} - test - test - - - org.apache.kafka - kafka-clients - ${kafka.version} - test - test - - - - org.bouncycastle - bcpkix-jdk15on - 1.54 - test - - - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 1.9.1 - - - add-source - generate-sources - - add-source - - - - src/gen/java - - - - - - - org.immutables.tools - maven-shade-plugin - 4 - - - package - - shade - - - true - - - - - com.hurence.logisland:logisland-api - com.hurence.logisland:logisland-utils - org.slf4j:* - junit:junit - jmock:* - ch.qos.logback:* - org.scala-lang:* - org.apache.zookeeper:* - commons-io:* - commons-cli:* - commons-codec:* - commons-collections:* - joda-time:* - org.json:* - org.apache.curator:* - org.apache.avro:* - org.codehaus.jackson:* - com.fasterxml.jackson.dataformat:* - com.googlecode.json:* - - - - - *:* - - META-INF/license/** - META-INF/* - META-INF/maven/** - LICENSE - NOTICE - /*.txt - build.properties - - - - - - - - GIT commit ID - ${maven.build.timestamp} - - - - - - - io - ${shaded.package}.io - - - - - com.google - ${shaded.package}.com.google - - - com.spatial4j - ${shaded.package}.com.spatial4j - - - com.fasterxml - ${shaded.package}.com.fasterxml - - - com.carrotsearch - ${shaded.package}.com.carrotsearch - - - com.twitter - ${shaded.package}.com.twitter - - - com.tdunning - ${shaded.package}.com.tdunning - - - com.thoughtworks - ${shaded.package}.com.thoughtworks - - - - com.ning - ${shaded.package}.com.ning - - - org.apache.lucene - ${shaded.package}.org.apache.lucene - - - org.elasticsearch - ${shaded.package}.org.elasticsearch - - - - - - - - - - - - - - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiException.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiException.java deleted file mode 100644 index fd9503dc5..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ApiException extends Exception{ - private int code; - public ApiException (int code, String msg) { - super(msg); - this.code = code; - } -} diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiOriginFilter.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiOriginFilter.java deleted file mode 100644 index 132c86101..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiOriginFilter.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import java.io.IOException; - -import javax.servlet.*; -import javax.servlet.http.HttpServletResponse; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ApiOriginFilter implements javax.servlet.Filter { - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - HttpServletResponse res = (HttpServletResponse) response; - res.addHeader("Access-Control-Allow-Origin", "*"); - res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT"); - res.addHeader("Access-Control-Allow-Headers", "Content-Type"); - chain.doFilter(request, response); - } - - public void destroy() {} - - public void init(FilterConfig filterConfig) throws ServletException {} -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiResponseMessage.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiResponseMessage.java deleted file mode 100644 index 7f96b14dd..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ApiResponseMessage.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import javax.xml.bind.annotation.XmlTransient; - -@javax.xml.bind.annotation.XmlRootElement -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ApiResponseMessage { - public static final int ERROR = 1; - public static final int WARNING = 2; - public static final int INFO = 3; - public static final int OK = 4; - public static final int TOO_BUSY = 5; - - int code; - String type; - String message; - - public ApiResponseMessage(){} - - public ApiResponseMessage(int code, String message){ - this.code = code; - switch(code){ - case ERROR: - setType("error"); - break; - case WARNING: - setType("warning"); - break; - case INFO: - setType("info"); - break; - case OK: - setType("ok"); - break; - case TOO_BUSY: - setType("too busy"); - break; - default: - setType("unknown"); - break; - } - this.message = message; - } - - @XmlTransient - public int getCode() { - return code; - } - - public void setCode(int code) { - this.code = code; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } -} diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApi.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApi.java deleted file mode 100644 index 95b22cbc7..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApi.java +++ /dev/null @@ -1,54 +0,0 @@ -// hola -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.agent.rest.api.ConfigsApiService; -import com.hurence.logisland.agent.rest.api.factories.ConfigsApiServiceFactory; - -import io.swagger.annotations.ApiParam; - - import javax.validation.constraints.*; - -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Property; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/configs") -@Consumes({ "application/json" }) -@Produces({ "application/json" }) -@io.swagger.annotations.Api(description = "the configs API") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ConfigsApi { - - private final ConfigsApiService delegate; - - public ConfigsApi(KafkaRegistry kafkaRegistry) { - this.delegate = ConfigsApiServiceFactory.getConfigsApi(kafkaRegistry); - } - - @GET - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "global config", notes = "get all global configuration properties", response = Property.class, responseContainer = "List", tags={ "config" }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "global configuration", response = Property.class, responseContainer = "List"), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Property.class, responseContainer = "List") }) - public Response getConfig( - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getConfig(securityContext); - } - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApiService.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApiService.java deleted file mode 100644 index 7dc974ea0..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ConfigsApiService.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.agent.rest.model.*; - - - -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Property; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - import javax.validation.constraints.*; -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public abstract class ConfigsApiService { - - protected final KafkaRegistry kafkaRegistry; - - public ConfigsApiService(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - public abstract Response getConfig(SecurityContext securityContext) - throws NotFoundException; - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApi.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApi.java deleted file mode 100644 index f5a7e9cf4..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApi.java +++ /dev/null @@ -1,51 +0,0 @@ -// hola -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.agent.rest.api.DefaultApiService; -import com.hurence.logisland.agent.rest.api.factories.DefaultApiServiceFactory; - -import io.swagger.annotations.ApiParam; - - import javax.validation.constraints.*; - - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/") -@Consumes({ "application/json" }) -@Produces({ "application/json" }) -@io.swagger.annotations.Api(description = "the API") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class DefaultApi { - - private final DefaultApiService delegate; - - public DefaultApi(KafkaRegistry kafkaRegistry) { - this.delegate = DefaultApiServiceFactory.getDefaultApi(kafkaRegistry); - } - - @GET - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "the root resource", notes = "/ entrypoint", response = void.class, tags={ }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "OK", response = void.class) }) - public Response rootGet( - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.rootGet(securityContext); - } - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApiService.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApiService.java deleted file mode 100644 index 3d719a1c1..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/DefaultApiService.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.agent.rest.model.*; - - - - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - import javax.validation.constraints.*; -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public abstract class DefaultApiService { - - protected final KafkaRegistry kafkaRegistry; - - public DefaultApiService(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - public abstract Response rootGet(SecurityContext securityContext) - throws NotFoundException; - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JacksonJsonProvider.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JacksonJsonProvider.java deleted file mode 100644 index 8cfbdca19..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JacksonJsonProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -import com.fasterxml.jackson.datatype.joda.*; - -import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; - -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.ext.Provider; - -@Provider -@Produces({MediaType.APPLICATION_JSON}) -public class JacksonJsonProvider extends JacksonJaxbJsonProvider { - - public JacksonJsonProvider() { - - ObjectMapper objectMapper = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) - .registerModule(new JodaModule()) - .setDateFormat(new RFC3339DateFormat()); - - setMapper(objectMapper); - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApi.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApi.java deleted file mode 100644 index 52f96319b..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApi.java +++ /dev/null @@ -1,289 +0,0 @@ -// hola -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.agent.rest.api.JobsApiService; -import com.hurence.logisland.agent.rest.api.factories.JobsApiServiceFactory; - -import io.swagger.annotations.ApiParam; - - import javax.validation.constraints.*; - -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Job; -import com.hurence.logisland.agent.rest.model.Metrics; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/jobs") -@Consumes({ "application/json" }) -@Produces({ "application/json" }) -@io.swagger.annotations.Api(description = "the jobs API") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class JobsApi { - - private final JobsApiService delegate; - - public JobsApi(KafkaRegistry kafkaRegistry) { - this.delegate = JobsApiServiceFactory.getJobsApi(kafkaRegistry); - } - - @POST - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "create new job", notes = "store a new job configuration if valid", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Job successfuly created", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 400, message = "Invalid ID supplied", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 404, message = "Job not found", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response addJob( - @ApiParam(value = "Job to add to the store" ,required=true) Job job -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.addJob(job,securityContext); - } - @POST - @Path("/{jobId}") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "create new job", notes = "store a new job configuration if valid", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Job successfuly created", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 400, message = "Invalid ID supplied", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 404, message = "Job not found", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response addJobWithId( - @ApiParam(value = "Job configuration to add to the store" ,required=true) Job body -, - @ApiParam(value = "JobId to add to the store",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.addJobWithId(body,jobId,securityContext); - } - @DELETE - @Path("/{jobId}") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "delete job", notes = "remove the corresponding Job definition and stop if its currently running", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job successfully removed", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 400, message = "Invalid ID supplied", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 404, message = "Job not found", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response deleteJob( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.deleteJob(jobId,securityContext); - } - @GET - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get all jobs", notes = "retrieve all jobs (retrieve only summary fields)", response = Job.class, responseContainer = "List", tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job configuration list", response = Job.class, responseContainer = "List"), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class, responseContainer = "List") }) - public Response getAllJobs( - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getAllJobs(securityContext); - } - @GET - @Path("/{jobId}") - @Consumes({ "application/json" }) - @Produces({ "application/json", "text/plain" }) - @io.swagger.annotations.ApiOperation(value = "get job", notes = "get the corresponding Job definition", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job definition", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response getJob( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJob(jobId,securityContext); - } - @GET - @Path("/alerts") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get job alerts", notes = "get the alerts", response = Metrics.class, responseContainer = "List", tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job metrics", response = Metrics.class, responseContainer = "List"), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Metrics.class, responseContainer = "List") }) - public Response getJobAlerts( - @ApiParam(value = "max number of ites to retrieve", defaultValue="20") @DefaultValue("20") @QueryParam("count") Integer count -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJobAlerts(count,securityContext); - } - @GET - @Path("/{jobId}/engine") - @Consumes({ "application/json" }) - @Produces({ "text/plain" }) - @io.swagger.annotations.ApiOperation(value = "get job engine configuration", notes = "this is usefull when you want to launch a spark app within YARN to retrieve the launching config before submitting the job itself", response = String.class, tags={ "job", "engine", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job status", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getJobEngine( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJobEngine(jobId,securityContext); - } - @GET - @Path("/errors") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get last job errors", notes = "get the errors", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job errors", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response getJobErrors( - @ApiParam(value = "max number of ites to retrieve", defaultValue="20") @DefaultValue("20") @QueryParam("count") Integer count -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJobErrors(count,securityContext); - } - @GET - @Path("/metrics") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get job metrics", notes = "get the metrics of corresponding Job", response = Metrics.class, responseContainer = "List", tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job metrics", response = Metrics.class, responseContainer = "List"), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Metrics.class, responseContainer = "List") }) - public Response getJobMetrics( - @ApiParam(value = "max number of ites to retrieve", defaultValue="20") @DefaultValue("20") @QueryParam("count") Integer count -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJobMetrics(count,securityContext); - } - @GET - @Path("/{jobId}/status") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get job status", notes = "get the status of corresponding Job", response = String.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job status", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getJobStatus( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJobStatus(jobId,securityContext); - } - @GET - @Path("/{jobId}/version") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get job version", notes = "get the version of corresponding Job", response = String.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job version", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getJobVersion( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getJobVersion(jobId,securityContext); - } - @POST - @Path("/{jobId}/pause") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "pause job", notes = "pause the corresponding Job", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job successfuly paused", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response pauseJob( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.pauseJob(jobId,securityContext); - } - @POST - @Path("/{jobId}/restart") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "start job", notes = "start the corresponding Job definition", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job successfuly started", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response reStartJob( - @ApiParam(value = "id of the job to restart",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.reStartJob(jobId,securityContext); - } - @POST - @Path("/{jobId}/shutdown") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "shutdown job", notes = "shutdown the running Job", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job successfuly started", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response shutdownJob( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.shutdownJob(jobId,securityContext); - } - @POST - @Path("/{jobId}/start") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "start job", notes = "start the corresponding Job definition", response = Job.class, tags={ "job", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job successfuly started", response = Job.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Job.class) }) - public Response startJob( - @ApiParam(value = "id of the job to return",required=true) @PathParam("jobId") String jobId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.startJob(jobId,securityContext); - } - @PUT - @Path("/{jobId}") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "update job", notes = "update an existing job configuration if valid", response = Job.class, tags={ "job" }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Job successfuly created", response = Job.class) }) - public Response updateJob( - @ApiParam(value = "Job to add to the store",required=true) @PathParam("jobId") String jobId -, - @ApiParam(value = "Job to add to the store" ,required=true) Job job -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.updateJob(jobId,job,securityContext); - } - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApiService.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApiService.java deleted file mode 100644 index 19ddbead9..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/JobsApiService.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.agent.rest.model.*; - - - -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Job; -import com.hurence.logisland.agent.rest.model.Metrics; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - import javax.validation.constraints.*; -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public abstract class JobsApiService { - - protected final KafkaRegistry kafkaRegistry; - - public JobsApiService(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - public abstract Response addJob(Job job,SecurityContext securityContext) - throws NotFoundException; - public abstract Response addJobWithId(Job body,String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response deleteJob(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getAllJobs(SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJob(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJobAlerts( Integer count,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJobEngine(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJobErrors( Integer count,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJobMetrics( Integer count,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJobStatus(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getJobVersion(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response pauseJob(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response reStartJob(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response shutdownJob(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response startJob(String jobId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response updateJob(String jobId,Job job,SecurityContext securityContext) - throws NotFoundException; - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApi.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApi.java deleted file mode 100644 index 7a78462c6..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApi.java +++ /dev/null @@ -1,53 +0,0 @@ -// hola -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.agent.rest.api.MetricsApiService; -import com.hurence.logisland.agent.rest.api.factories.MetricsApiServiceFactory; - -import io.swagger.annotations.ApiParam; - - import javax.validation.constraints.*; - -import com.hurence.logisland.agent.rest.model.Error; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/metrics") -@Consumes({ "application/json" }) -@Produces({ "application/json" }) -@io.swagger.annotations.Api(description = "the metrics API") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class MetricsApi { - - private final MetricsApiService delegate; - - public MetricsApi(KafkaRegistry kafkaRegistry) { - this.delegate = MetricsApiServiceFactory.getMetricsApi(kafkaRegistry); - } - - @GET - - @Consumes({ "application/json" }) - @Produces({ "text/plain" }) - @io.swagger.annotations.ApiOperation(value = "retrieve all job metrics in Prometheus format", notes = "get Prometheus metrics. have a look to https://prometheus.io/docs/instrumenting/exposition_formats/", response = String.class, tags={ "metrics" }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "metrics", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getMetrics( - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getMetrics(securityContext); - } - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApiService.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApiService.java deleted file mode 100644 index 81453e2ca..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/MetricsApiService.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.agent.rest.model.*; - - - -import com.hurence.logisland.agent.rest.model.Error; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - import javax.validation.constraints.*; -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public abstract class MetricsApiService { - - protected final KafkaRegistry kafkaRegistry; - - public MetricsApiService(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - public abstract Response getMetrics(SecurityContext securityContext) - throws NotFoundException; - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/NotFoundException.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/NotFoundException.java deleted file mode 100644 index af86e2275..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/NotFoundException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class NotFoundException extends ApiException { - private int code; - public NotFoundException (int code, String msg) { - super(code, msg); - this.code = code; - } -} diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApi.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApi.java deleted file mode 100644 index b0c42c1f8..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApi.java +++ /dev/null @@ -1,53 +0,0 @@ -// hola -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.agent.rest.api.ProcessorsApiService; -import com.hurence.logisland.agent.rest.api.factories.ProcessorsApiServiceFactory; - -import io.swagger.annotations.ApiParam; - - import javax.validation.constraints.*; - -import com.hurence.logisland.agent.rest.model.Error; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/processors") -@Consumes({ "application/json" }) -@Produces({ "application/json" }) -@io.swagger.annotations.Api(description = "the processors API") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ProcessorsApi { - - private final ProcessorsApiService delegate; - - public ProcessorsApi(KafkaRegistry kafkaRegistry) { - this.delegate = ProcessorsApiServiceFactory.getProcessorsApi(kafkaRegistry); - } - - @GET - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get all processors", notes = "get all processors", response = String.class, tags={ "config" }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "processors", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getProcessors( - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getProcessors(securityContext); - } - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApiService.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApiService.java deleted file mode 100644 index 306c2f534..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/ProcessorsApiService.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.agent.rest.model.*; - - - -import com.hurence.logisland.agent.rest.model.Error; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - import javax.validation.constraints.*; -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public abstract class ProcessorsApiService { - - protected final KafkaRegistry kafkaRegistry; - - public ProcessorsApiService(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - public abstract Response getProcessors(SecurityContext securityContext) - throws NotFoundException; - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/RFC3339DateFormat.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/RFC3339DateFormat.java deleted file mode 100644 index e045d1034..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/RFC3339DateFormat.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.fasterxml.jackson.databind.util.ISO8601DateFormat; -import com.fasterxml.jackson.databind.util.ISO8601Utils; - -import java.text.FieldPosition; -import java.util.Date; - -public class RFC3339DateFormat extends ISO8601DateFormat { - - // Same as ISO8601DateFormat but serializing milliseconds. - @Override - public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { - String value = ISO8601Utils.format(date, true); - toAppendTo.append(value); - return toAppendTo; - } - -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/StringUtil.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/StringUtil.java deleted file mode 100644 index 54e4cf13f..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/StringUtil.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class StringUtil { - /** - * Check if the given array contains the given value (with case-insensitive comparison). - * - * @param array The array - * @param value The value to search - * @return true if the array contains the value - */ - public static boolean containsIgnoreCase(String[] array, String value) { - for (String str : array) { - if (value == null && str == null) return true; - if (value != null && value.equalsIgnoreCase(str)) return true; - } - return false; - } - - /** - * Join an array of strings with the given separator. - *

- * Note: This might be replaced by utility method from commons-lang or guava someday - * if one of those libraries is added as dependency. - *

- * - * @param array The array of strings - * @param separator The separator - * @return the resulting string - */ - public static String join(String[] array, String separator) { - int len = array.length; - if (len == 0) return ""; - - StringBuilder out = new StringBuilder(); - out.append(array[0]); - for (int i = 1; i < len; i++) { - out.append(separator).append(array[i]); - } - return out.toString(); - } -} diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApi.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApi.java deleted file mode 100644 index 55a11594b..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApi.java +++ /dev/null @@ -1,216 +0,0 @@ -// hola -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.agent.rest.api.TopicsApiService; -import com.hurence.logisland.agent.rest.api.factories.TopicsApiServiceFactory; - -import io.swagger.annotations.ApiParam; - - import javax.validation.constraints.*; - -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Topic; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/topics") -@Consumes({ "application/json" }) -@Produces({ "application/json" }) -@io.swagger.annotations.Api(description = "the topics API") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class TopicsApi { - - private final TopicsApiService delegate; - - public TopicsApi(KafkaRegistry kafkaRegistry) { - this.delegate = TopicsApiServiceFactory.getTopicsApi(kafkaRegistry); - } - - @POST - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "create new topic", notes = "", response = void.class, tags={ "topic", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Status 200", response = void.class) }) - public Response addNewTopic( - @ApiParam(value = "" ,required=true) Topic body -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.addNewTopic(body,securityContext); - } - @POST - @Path("/{topicId}/keySchema/checkCompatibility") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "check topic key schema compatibility", notes = "", response = String.class, tags={ "schema", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "compatibility level", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response checkTopicKeySchemaCompatibility( - @ApiParam(value = "Avro schema as a json string" ,required=true) String body -, - @ApiParam(value = "id of the job to return",required=true) @PathParam("topicId") String topicId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.checkTopicKeySchemaCompatibility(body,topicId,securityContext); - } - @POST - @Path("/{topicId}/valueSchema/checkCompatibility") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "check topic value schema compatibility", notes = "", response = String.class, tags={ "schema", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "compatibility level", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response checkTopicValueSchemaCompatibility( - @ApiParam(value = "id of the job to return",required=true) @PathParam("topicId") String topicId -, - @ApiParam(value = "Avro schema as a json string" ,required=true) String body -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.checkTopicValueSchemaCompatibility(topicId,body,securityContext); - } - @DELETE - @Path("/{topicId}") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "delete topic", notes = "remove a topic config and remove all content from Kafka", response = String.class, tags={ "topic", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "topic successfully deleted", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response deleteTopic( - @ApiParam(value = "",required=true) @PathParam("topicId") String topicId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.deleteTopic(topicId,securityContext); - } - @GET - - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get all topics", notes = "", response = Topic.class, responseContainer = "List", tags={ "topic", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Status 200", response = Topic.class, responseContainer = "List") }) - public Response getAllTopics( - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getAllTopics(securityContext); - } - @GET - @Path("/{topicId}") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get topic", notes = "", response = Topic.class, tags={ "topic", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Status 200", response = Topic.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Topic.class) }) - public Response getTopic( - @ApiParam(value = "",required=true) @PathParam("topicId") String topicId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getTopic(topicId,securityContext); - } - @GET - @Path("/{topicId}/keySchema") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get topic key schema", notes = "", response = String.class, tags={ "schema", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Avro schema", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getTopicKeySchema( - @ApiParam(value = "",required=true) @PathParam("topicId") String topicId -, - @ApiParam(value = "version of the schema (\"latest\" if not provided)", defaultValue="latest") @DefaultValue("latest") @QueryParam("version") String version -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getTopicKeySchema(topicId,version,securityContext); - } - @GET - @Path("/{topicId}/valueSchema") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "get topic value schema", notes = "", response = String.class, tags={ "schema", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job definition", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response getTopicValueSchema( - @ApiParam(value = "id of the job to return",required=true) @PathParam("topicId") String topicId -, - @ApiParam(value = "version of the schema (\"latest\" if not provided)", defaultValue="latest") @DefaultValue("latest") @QueryParam("version") String version -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.getTopicValueSchema(topicId,version,securityContext); - } - @PUT - @Path("/{topicId}") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "update topic", notes = "", response = Topic.class, tags={ "topic", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "job successfuly started", response = Topic.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = Topic.class) }) - public Response updateTopic( - @ApiParam(value = "" ,required=true) Topic body -, - @ApiParam(value = "",required=true) @PathParam("topicId") String topicId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.updateTopic(body,topicId,securityContext); - } - @PUT - @Path("/{topicId}/keySchema") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "update topic key schema", notes = "", response = String.class, tags={ "schema", }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Avro schema", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response updateTopicKeySchema( - @ApiParam(value = "schema to add to the store" ,required=true) String body -, - @ApiParam(value = "id of the job to return",required=true) @PathParam("topicId") String topicId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.updateTopicKeySchema(body,topicId,securityContext); - } - @PUT - @Path("/{topicId}/valueSchema") - @Consumes({ "application/json" }) - @Produces({ "application/json" }) - @io.swagger.annotations.ApiOperation(value = "update topic value schema", notes = "", response = String.class, tags={ "schema" }) - @io.swagger.annotations.ApiResponses(value = { - @io.swagger.annotations.ApiResponse(code = 200, message = "Avro schema", response = String.class), - @io.swagger.annotations.ApiResponse(code = 200, message = "unexpected error", response = String.class) }) - public Response updateTopicValueSchema( - @ApiParam(value = "Avro schema as a json string" ,required=true) String body -, - @ApiParam(value = "id of the job to return",required=true) @PathParam("topicId") String topicId -, - @Context SecurityContext securityContext) - throws NotFoundException { - return delegate.updateTopicValueSchema(body,topicId,securityContext); - } - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApiService.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApiService.java deleted file mode 100644 index aeacc6f58..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/api/TopicsApiService.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.hurence.logisland.agent.rest.api; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.agent.rest.model.*; - - - -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Topic; - -import java.util.List; -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - import javax.validation.constraints.*; -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public abstract class TopicsApiService { - - protected final KafkaRegistry kafkaRegistry; - - public TopicsApiService(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - public abstract Response addNewTopic(Topic body,SecurityContext securityContext) - throws NotFoundException; - public abstract Response checkTopicKeySchemaCompatibility(String body,String topicId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response checkTopicValueSchemaCompatibility(String topicId,String body,SecurityContext securityContext) - throws NotFoundException; - public abstract Response deleteTopic(String topicId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getAllTopics(SecurityContext securityContext) - throws NotFoundException; - public abstract Response getTopic(String topicId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getTopicKeySchema(String topicId, String version,SecurityContext securityContext) - throws NotFoundException; - public abstract Response getTopicValueSchema(String topicId, String version,SecurityContext securityContext) - throws NotFoundException; - public abstract Response updateTopic(Topic body,String topicId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response updateTopicKeySchema(String body,String topicId,SecurityContext securityContext) - throws NotFoundException; - public abstract Response updateTopicValueSchema(String body,String topicId,SecurityContext securityContext) - throws NotFoundException; - } diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Engine.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Engine.java deleted file mode 100644 index b6af44b3a..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Engine.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.hurence.logisland.agent.rest.model.Property; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.ArrayList; -import java.util.List; -import javax.validation.constraints.*; - -/** - * Engine - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Engine { - @JsonProperty("name") - private String name = null; - - @JsonProperty("component") - private String component = null; - - @JsonProperty("config") - private List config = new ArrayList(); - - public Engine name(String name) { - this.name = name; - return this; - } - - /** - * Get name - * @return name - **/ - @JsonProperty("name") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Engine component(String component) { - this.component = component; - return this; - } - - /** - * Get component - * @return component - **/ - @JsonProperty("component") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getComponent() { - return component; - } - - public void setComponent(String component) { - this.component = component; - } - - public Engine config(List config) { - this.config = config; - return this; - } - - public Engine addConfigItem(Property configItem) { - this.config.add(configItem); - return this; - } - - /** - * Get config - * @return config - **/ - @JsonProperty("config") - @ApiModelProperty(required = true, value = "") - @NotNull - public List getConfig() { - return config; - } - - public void setConfig(List config) { - this.config = config; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Engine engine = (Engine) o; - return Objects.equals(this.name, engine.name) && - Objects.equals(this.component, engine.component) && - Objects.equals(this.config, engine.config); - } - - @Override - public int hashCode() { - return Objects.hash(name, component, config); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Engine {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" component: ").append(toIndentedString(component)).append("\n"); - sb.append(" config: ").append(toIndentedString(config)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Error.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Error.java deleted file mode 100644 index 11614720d..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Error.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import javax.validation.constraints.*; - -/** - * Error - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Error { - @JsonProperty("code") - private Integer code = null; - - @JsonProperty("message") - private String message = null; - - public Error code(Integer code) { - this.code = code; - return this; - } - - /** - * Get code - * @return code - **/ - @JsonProperty("code") - @ApiModelProperty(required = true, value = "") - @NotNull - public Integer getCode() { - return code; - } - - public void setCode(Integer code) { - this.code = code; - } - - public Error message(String message) { - this.message = message; - return this; - } - - /** - * Get message - * @return message - **/ - @JsonProperty("message") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Error error = (Error) o; - return Objects.equals(this.code, error.code) && - Objects.equals(this.message, error.message); - } - - @Override - public int hashCode() { - return Objects.hash(code, message); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Error {\n"); - - sb.append(" code: ").append(toIndentedString(code)).append("\n"); - sb.append(" message: ").append(toIndentedString(message)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Field.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Field.java deleted file mode 100644 index ba4b857a4..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Field.java +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; - - - - -/** - * Field - */ -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-02-28T16:28:07.083+01:00") -public class Field { - private String name = null; - - private Boolean encrypted = false; - - private Boolean indexed = true; - - private Boolean persistent = true; - - private Boolean optional = true; - - /** - * the type of the field - */ - public enum TypeEnum { - STRING("string"), - - LONG("long"), - - ARRAY("array"), - - FLOAT("float"), - - DOUBLE("double"), - - BYTES("bytes"), - - RECORD("record"), - - MAP("map"), - - ENUM("enum"), - - BOOLEAN("boolean"); - - private String value; - - TypeEnum(String value) { - this.value = value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - } - - private TypeEnum type = TypeEnum.STRING; - - public Field name(String name) { - this.name = name; - return this; - } - - /** - * a unique identifier for the topic - * @return name - **/ - @ApiModelProperty(required = true, value = "a unique identifier for the topic") - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Field encrypted(Boolean encrypted) { - this.encrypted = encrypted; - return this; - } - - /** - * is the field need to be encrypted - * @return encrypted - **/ - @ApiModelProperty(value = "is the field need to be encrypted") - public Boolean getEncrypted() { - return encrypted; - } - - public void setEncrypted(Boolean encrypted) { - this.encrypted = encrypted; - } - - public Field indexed(Boolean indexed) { - this.indexed = indexed; - return this; - } - - /** - * is the field need to be indexed to search store - * @return indexed - **/ - @ApiModelProperty(value = "is the field need to be indexed to search store") - public Boolean getIndexed() { - return indexed; - } - - public void setIndexed(Boolean indexed) { - this.indexed = indexed; - } - - public Field persistent(Boolean persistent) { - this.persistent = persistent; - return this; - } - - /** - * is the field need to be persisted to data store - * @return persistent - **/ - @ApiModelProperty(value = "is the field need to be persisted to data store") - public Boolean getPersistent() { - return persistent; - } - - public void setPersistent(Boolean persistent) { - this.persistent = persistent; - } - - public Field optional(Boolean optional) { - this.optional = optional; - return this; - } - - /** - * is the field mandatory - * @return optional - **/ - @ApiModelProperty(value = "is the field mandatory") - public Boolean getOptional() { - return optional; - } - - public void setOptional(Boolean optional) { - this.optional = optional; - } - - public Field type(TypeEnum type) { - this.type = type; - return this; - } - - /** - * the type of the field - * @return type - **/ - @ApiModelProperty(required = true, value = "the type of the field") - public TypeEnum getType() { - return type; - } - - public void setType(TypeEnum type) { - this.type = type; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Field field = (Field) o; - return Objects.equals(this.name, field.name) && - Objects.equals(this.encrypted, field.encrypted) && - Objects.equals(this.indexed, field.indexed) && - Objects.equals(this.persistent, field.persistent) && - Objects.equals(this.optional, field.optional) && - Objects.equals(this.type, field.type); - } - - @Override - public int hashCode() { - return Objects.hash(name, encrypted, indexed, persistent, optional, type); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Field {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" encrypted: ").append(toIndentedString(encrypted)).append("\n"); - sb.append(" indexed: ").append(toIndentedString(indexed)).append("\n"); - sb.append(" persistent: ").append(toIndentedString(persistent)).append("\n"); - sb.append(" optional: ").append(toIndentedString(optional)).append("\n"); - sb.append(" type: ").append(toIndentedString(type)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/FieldType.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/FieldType.java deleted file mode 100644 index e26ddac2a..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/FieldType.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import javax.validation.constraints.*; - -/** - * FieldType - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class FieldType { - @JsonProperty("name") - private String name = null; - - @JsonProperty("encrypted") - private Boolean encrypted = false; - - @JsonProperty("indexed") - private Boolean indexed = true; - - @JsonProperty("persistent") - private Boolean persistent = true; - - @JsonProperty("optional") - private Boolean optional = true; - - /** - * the type of the field - */ - public enum TypeEnum { - STRING("string"), - - INT("int"), - - LONG("long"), - - ARRAY("array"), - - FLOAT("float"), - - DOUBLE("double"), - - BYTES("bytes"), - - RECORD("record"), - - MAP("map"), - - ENUM("enum"), - - BOOLEAN("boolean"); - - private String value; - - TypeEnum(String value) { - this.value = value; - } - - @Override - @JsonValue - public String toString() { - return String.valueOf(value); - } - - @JsonCreator - public static TypeEnum fromValue(String text) { - for (TypeEnum b : TypeEnum.values()) { - if (String.valueOf(b.value).equals(text)) { - return b; - } - } - return null; - } - } - - @JsonProperty("type") - private TypeEnum type = TypeEnum.STRING; - - public FieldType name(String name) { - this.name = name; - return this; - } - - /** - * a unique identifier for the topic - * @return name - **/ - @JsonProperty("name") - @ApiModelProperty(required = true, value = "a unique identifier for the topic") - @NotNull - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public FieldType encrypted(Boolean encrypted) { - this.encrypted = encrypted; - return this; - } - - /** - * is the field need to be encrypted - * @return encrypted - **/ - @JsonProperty("encrypted") - @ApiModelProperty(value = "is the field need to be encrypted") - public Boolean getEncrypted() { - return encrypted; - } - - public void setEncrypted(Boolean encrypted) { - this.encrypted = encrypted; - } - - public FieldType indexed(Boolean indexed) { - this.indexed = indexed; - return this; - } - - /** - * is the field need to be indexed to search store - * @return indexed - **/ - @JsonProperty("indexed") - @ApiModelProperty(value = "is the field need to be indexed to search store") - public Boolean getIndexed() { - return indexed; - } - - public void setIndexed(Boolean indexed) { - this.indexed = indexed; - } - - public FieldType persistent(Boolean persistent) { - this.persistent = persistent; - return this; - } - - /** - * is the field need to be persisted to data store - * @return persistent - **/ - @JsonProperty("persistent") - @ApiModelProperty(value = "is the field need to be persisted to data store") - public Boolean getPersistent() { - return persistent; - } - - public void setPersistent(Boolean persistent) { - this.persistent = persistent; - } - - public FieldType optional(Boolean optional) { - this.optional = optional; - return this; - } - - /** - * is the field mandatory - * @return optional - **/ - @JsonProperty("optional") - @ApiModelProperty(value = "is the field mandatory") - public Boolean getOptional() { - return optional; - } - - public void setOptional(Boolean optional) { - this.optional = optional; - } - - public FieldType type(TypeEnum type) { - this.type = type; - return this; - } - - /** - * the type of the field - * @return type - **/ - @JsonProperty("type") - @ApiModelProperty(required = true, value = "the type of the field") - @NotNull - public TypeEnum getType() { - return type; - } - - public void setType(TypeEnum type) { - this.type = type; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FieldType fieldType = (FieldType) o; - return Objects.equals(this.name, fieldType.name) && - Objects.equals(this.encrypted, fieldType.encrypted) && - Objects.equals(this.indexed, fieldType.indexed) && - Objects.equals(this.persistent, fieldType.persistent) && - Objects.equals(this.optional, fieldType.optional) && - Objects.equals(this.type, fieldType.type); - } - - @Override - public int hashCode() { - return Objects.hash(name, encrypted, indexed, persistent, optional, type); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class FieldType {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" encrypted: ").append(toIndentedString(encrypted)).append("\n"); - sb.append(" indexed: ").append(toIndentedString(indexed)).append("\n"); - sb.append(" persistent: ").append(toIndentedString(persistent)).append("\n"); - sb.append(" optional: ").append(toIndentedString(optional)).append("\n"); - sb.append(" type: ").append(toIndentedString(type)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Job.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Job.java deleted file mode 100644 index e9242ebf7..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Job.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.hurence.logisland.agent.rest.model.Engine; -import com.hurence.logisland.agent.rest.model.JobSummary; -import com.hurence.logisland.agent.rest.model.Stream; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.ArrayList; -import java.util.List; -import javax.validation.constraints.*; - -/** - * Job - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Job { - @JsonProperty("id") - private Long id = null; - - @JsonProperty("version") - private Integer version = null; - - @JsonProperty("name") - private String name = null; - - @JsonProperty("summary") - private JobSummary summary = null; - - @JsonProperty("engine") - private Engine engine = null; - - @JsonProperty("streams") - private List streams = new ArrayList(); - - public Job id(Long id) { - this.id = id; - return this; - } - - /** - * a unique identifier for the job - * @return id - **/ - @JsonProperty("id") - @ApiModelProperty(value = "a unique identifier for the job") - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Job version(Integer version) { - this.version = version; - return this; - } - - /** - * the version of the job configuration - * @return version - **/ - @JsonProperty("version") - @ApiModelProperty(required = true, value = "the version of the job configuration") - @NotNull - public Integer getVersion() { - return version; - } - - public void setVersion(Integer version) { - this.version = version; - } - - public Job name(String name) { - this.name = name; - return this; - } - - /** - * the job name - * @return name - **/ - @JsonProperty("name") - @ApiModelProperty(required = true, value = "the job name") - @NotNull - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Job summary(JobSummary summary) { - this.summary = summary; - return this; - } - - /** - * Get summary - * @return summary - **/ - @JsonProperty("summary") - @ApiModelProperty(value = "") - public JobSummary getSummary() { - return summary; - } - - public void setSummary(JobSummary summary) { - this.summary = summary; - } - - public Job engine(Engine engine) { - this.engine = engine; - return this; - } - - /** - * Get engine - * @return engine - **/ - @JsonProperty("engine") - @ApiModelProperty(required = true, value = "") - @NotNull - public Engine getEngine() { - return engine; - } - - public void setEngine(Engine engine) { - this.engine = engine; - } - - public Job streams(List streams) { - this.streams = streams; - return this; - } - - public Job addStreamsItem(Stream streamsItem) { - this.streams.add(streamsItem); - return this; - } - - /** - * Get streams - * @return streams - **/ - @JsonProperty("streams") - @ApiModelProperty(required = true, value = "") - @NotNull - public List getStreams() { - return streams; - } - - public void setStreams(List streams) { - this.streams = streams; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Job job = (Job) o; - return Objects.equals(this.id, job.id) && - Objects.equals(this.version, job.version) && - Objects.equals(this.name, job.name) && - Objects.equals(this.summary, job.summary) && - Objects.equals(this.engine, job.engine) && - Objects.equals(this.streams, job.streams); - } - - @Override - public int hashCode() { - return Objects.hash(id, version, name, summary, engine, streams); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Job {\n"); - - sb.append(" id: ").append(toIndentedString(id)).append("\n"); - sb.append(" version: ").append(toIndentedString(version)).append("\n"); - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" summary: ").append(toIndentedString(summary)).append("\n"); - sb.append(" engine: ").append(toIndentedString(engine)).append("\n"); - sb.append(" streams: ").append(toIndentedString(streams)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/JobSummary.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/JobSummary.java deleted file mode 100644 index 53f7bba05..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/JobSummary.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.Date; -import javax.validation.constraints.*; - -/** - * JobSummary - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class JobSummary { - @JsonProperty("usedCores") - private Integer usedCores = null; - - @JsonProperty("usedMemory") - private Integer usedMemory = null; - - /** - * the job status - */ - public enum StatusEnum { - STOPPED("stopped"), - - RUNNING("running"), - - FAILED("failed"), - - PAUSED("paused"); - - private String value; - - StatusEnum(String value) { - this.value = value; - } - - @Override - @JsonValue - public String toString() { - return String.valueOf(value); - } - - @JsonCreator - public static StatusEnum fromValue(String text) { - for (StatusEnum b : StatusEnum.values()) { - if (String.valueOf(b.value).equals(text)) { - return b; - } - } - return null; - } - } - - @JsonProperty("status") - private StatusEnum status = StatusEnum.STOPPED; - - @JsonProperty("dateModified") - private Date dateModified = null; - - @JsonProperty("documentation") - private String documentation = null; - - public JobSummary usedCores(Integer usedCores) { - this.usedCores = usedCores; - return this; - } - - /** - * the number of used cores - * @return usedCores - **/ - @JsonProperty("usedCores") - @ApiModelProperty(value = "the number of used cores") - public Integer getUsedCores() { - return usedCores; - } - - public void setUsedCores(Integer usedCores) { - this.usedCores = usedCores; - } - - public JobSummary usedMemory(Integer usedMemory) { - this.usedMemory = usedMemory; - return this; - } - - /** - * the total memory allocated for this job - * @return usedMemory - **/ - @JsonProperty("usedMemory") - @ApiModelProperty(value = "the total memory allocated for this job") - public Integer getUsedMemory() { - return usedMemory; - } - - public void setUsedMemory(Integer usedMemory) { - this.usedMemory = usedMemory; - } - - public JobSummary status(StatusEnum status) { - this.status = status; - return this; - } - - /** - * the job status - * @return status - **/ - @JsonProperty("status") - @ApiModelProperty(value = "the job status") - public StatusEnum getStatus() { - return status; - } - - public void setStatus(StatusEnum status) { - this.status = status; - } - - public JobSummary dateModified(Date dateModified) { - this.dateModified = dateModified; - return this; - } - - /** - * latest date of modification - * @return dateModified - **/ - @JsonProperty("dateModified") - @ApiModelProperty(value = "latest date of modification") - public Date getDateModified() { - return dateModified; - } - - public void setDateModified(Date dateModified) { - this.dateModified = dateModified; - } - - public JobSummary documentation(String documentation) { - this.documentation = documentation; - return this; - } - - /** - * write here what the job is doing - * @return documentation - **/ - @JsonProperty("documentation") - @ApiModelProperty(value = "write here what the job is doing") - public String getDocumentation() { - return documentation; - } - - public void setDocumentation(String documentation) { - this.documentation = documentation; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - JobSummary jobSummary = (JobSummary) o; - return Objects.equals(this.usedCores, jobSummary.usedCores) && - Objects.equals(this.usedMemory, jobSummary.usedMemory) && - Objects.equals(this.status, jobSummary.status) && - Objects.equals(this.dateModified, jobSummary.dateModified) && - Objects.equals(this.documentation, jobSummary.documentation); - } - - @Override - public int hashCode() { - return Objects.hash(usedCores, usedMemory, status, dateModified, documentation); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class JobSummary {\n"); - - sb.append(" usedCores: ").append(toIndentedString(usedCores)).append("\n"); - sb.append(" usedMemory: ").append(toIndentedString(usedMemory)).append("\n"); - sb.append(" status: ").append(toIndentedString(status)).append("\n"); - sb.append(" dateModified: ").append(toIndentedString(dateModified)).append("\n"); - sb.append(" documentation: ").append(toIndentedString(documentation)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Metrics.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Metrics.java deleted file mode 100644 index 2309b4409..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Metrics.java +++ /dev/null @@ -1,546 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import javax.validation.constraints.*; - -/** - * Metrics - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Metrics { - @JsonProperty("spark_app_name") - private String sparkAppName = null; - - @JsonProperty("spark_partition_id") - private Integer sparkPartitionId = null; - - @JsonProperty("component_name") - private String componentName = null; - - @JsonProperty("input_topics") - private String inputTopics = null; - - @JsonProperty("output_topics") - private String outputTopics = null; - - @JsonProperty("topic_offset_from") - private Long topicOffsetFrom = null; - - @JsonProperty("topic_offset_until") - private Long topicOffsetUntil = null; - - @JsonProperty("num_incoming_messages") - private Integer numIncomingMessages = null; - - @JsonProperty("num_incoming_records") - private Integer numIncomingRecords = null; - - @JsonProperty("num_outgoing_records") - private Integer numOutgoingRecords = null; - - @JsonProperty("num_errors_records") - private Long numErrorsRecords = null; - - @JsonProperty("error_percentage") - private Float errorPercentage = null; - - @JsonProperty("average_bytes_per_field") - private Integer averageBytesPerField = null; - - @JsonProperty("average_bytes_per_second") - private Integer averageBytesPerSecond = null; - - @JsonProperty("average_num_records_per_second") - private Integer averageNumRecordsPerSecond = null; - - @JsonProperty("average_fields_per_record") - private Integer averageFieldsPerRecord = null; - - @JsonProperty("average_bytes_per_record") - private Integer averageBytesPerRecord = null; - - @JsonProperty("total_bytes") - private Integer totalBytes = null; - - @JsonProperty("total_fields") - private Integer totalFields = null; - - @JsonProperty("total_processing_time_in_ms") - private Long totalProcessingTimeInMs = null; - - public Metrics sparkAppName(String sparkAppName) { - this.sparkAppName = sparkAppName; - return this; - } - - /** - * Get sparkAppName - * @return sparkAppName - **/ - @JsonProperty("spark_app_name") - @ApiModelProperty(value = "") - public String getSparkAppName() { - return sparkAppName; - } - - public void setSparkAppName(String sparkAppName) { - this.sparkAppName = sparkAppName; - } - - public Metrics sparkPartitionId(Integer sparkPartitionId) { - this.sparkPartitionId = sparkPartitionId; - return this; - } - - /** - * Get sparkPartitionId - * @return sparkPartitionId - **/ - @JsonProperty("spark_partition_id") - @ApiModelProperty(value = "") - public Integer getSparkPartitionId() { - return sparkPartitionId; - } - - public void setSparkPartitionId(Integer sparkPartitionId) { - this.sparkPartitionId = sparkPartitionId; - } - - public Metrics componentName(String componentName) { - this.componentName = componentName; - return this; - } - - /** - * Get componentName - * @return componentName - **/ - @JsonProperty("component_name") - @ApiModelProperty(value = "") - public String getComponentName() { - return componentName; - } - - public void setComponentName(String componentName) { - this.componentName = componentName; - } - - public Metrics inputTopics(String inputTopics) { - this.inputTopics = inputTopics; - return this; - } - - /** - * Get inputTopics - * @return inputTopics - **/ - @JsonProperty("input_topics") - @ApiModelProperty(value = "") - public String getInputTopics() { - return inputTopics; - } - - public void setInputTopics(String inputTopics) { - this.inputTopics = inputTopics; - } - - public Metrics outputTopics(String outputTopics) { - this.outputTopics = outputTopics; - return this; - } - - /** - * Get outputTopics - * @return outputTopics - **/ - @JsonProperty("output_topics") - @ApiModelProperty(value = "") - public String getOutputTopics() { - return outputTopics; - } - - public void setOutputTopics(String outputTopics) { - this.outputTopics = outputTopics; - } - - public Metrics topicOffsetFrom(Long topicOffsetFrom) { - this.topicOffsetFrom = topicOffsetFrom; - return this; - } - - /** - * Get topicOffsetFrom - * @return topicOffsetFrom - **/ - @JsonProperty("topic_offset_from") - @ApiModelProperty(value = "") - public Long getTopicOffsetFrom() { - return topicOffsetFrom; - } - - public void setTopicOffsetFrom(Long topicOffsetFrom) { - this.topicOffsetFrom = topicOffsetFrom; - } - - public Metrics topicOffsetUntil(Long topicOffsetUntil) { - this.topicOffsetUntil = topicOffsetUntil; - return this; - } - - /** - * Get topicOffsetUntil - * @return topicOffsetUntil - **/ - @JsonProperty("topic_offset_until") - @ApiModelProperty(value = "") - public Long getTopicOffsetUntil() { - return topicOffsetUntil; - } - - public void setTopicOffsetUntil(Long topicOffsetUntil) { - this.topicOffsetUntil = topicOffsetUntil; - } - - public Metrics numIncomingMessages(Integer numIncomingMessages) { - this.numIncomingMessages = numIncomingMessages; - return this; - } - - /** - * Get numIncomingMessages - * @return numIncomingMessages - **/ - @JsonProperty("num_incoming_messages") - @ApiModelProperty(value = "") - public Integer getNumIncomingMessages() { - return numIncomingMessages; - } - - public void setNumIncomingMessages(Integer numIncomingMessages) { - this.numIncomingMessages = numIncomingMessages; - } - - public Metrics numIncomingRecords(Integer numIncomingRecords) { - this.numIncomingRecords = numIncomingRecords; - return this; - } - - /** - * Get numIncomingRecords - * @return numIncomingRecords - **/ - @JsonProperty("num_incoming_records") - @ApiModelProperty(value = "") - public Integer getNumIncomingRecords() { - return numIncomingRecords; - } - - public void setNumIncomingRecords(Integer numIncomingRecords) { - this.numIncomingRecords = numIncomingRecords; - } - - public Metrics numOutgoingRecords(Integer numOutgoingRecords) { - this.numOutgoingRecords = numOutgoingRecords; - return this; - } - - /** - * Get numOutgoingRecords - * @return numOutgoingRecords - **/ - @JsonProperty("num_outgoing_records") - @ApiModelProperty(value = "") - public Integer getNumOutgoingRecords() { - return numOutgoingRecords; - } - - public void setNumOutgoingRecords(Integer numOutgoingRecords) { - this.numOutgoingRecords = numOutgoingRecords; - } - - public Metrics numErrorsRecords(Long numErrorsRecords) { - this.numErrorsRecords = numErrorsRecords; - return this; - } - - /** - * Get numErrorsRecords - * @return numErrorsRecords - **/ - @JsonProperty("num_errors_records") - @ApiModelProperty(value = "") - public Long getNumErrorsRecords() { - return numErrorsRecords; - } - - public void setNumErrorsRecords(Long numErrorsRecords) { - this.numErrorsRecords = numErrorsRecords; - } - - public Metrics errorPercentage(Float errorPercentage) { - this.errorPercentage = errorPercentage; - return this; - } - - /** - * Get errorPercentage - * @return errorPercentage - **/ - @JsonProperty("error_percentage") - @ApiModelProperty(value = "") - public Float getErrorPercentage() { - return errorPercentage; - } - - public void setErrorPercentage(Float errorPercentage) { - this.errorPercentage = errorPercentage; - } - - public Metrics averageBytesPerField(Integer averageBytesPerField) { - this.averageBytesPerField = averageBytesPerField; - return this; - } - - /** - * Get averageBytesPerField - * @return averageBytesPerField - **/ - @JsonProperty("average_bytes_per_field") - @ApiModelProperty(value = "") - public Integer getAverageBytesPerField() { - return averageBytesPerField; - } - - public void setAverageBytesPerField(Integer averageBytesPerField) { - this.averageBytesPerField = averageBytesPerField; - } - - public Metrics averageBytesPerSecond(Integer averageBytesPerSecond) { - this.averageBytesPerSecond = averageBytesPerSecond; - return this; - } - - /** - * Get averageBytesPerSecond - * @return averageBytesPerSecond - **/ - @JsonProperty("average_bytes_per_second") - @ApiModelProperty(value = "") - public Integer getAverageBytesPerSecond() { - return averageBytesPerSecond; - } - - public void setAverageBytesPerSecond(Integer averageBytesPerSecond) { - this.averageBytesPerSecond = averageBytesPerSecond; - } - - public Metrics averageNumRecordsPerSecond(Integer averageNumRecordsPerSecond) { - this.averageNumRecordsPerSecond = averageNumRecordsPerSecond; - return this; - } - - /** - * Get averageNumRecordsPerSecond - * @return averageNumRecordsPerSecond - **/ - @JsonProperty("average_num_records_per_second") - @ApiModelProperty(value = "") - public Integer getAverageNumRecordsPerSecond() { - return averageNumRecordsPerSecond; - } - - public void setAverageNumRecordsPerSecond(Integer averageNumRecordsPerSecond) { - this.averageNumRecordsPerSecond = averageNumRecordsPerSecond; - } - - public Metrics averageFieldsPerRecord(Integer averageFieldsPerRecord) { - this.averageFieldsPerRecord = averageFieldsPerRecord; - return this; - } - - /** - * Get averageFieldsPerRecord - * @return averageFieldsPerRecord - **/ - @JsonProperty("average_fields_per_record") - @ApiModelProperty(value = "") - public Integer getAverageFieldsPerRecord() { - return averageFieldsPerRecord; - } - - public void setAverageFieldsPerRecord(Integer averageFieldsPerRecord) { - this.averageFieldsPerRecord = averageFieldsPerRecord; - } - - public Metrics averageBytesPerRecord(Integer averageBytesPerRecord) { - this.averageBytesPerRecord = averageBytesPerRecord; - return this; - } - - /** - * Get averageBytesPerRecord - * @return averageBytesPerRecord - **/ - @JsonProperty("average_bytes_per_record") - @ApiModelProperty(value = "") - public Integer getAverageBytesPerRecord() { - return averageBytesPerRecord; - } - - public void setAverageBytesPerRecord(Integer averageBytesPerRecord) { - this.averageBytesPerRecord = averageBytesPerRecord; - } - - public Metrics totalBytes(Integer totalBytes) { - this.totalBytes = totalBytes; - return this; - } - - /** - * Get totalBytes - * @return totalBytes - **/ - @JsonProperty("total_bytes") - @ApiModelProperty(value = "") - public Integer getTotalBytes() { - return totalBytes; - } - - public void setTotalBytes(Integer totalBytes) { - this.totalBytes = totalBytes; - } - - public Metrics totalFields(Integer totalFields) { - this.totalFields = totalFields; - return this; - } - - /** - * Get totalFields - * @return totalFields - **/ - @JsonProperty("total_fields") - @ApiModelProperty(value = "") - public Integer getTotalFields() { - return totalFields; - } - - public void setTotalFields(Integer totalFields) { - this.totalFields = totalFields; - } - - public Metrics totalProcessingTimeInMs(Long totalProcessingTimeInMs) { - this.totalProcessingTimeInMs = totalProcessingTimeInMs; - return this; - } - - /** - * Get totalProcessingTimeInMs - * @return totalProcessingTimeInMs - **/ - @JsonProperty("total_processing_time_in_ms") - @ApiModelProperty(value = "") - public Long getTotalProcessingTimeInMs() { - return totalProcessingTimeInMs; - } - - public void setTotalProcessingTimeInMs(Long totalProcessingTimeInMs) { - this.totalProcessingTimeInMs = totalProcessingTimeInMs; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Metrics metrics = (Metrics) o; - return Objects.equals(this.sparkAppName, metrics.sparkAppName) && - Objects.equals(this.sparkPartitionId, metrics.sparkPartitionId) && - Objects.equals(this.componentName, metrics.componentName) && - Objects.equals(this.inputTopics, metrics.inputTopics) && - Objects.equals(this.outputTopics, metrics.outputTopics) && - Objects.equals(this.topicOffsetFrom, metrics.topicOffsetFrom) && - Objects.equals(this.topicOffsetUntil, metrics.topicOffsetUntil) && - Objects.equals(this.numIncomingMessages, metrics.numIncomingMessages) && - Objects.equals(this.numIncomingRecords, metrics.numIncomingRecords) && - Objects.equals(this.numOutgoingRecords, metrics.numOutgoingRecords) && - Objects.equals(this.numErrorsRecords, metrics.numErrorsRecords) && - Objects.equals(this.errorPercentage, metrics.errorPercentage) && - Objects.equals(this.averageBytesPerField, metrics.averageBytesPerField) && - Objects.equals(this.averageBytesPerSecond, metrics.averageBytesPerSecond) && - Objects.equals(this.averageNumRecordsPerSecond, metrics.averageNumRecordsPerSecond) && - Objects.equals(this.averageFieldsPerRecord, metrics.averageFieldsPerRecord) && - Objects.equals(this.averageBytesPerRecord, metrics.averageBytesPerRecord) && - Objects.equals(this.totalBytes, metrics.totalBytes) && - Objects.equals(this.totalFields, metrics.totalFields) && - Objects.equals(this.totalProcessingTimeInMs, metrics.totalProcessingTimeInMs); - } - - @Override - public int hashCode() { - return Objects.hash(sparkAppName, sparkPartitionId, componentName, inputTopics, outputTopics, topicOffsetFrom, topicOffsetUntil, numIncomingMessages, numIncomingRecords, numOutgoingRecords, numErrorsRecords, errorPercentage, averageBytesPerField, averageBytesPerSecond, averageNumRecordsPerSecond, averageFieldsPerRecord, averageBytesPerRecord, totalBytes, totalFields, totalProcessingTimeInMs); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Metrics {\n"); - - sb.append(" sparkAppName: ").append(toIndentedString(sparkAppName)).append("\n"); - sb.append(" sparkPartitionId: ").append(toIndentedString(sparkPartitionId)).append("\n"); - sb.append(" componentName: ").append(toIndentedString(componentName)).append("\n"); - sb.append(" inputTopics: ").append(toIndentedString(inputTopics)).append("\n"); - sb.append(" outputTopics: ").append(toIndentedString(outputTopics)).append("\n"); - sb.append(" topicOffsetFrom: ").append(toIndentedString(topicOffsetFrom)).append("\n"); - sb.append(" topicOffsetUntil: ").append(toIndentedString(topicOffsetUntil)).append("\n"); - sb.append(" numIncomingMessages: ").append(toIndentedString(numIncomingMessages)).append("\n"); - sb.append(" numIncomingRecords: ").append(toIndentedString(numIncomingRecords)).append("\n"); - sb.append(" numOutgoingRecords: ").append(toIndentedString(numOutgoingRecords)).append("\n"); - sb.append(" numErrorsRecords: ").append(toIndentedString(numErrorsRecords)).append("\n"); - sb.append(" errorPercentage: ").append(toIndentedString(errorPercentage)).append("\n"); - sb.append(" averageBytesPerField: ").append(toIndentedString(averageBytesPerField)).append("\n"); - sb.append(" averageBytesPerSecond: ").append(toIndentedString(averageBytesPerSecond)).append("\n"); - sb.append(" averageNumRecordsPerSecond: ").append(toIndentedString(averageNumRecordsPerSecond)).append("\n"); - sb.append(" averageFieldsPerRecord: ").append(toIndentedString(averageFieldsPerRecord)).append("\n"); - sb.append(" averageBytesPerRecord: ").append(toIndentedString(averageBytesPerRecord)).append("\n"); - sb.append(" totalBytes: ").append(toIndentedString(totalBytes)).append("\n"); - sb.append(" totalFields: ").append(toIndentedString(totalFields)).append("\n"); - sb.append(" totalProcessingTimeInMs: ").append(toIndentedString(totalProcessingTimeInMs)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Processor.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Processor.java deleted file mode 100644 index a94190290..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Processor.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.hurence.logisland.agent.rest.model.Property; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.ArrayList; -import java.util.List; -import javax.validation.constraints.*; - -/** - * Processor - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Processor { - @JsonProperty("name") - private String name = null; - - @JsonProperty("component") - private String component = null; - - @JsonProperty("documentation") - private String documentation = null; - - @JsonProperty("config") - private List config = new ArrayList(); - - public Processor name(String name) { - this.name = name; - return this; - } - - /** - * Get name - * @return name - **/ - @JsonProperty("name") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Processor component(String component) { - this.component = component; - return this; - } - - /** - * Get component - * @return component - **/ - @JsonProperty("component") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getComponent() { - return component; - } - - public void setComponent(String component) { - this.component = component; - } - - public Processor documentation(String documentation) { - this.documentation = documentation; - return this; - } - - /** - * Get documentation - * @return documentation - **/ - @JsonProperty("documentation") - @ApiModelProperty(value = "") - public String getDocumentation() { - return documentation; - } - - public void setDocumentation(String documentation) { - this.documentation = documentation; - } - - public Processor config(List config) { - this.config = config; - return this; - } - - public Processor addConfigItem(Property configItem) { - this.config.add(configItem); - return this; - } - - /** - * Get config - * @return config - **/ - @JsonProperty("config") - @ApiModelProperty(required = true, value = "") - @NotNull - public List getConfig() { - return config; - } - - public void setConfig(List config) { - this.config = config; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Processor processor = (Processor) o; - return Objects.equals(this.name, processor.name) && - Objects.equals(this.component, processor.component) && - Objects.equals(this.documentation, processor.documentation) && - Objects.equals(this.config, processor.config); - } - - @Override - public int hashCode() { - return Objects.hash(name, component, documentation, config); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Processor {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" component: ").append(toIndentedString(component)).append("\n"); - sb.append(" documentation: ").append(toIndentedString(documentation)).append("\n"); - sb.append(" config: ").append(toIndentedString(config)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Property.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Property.java deleted file mode 100644 index 9b3eb2777..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Property.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import javax.validation.constraints.*; - -/** - * Property - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Property { - @JsonProperty("key") - private String key = null; - - @JsonProperty("type") - private String type = "string"; - - @JsonProperty("value") - private String value = null; - - public Property key(String key) { - this.key = key; - return this; - } - - /** - * Get key - * @return key - **/ - @JsonProperty("key") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public Property type(String type) { - this.type = type; - return this; - } - - /** - * Get type - * @return type - **/ - @JsonProperty("type") - @ApiModelProperty(value = "") - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Property value(String value) { - this.value = value; - return this; - } - - /** - * Get value - * @return value - **/ - @JsonProperty("value") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Property property = (Property) o; - return Objects.equals(this.key, property.key) && - Objects.equals(this.type, property.type) && - Objects.equals(this.value, property.value); - } - - @Override - public int hashCode() { - return Objects.hash(key, type, value); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Property {\n"); - - sb.append(" key: ").append(toIndentedString(key)).append("\n"); - sb.append(" type: ").append(toIndentedString(type)).append("\n"); - sb.append(" value: ").append(toIndentedString(value)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Record.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Record.java deleted file mode 100644 index b67f6959f..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Record.java +++ /dev/null @@ -1,187 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.hurence.logisland.agent.rest.model.Field; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.ArrayList; -import java.util.List; - - - - -/** - * Record - */ -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-02-28T16:28:07.083+01:00") -public class Record { - private String id = null; - - private String type = null; - - private String timestampField = "record_time"; - - private String rowkeyField = "record_id"; - - private List fields = new ArrayList(); - - public Record id(String id) { - this.id = id; - return this; - } - - /** - * a unique identifier - * @return id - **/ - @ApiModelProperty(value = "a unique identifier") - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public Record type(String type) { - this.type = type; - return this; - } - - /** - * the type of the record - * @return type - **/ - @ApiModelProperty(required = true, value = "the type of the record") - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public Record timestampField(String timestampField) { - this.timestampField = timestampField; - return this; - } - - /** - * the record_time field - * @return timestampField - **/ - @ApiModelProperty(value = "the record_time field") - public String getTimestampField() { - return timestampField; - } - - public void setTimestampField(String timestampField) { - this.timestampField = timestampField; - } - - public Record rowkeyField(String rowkeyField) { - this.rowkeyField = rowkeyField; - return this; - } - - /** - * the record_id field - * @return rowkeyField - **/ - @ApiModelProperty(value = "the record_id field") - public String getRowkeyField() { - return rowkeyField; - } - - public void setRowkeyField(String rowkeyField) { - this.rowkeyField = rowkeyField; - } - - public Record fields(List fields) { - this.fields = fields; - return this; - } - - public Record addFieldsItem(Field fieldsItem) { - this.fields.add(fieldsItem); - return this; - } - - /** - * Get fields - * @return fields - **/ - @ApiModelProperty(value = "") - public List getFields() { - return fields; - } - - public void setFields(List fields) { - this.fields = fields; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Record record = (Record) o; - return Objects.equals(this.id, record.id) && - Objects.equals(this.type, record.type) && - Objects.equals(this.timestampField, record.timestampField) && - Objects.equals(this.rowkeyField, record.rowkeyField) && - Objects.equals(this.fields, record.fields); - } - - @Override - public int hashCode() { - return Objects.hash(id, type, timestampField, rowkeyField, fields); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Record {\n"); - - sb.append(" id: ").append(toIndentedString(id)).append("\n"); - sb.append(" type: ").append(toIndentedString(type)).append("\n"); - sb.append(" timestampField: ").append(toIndentedString(timestampField)).append("\n"); - sb.append(" rowkeyField: ").append(toIndentedString(rowkeyField)).append("\n"); - sb.append(" fields: ").append(toIndentedString(fields)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Stream.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Stream.java deleted file mode 100644 index 9c6969027..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Stream.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.hurence.logisland.agent.rest.model.Processor; -import com.hurence.logisland.agent.rest.model.Property; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.ArrayList; -import java.util.List; -import javax.validation.constraints.*; - -/** - * Stream - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Stream { - @JsonProperty("name") - private String name = null; - - @JsonProperty("component") - private String component = null; - - @JsonProperty("documentation") - private String documentation = null; - - @JsonProperty("config") - private List config = null; - - @JsonProperty("processors") - private List processors = null; - - public Stream name(String name) { - this.name = name; - return this; - } - - /** - * Get name - * @return name - **/ - @JsonProperty("name") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Stream component(String component) { - this.component = component; - return this; - } - - /** - * Get component - * @return component - **/ - @JsonProperty("component") - @ApiModelProperty(required = true, value = "") - @NotNull - public String getComponent() { - return component; - } - - public void setComponent(String component) { - this.component = component; - } - - public Stream documentation(String documentation) { - this.documentation = documentation; - return this; - } - - /** - * Get documentation - * @return documentation - **/ - @JsonProperty("documentation") - @ApiModelProperty(value = "") - public String getDocumentation() { - return documentation; - } - - public void setDocumentation(String documentation) { - this.documentation = documentation; - } - - public Stream config(List config) { - this.config = config; - return this; - } - - public Stream addConfigItem(Property configItem) { - if (this.config == null) { - this.config = new ArrayList(); - } - this.config.add(configItem); - return this; - } - - /** - * Get config - * @return config - **/ - @JsonProperty("config") - @ApiModelProperty(value = "") - public List getConfig() { - return config; - } - - public void setConfig(List config) { - this.config = config; - } - - public Stream processors(List processors) { - this.processors = processors; - return this; - } - - public Stream addProcessorsItem(Processor processorsItem) { - if (this.processors == null) { - this.processors = new ArrayList(); - } - this.processors.add(processorsItem); - return this; - } - - /** - * Get processors - * @return processors - **/ - @JsonProperty("processors") - @ApiModelProperty(value = "") - public List getProcessors() { - return processors; - } - - public void setProcessors(List processors) { - this.processors = processors; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Stream stream = (Stream) o; - return Objects.equals(this.name, stream.name) && - Objects.equals(this.component, stream.component) && - Objects.equals(this.documentation, stream.documentation) && - Objects.equals(this.config, stream.config) && - Objects.equals(this.processors, stream.processors); - } - - @Override - public int hashCode() { - return Objects.hash(name, component, documentation, config, processors); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Stream {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" component: ").append(toIndentedString(component)).append("\n"); - sb.append(" documentation: ").append(toIndentedString(documentation)).append("\n"); - sb.append(" config: ").append(toIndentedString(config)).append("\n"); - sb.append(" processors: ").append(toIndentedString(processors)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Topic.java b/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Topic.java deleted file mode 100644 index 661423e52..000000000 --- a/logisland-framework/logisland-agent/src/gen/java/com/hurence/logisland/agent/rest/model/Topic.java +++ /dev/null @@ -1,400 +0,0 @@ -/* - * logisland-agent - * REST API for logisland agent - * - * OpenAPI spec version: v1 - * Contact: bailet.thomas@gmail.com - * - * NOTE: This class is auto generated by the swagger code generator program. - * https://github.com/swagger-api/swagger-codegen.git - * Do not edit the class manually. - */ - - -package com.hurence.logisland.agent.rest.model; - -import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.hurence.logisland.agent.rest.model.FieldType; -import io.swagger.annotations.ApiModel; -import io.swagger.annotations.ApiModelProperty; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import javax.validation.constraints.*; - -/** - * Topic - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class Topic { - @JsonProperty("id") - private Long id = null; - - @JsonProperty("version") - private Integer version = null; - - @JsonProperty("name") - private String name = null; - - @JsonProperty("partitions") - private Integer partitions = null; - - @JsonProperty("replicationFactor") - private Integer replicationFactor = null; - - @JsonProperty("dateModified") - private Date dateModified = null; - - @JsonProperty("documentation") - private String documentation = null; - - @JsonProperty("serializer") - private String serializer = "com.hurence.logisland.serializer.KryoSerializer"; - - @JsonProperty("businessTimeField") - private String businessTimeField = "record_time"; - - @JsonProperty("rowkeyField") - private String rowkeyField = "record_id"; - - @JsonProperty("recordTypeField") - private String recordTypeField = "record_type"; - - @JsonProperty("keySchema") - private List keySchema = null; - - @JsonProperty("valueSchema") - private List valueSchema = new ArrayList(); - - public Topic id(Long id) { - this.id = id; - return this; - } - - /** - * a unique identifier for the topic - * @return id - **/ - @JsonProperty("id") - @ApiModelProperty(value = "a unique identifier for the topic") - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Topic version(Integer version) { - this.version = version; - return this; - } - - /** - * the version of the topic configuration - * @return version - **/ - @JsonProperty("version") - @ApiModelProperty(value = "the version of the topic configuration") - public Integer getVersion() { - return version; - } - - public void setVersion(Integer version) { - this.version = version; - } - - public Topic name(String name) { - this.name = name; - return this; - } - - /** - * the name of the topic - * @return name - **/ - @JsonProperty("name") - @ApiModelProperty(required = true, value = "the name of the topic") - @NotNull - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Topic partitions(Integer partitions) { - this.partitions = partitions; - return this; - } - - /** - * default number of partitions - * @return partitions - **/ - @JsonProperty("partitions") - @ApiModelProperty(required = true, value = "default number of partitions") - @NotNull - public Integer getPartitions() { - return partitions; - } - - public void setPartitions(Integer partitions) { - this.partitions = partitions; - } - - public Topic replicationFactor(Integer replicationFactor) { - this.replicationFactor = replicationFactor; - return this; - } - - /** - * default replication factor - * @return replicationFactor - **/ - @JsonProperty("replicationFactor") - @ApiModelProperty(required = true, value = "default replication factor") - @NotNull - public Integer getReplicationFactor() { - return replicationFactor; - } - - public void setReplicationFactor(Integer replicationFactor) { - this.replicationFactor = replicationFactor; - } - - public Topic dateModified(Date dateModified) { - this.dateModified = dateModified; - return this; - } - - /** - * latest date of modification - * @return dateModified - **/ - @JsonProperty("dateModified") - @ApiModelProperty(value = "latest date of modification") - public Date getDateModified() { - return dateModified; - } - - public void setDateModified(Date dateModified) { - this.dateModified = dateModified; - } - - public Topic documentation(String documentation) { - this.documentation = documentation; - return this; - } - - /** - * the description of the topic - * @return documentation - **/ - @JsonProperty("documentation") - @ApiModelProperty(value = "the description of the topic") - public String getDocumentation() { - return documentation; - } - - public void setDocumentation(String documentation) { - this.documentation = documentation; - } - - public Topic serializer(String serializer) { - this.serializer = serializer; - return this; - } - - /** - * the class of the Serializer - * @return serializer - **/ - @JsonProperty("serializer") - @ApiModelProperty(required = true, value = "the class of the Serializer") - @NotNull - public String getSerializer() { - return serializer; - } - - public void setSerializer(String serializer) { - this.serializer = serializer; - } - - public Topic businessTimeField(String businessTimeField) { - this.businessTimeField = businessTimeField; - return this; - } - - /** - * the record_time field - * @return businessTimeField - **/ - @JsonProperty("businessTimeField") - @ApiModelProperty(value = "the record_time field") - public String getBusinessTimeField() { - return businessTimeField; - } - - public void setBusinessTimeField(String businessTimeField) { - this.businessTimeField = businessTimeField; - } - - public Topic rowkeyField(String rowkeyField) { - this.rowkeyField = rowkeyField; - return this; - } - - /** - * the record_id field - * @return rowkeyField - **/ - @JsonProperty("rowkeyField") - @ApiModelProperty(value = "the record_id field") - public String getRowkeyField() { - return rowkeyField; - } - - public void setRowkeyField(String rowkeyField) { - this.rowkeyField = rowkeyField; - } - - public Topic recordTypeField(String recordTypeField) { - this.recordTypeField = recordTypeField; - return this; - } - - /** - * the record type field - * @return recordTypeField - **/ - @JsonProperty("recordTypeField") - @ApiModelProperty(value = "the record type field") - public String getRecordTypeField() { - return recordTypeField; - } - - public void setRecordTypeField(String recordTypeField) { - this.recordTypeField = recordTypeField; - } - - public Topic keySchema(List keySchema) { - this.keySchema = keySchema; - return this; - } - - public Topic addKeySchemaItem(FieldType keySchemaItem) { - if (this.keySchema == null) { - this.keySchema = new ArrayList(); - } - this.keySchema.add(keySchemaItem); - return this; - } - - /** - * Get keySchema - * @return keySchema - **/ - @JsonProperty("keySchema") - @ApiModelProperty(value = "") - public List getKeySchema() { - return keySchema; - } - - public void setKeySchema(List keySchema) { - this.keySchema = keySchema; - } - - public Topic valueSchema(List valueSchema) { - this.valueSchema = valueSchema; - return this; - } - - public Topic addValueSchemaItem(FieldType valueSchemaItem) { - this.valueSchema.add(valueSchemaItem); - return this; - } - - /** - * Get valueSchema - * @return valueSchema - **/ - @JsonProperty("valueSchema") - @ApiModelProperty(required = true, value = "") - @NotNull - public List getValueSchema() { - return valueSchema; - } - - public void setValueSchema(List valueSchema) { - this.valueSchema = valueSchema; - } - - - @Override - public boolean equals(java.lang.Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Topic topic = (Topic) o; - return Objects.equals(this.id, topic.id) && - Objects.equals(this.version, topic.version) && - Objects.equals(this.name, topic.name) && - Objects.equals(this.partitions, topic.partitions) && - Objects.equals(this.replicationFactor, topic.replicationFactor) && - Objects.equals(this.dateModified, topic.dateModified) && - Objects.equals(this.documentation, topic.documentation) && - Objects.equals(this.serializer, topic.serializer) && - Objects.equals(this.businessTimeField, topic.businessTimeField) && - Objects.equals(this.rowkeyField, topic.rowkeyField) && - Objects.equals(this.recordTypeField, topic.recordTypeField) && - Objects.equals(this.keySchema, topic.keySchema) && - Objects.equals(this.valueSchema, topic.valueSchema); - } - - @Override - public int hashCode() { - return Objects.hash(id, version, name, partitions, replicationFactor, dateModified, documentation, serializer, businessTimeField, rowkeyField, recordTypeField, keySchema, valueSchema); - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Topic {\n"); - - sb.append(" id: ").append(toIndentedString(id)).append("\n"); - sb.append(" version: ").append(toIndentedString(version)).append("\n"); - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" partitions: ").append(toIndentedString(partitions)).append("\n"); - sb.append(" replicationFactor: ").append(toIndentedString(replicationFactor)).append("\n"); - sb.append(" dateModified: ").append(toIndentedString(dateModified)).append("\n"); - sb.append(" documentation: ").append(toIndentedString(documentation)).append("\n"); - sb.append(" serializer: ").append(toIndentedString(serializer)).append("\n"); - sb.append(" businessTimeField: ").append(toIndentedString(businessTimeField)).append("\n"); - sb.append(" rowkeyField: ").append(toIndentedString(rowkeyField)).append("\n"); - sb.append(" recordTypeField: ").append(toIndentedString(recordTypeField)).append("\n"); - sb.append(" keySchema: ").append(toIndentedString(keySchema)).append("\n"); - sb.append(" valueSchema: ").append(toIndentedString(valueSchema)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(java.lang.Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/LogislandAgentMain.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/LogislandAgentMain.java deleted file mode 100644 index c1e1cb995..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/LogislandAgentMain.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent; - -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import com.hurence.logisland.kafka.registry.KafkaRegistryRestApplication; -import io.confluent.rest.RestConfigException; -import org.eclipse.jetty.server.Server; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -public class LogislandAgentMain { - - private static final org.slf4j.Logger log = LoggerFactory.getLogger(LogislandAgentMain.class); - - /** - * Starts an embedded Jetty server running the REST server. - */ - public static void main(String[] args) throws IOException { - - try { - if (args.length != 1) { - log.error("Properties file is required to start the schema registry REST instance"); - System.exit(1); - } - - - KafkaRegistryConfig config = new KafkaRegistryConfig(args[0]); - KafkaRegistryRestApplication app = new KafkaRegistryRestApplication(config); - Server server = app.createServer(); - - - server.start(); - log.info("Server started, listening for requests..."); - server.join(); - } catch (RestConfigException e) { - log.error("Server configuration failed: ", e); - System.exit(1); - } catch (Exception e) { - log.error("Server died unexpectedly: ", e); - System.exit(1); - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/RestService.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/RestService.java deleted file mode 100644 index 56b86400b..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/RestService.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest; - -public class RestService { - public RestService(String restConnect) { - - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/Versions.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/Versions.java deleted file mode 100644 index e233a2a3d..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/Versions.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest; - -import java.util.Arrays; -import java.util.List; - -public class Versions { - - public static final String KAFKA_REGISTRY_V1_JSON = "application/vnd.logisland.v1+json"; - // Default weight = 1 - public static final String KAFKA_REGISTRY_V1_JSON_WEIGHTED = KAFKA_REGISTRY_V1_JSON; - // These are defaults that track the most recent API version. These should always be specified - // anywhere the latest version is produced/consumed. - public static final String KAFKA_REGISTRY_MOST_SPECIFIC_DEFAULT = KAFKA_REGISTRY_V1_JSON; - public static final String KAFKA_REGISTRY_DEFAULT_JSON = "application/vnd.logisland+json"; - public static final String SCHEMA_REGISTRY_DEFAULT_JSON_WEIGHTED = - KAFKA_REGISTRY_DEFAULT_JSON + - "; qs=0.9"; - public static final String JSON = "application/json"; - public static final String JSON_WEIGHTED = JSON + "; qs=0.5"; - - public static final List PREFERRED_RESPONSE_TYPES = Arrays - .asList(Versions.KAFKA_REGISTRY_V1_JSON, Versions.KAFKA_REGISTRY_DEFAULT_JSON, - Versions.JSON); - - // This type is completely generic and carries no actual information about the type of data, but - // it is the default for request entities if no content type is specified. Well behaving users - // of the API will always specify the content type, but ad hoc use may omit it. We treat this as - // JSON since that's all we currently support. - public static final String GENERIC_REQUEST = "application/octet-stream"; -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/Bootstrap.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/Bootstrap.java deleted file mode 100644 index f24c02135..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/Bootstrap.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.api; - -import io.swagger.jaxrs.config.SwaggerContextService; -import io.swagger.models.Contact; -import io.swagger.models.Info; -import io.swagger.models.License; -import io.swagger.models.Swagger; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; - -public class Bootstrap extends HttpServlet { - - private static Logger logger = LoggerFactory.getLogger(Bootstrap.class); - - @Override - public void init(ServletConfig config) throws ServletException { - Info info = new Info() - .title("Swagger Server") - .description("REST API for logisland agent") - .termsOfService("") - .contact(new Contact() - .email("bailet.thomas@gmail.com")) - .license(new License() - .name("") - .url("")); - - logger.info("starting logisland Agent"); - - - ServletContext context = config.getServletContext(); - Swagger swagger = new Swagger().info(info); - - new SwaggerContextService().withServletConfig(config).updateSwagger(swagger); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ConfigsApiServiceFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ConfigsApiServiceFactory.java deleted file mode 100644 index e4c69661a..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ConfigsApiServiceFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hurence.logisland.agent.rest.api.factories; - -import com.hurence.logisland.agent.rest.api.ConfigsApiService; -import com.hurence.logisland.agent.rest.api.impl.ConfigsApiServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ConfigsApiServiceFactory { - private static ConfigsApiService service = null; - - public static ConfigsApiService getConfigsApi(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new ConfigsApiServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/DefaultApiServiceFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/DefaultApiServiceFactory.java deleted file mode 100644 index 828ef3969..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/DefaultApiServiceFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hurence.logisland.agent.rest.api.factories; - -import com.hurence.logisland.agent.rest.api.DefaultApiService; -import com.hurence.logisland.agent.rest.api.impl.DefaultApiServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class DefaultApiServiceFactory { - private static DefaultApiService service = null; - - public static DefaultApiService getDefaultApi(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new DefaultApiServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/JobsApiServiceFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/JobsApiServiceFactory.java deleted file mode 100644 index 3fa845ec9..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/JobsApiServiceFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hurence.logisland.agent.rest.api.factories; - -import com.hurence.logisland.agent.rest.api.JobsApiService; -import com.hurence.logisland.agent.rest.api.impl.JobsApiServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class JobsApiServiceFactory { - private static JobsApiService service = null; - - public static JobsApiService getJobsApi(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new JobsApiServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/MetricsApiServiceFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/MetricsApiServiceFactory.java deleted file mode 100644 index 6d6b235ba..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/MetricsApiServiceFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hurence.logisland.agent.rest.api.factories; - -import com.hurence.logisland.agent.rest.api.MetricsApiService; -import com.hurence.logisland.agent.rest.api.impl.MetricsApiServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class MetricsApiServiceFactory { - private static MetricsApiService service = null; - - public static MetricsApiService getMetricsApi(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new MetricsApiServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ProcessorsApiServiceFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ProcessorsApiServiceFactory.java deleted file mode 100644 index 103370039..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/ProcessorsApiServiceFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hurence.logisland.agent.rest.api.factories; - -import com.hurence.logisland.agent.rest.api.ProcessorsApiService; -import com.hurence.logisland.agent.rest.api.impl.ProcessorsApiServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class ProcessorsApiServiceFactory { - private static ProcessorsApiService service = null; - - public static ProcessorsApiService getProcessorsApi(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new ProcessorsApiServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/TopicsApiServiceFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/TopicsApiServiceFactory.java deleted file mode 100644 index ddaf01904..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/factories/TopicsApiServiceFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hurence.logisland.agent.rest.api.factories; - -import com.hurence.logisland.agent.rest.api.TopicsApiService; -import com.hurence.logisland.agent.rest.api.impl.TopicsApiServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class TopicsApiServiceFactory { - private static TopicsApiService service = null; - - public static TopicsApiService getTopicsApi(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new TopicsApiServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ConfigsApiServiceImpl.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ConfigsApiServiceImpl.java deleted file mode 100644 index 7537fee9f..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ConfigsApiServiceImpl.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.api.impl; - -import com.hurence.logisland.agent.rest.api.ConfigsApiService; -import com.hurence.logisland.agent.rest.api.NotFoundException; -import com.hurence.logisland.agent.rest.model.Property; -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import java.util.ArrayList; -import java.util.List; - - -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-02-28T17:12:21.474+01:00") -public class ConfigsApiServiceImpl extends ConfigsApiService { - - public ConfigsApiServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - } - - @Override - public Response getConfig(SecurityContext securityContext) throws NotFoundException { - - KafkaRegistryConfig config = kafkaRegistry.getConfig(); - List configs = new ArrayList<>(); - configs.add(new Property() - .key(config.KAFKASTORE_TOPIC_JOBS_CONFIG) - .value(config.getString(config.KAFKASTORE_TOPIC_JOBS_CONFIG))); - configs.add(new Property() - .key(config.KAFKASTORE_TOPIC_TOPICS_CONFIG) - .value(config.getString(config.KAFKASTORE_TOPIC_TOPICS_CONFIG))); - configs.add(new Property() - .key(config.KAFKA_METADATA_BROKER_LIST_CONFIG) - .value(config.getString(config.KAFKA_METADATA_BROKER_LIST_CONFIG))); - configs.add(new Property() - .key(config.KAFKA_ZOOKEEPER_QUORUM_CONFIG) - .value(config.getString(config.KAFKA_ZOOKEEPER_QUORUM_CONFIG))); - configs.add(new Property() - .key(config.KAFKA_TOPIC_AUTOCREATE_CONFIG) - .type("boolean") - .value(String.valueOf(config.getBoolean(config.KAFKA_TOPIC_AUTOCREATE_CONFIG)))); - configs.add(new Property() - .key(config.KAFKA_TOPIC_DEFAULT_PARTITION_CONFIG) - .type("integer") - .value(String.valueOf(config.getInt(config.KAFKA_TOPIC_DEFAULT_PARTITION_CONFIG)))); - configs.add(new Property() - .key(config.KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR_CONFIG) - .type("integer") - .value(String.valueOf(config.getInt(config.KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR_CONFIG)))); - - return Response.ok().entity(configs).build(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/DefaultApiServiceImpl.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/DefaultApiServiceImpl.java deleted file mode 100644 index 115234963..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/DefaultApiServiceImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.api.impl; - -import com.hurence.logisland.agent.rest.api.*; - - -import com.hurence.logisland.agent.rest.api.NotFoundException; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-02-17T11:14:18.946+01:00") - public class DefaultApiServiceImpl extends DefaultApiService { - - public DefaultApiServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - } - - @Override - public Response rootGet(SecurityContext securityContext) throws NotFoundException { - // do some magic! - return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); - } - } diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/JobsApiServiceImpl.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/JobsApiServiceImpl.java deleted file mode 100644 index cb6777cc8..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/JobsApiServiceImpl.java +++ /dev/null @@ -1,404 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.api.impl; - -import com.hurence.logisland.agent.rest.api.ApiResponseMessage; -import com.hurence.logisland.agent.rest.api.JobsApiService; -import com.hurence.logisland.agent.rest.api.NotFoundException; -import com.hurence.logisland.agent.rest.model.Engine; -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Job; -import com.hurence.logisland.agent.rest.model.JobSummary; -import com.hurence.logisland.agent.utils.YarnApplication; -import com.hurence.logisland.agent.utils.YarnApplicationWrapper; -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.exceptions.RegistryException; -import org.apache.commons.exec.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Date; -import java.util.List; - -import static com.hurence.logisland.agent.rest.model.JobSummary.StatusEnum.*; - -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-02-15T10:15:35.873+01:00") -public class JobsApiServiceImpl extends JobsApiService { - - public JobsApiServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - } - - - private static Logger logger = LoggerFactory.getLogger(JobsApiService.class); - - - //------------------------ - // CRUD Jobs section - //------------------------ - - @Override - public Response addJobWithId(Job body, String jobId, SecurityContext securityContext) throws NotFoundException { - return addJob(body.name(jobId), securityContext); - } - - @Override - public Response addJob(Job job, SecurityContext securityContext) throws NotFoundException { - logger.debug("adding job " + job); - - try { - Job job0 = kafkaRegistry.addJob(job); - return Response.ok().entity(job0).build(); - } catch (RegistryException e) { - String error = "unable to add job into kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - - - } - - @Override - public Response updateJob(String jobId, Job job, SecurityContext securityContext) throws NotFoundException { - logger.debug("update job " + job); - - try { - // update date modified - updateJobStatus(job, job.getSummary().getStatus()); - Job job0 = kafkaRegistry.updateJob(job); - return Response.ok().entity(job0).build(); - } catch (RegistryException e) { - String error = "unable to update job into kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - } - - - @Override - public Response deleteJob(String jobId, SecurityContext securityContext) throws NotFoundException { - logger.debug("delete job"); - try { - kafkaRegistry.deleteJob(jobId); - return Response.ok().build(); - } catch (RegistryException e) { - String error = "unable to get alls job from kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - } - - @Override - public Response getAllJobs(SecurityContext securityContext) throws NotFoundException { - - logger.debug("get all jobs"); - try { - List jobs = kafkaRegistry.getAllJobs(); - return Response.ok().entity(jobs).build(); - } catch (RegistryException e) { - String error = "unable to get alls job from kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - - } - - @Override - public Response getJob(String jobId, SecurityContext securityContext) throws NotFoundException { - - logger.debug("get job " + jobId); - - - Job job = null; - try { - job = kafkaRegistry.getJob(jobId); - } catch (RegistryException e) { - return Response.serverError().entity(e).build(); - } - - if (job == null) - return Response.serverError() - .status(Response.Status.NOT_FOUND) - .entity(new Error().code(404).message("Job not found for id: " + jobId)) - .build(); - else - return Response.ok().entity(job).build(); - } - - - @Override - public Response getJobEngine(String jobId, SecurityContext securityContext) throws NotFoundException { - logger.debug("get job engine" + jobId); - - - StringBuilder builder = new StringBuilder(); - try { - Engine engine = kafkaRegistry.getJob(jobId).getEngine(); - engine.getConfig().stream().forEach(property -> { - builder.append(property.getKey()); - builder.append(": "); - builder.append(property.getValue()); - builder.append("\n"); - }); - } catch (RegistryException e) { - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Unable to find job " + jobId)) - .build(); - } - - - return Response.ok().entity(builder.toString()).build(); - } - - - //------------------------ - // Jobs metrology - //------------------------ - @Override - public Response getJobAlerts(Integer count, SecurityContext securityContext) throws NotFoundException { - return Response.status(Response.Status.NOT_IMPLEMENTED) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "not implemented yet")) - .build(); - } - - @Override - public Response getJobErrors(Integer count, SecurityContext securityContext) throws NotFoundException { - return Response.status(Response.Status.NOT_IMPLEMENTED) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "not implemented yet")) - .build(); - } - - @Override - public Response getJobMetrics(Integer count, SecurityContext securityContext) throws NotFoundException { - return Response.status(Response.Status.NOT_IMPLEMENTED) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "not implemented yet")) - .build(); - } - - - //------------------------ - // Jobs scheduling - //------------------------ - @Override - public Response getJobStatus(String jobId, SecurityContext securityContext) throws NotFoundException { - - Job job = null; - try { - job = kafkaRegistry.getJob(jobId); - if (job == null) - throw new RegistryException("job " + jobId + "not found !"); - } catch (RegistryException e) { - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Unable to find job " + jobId)) - .build(); - } - - return Response.ok().entity(job.getSummary().getStatus()).build(); - } - - @Override - public Response getJobVersion(String jobId, SecurityContext securityContext) throws NotFoundException { - Job job = null; - try { - job = kafkaRegistry.getJob(jobId); - if (job == null) - throw new RegistryException("job " + jobId + "not found !"); - } catch (RegistryException e) { - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Unable to find job " + jobId)) - .build(); - } - - return Response.ok().entity(job.getVersion()).build(); - } - - @Override - public Response pauseJob(String jobId, SecurityContext securityContext) throws NotFoundException { - Job job = null; - try { - job = kafkaRegistry.getJob(jobId); - if (job == null) - throw new RegistryException("job " + jobId + "not found !"); - } catch (RegistryException e) { - return Response.serverError().entity(e).build(); - } - - switch (job.getSummary().getStatus()) { - case PAUSED: - updateJobStatus(job, RUNNING); - return updateJob(jobId, job, securityContext); - case RUNNING: - updateJobStatus(job, PAUSED); - return updateJob(jobId, job, securityContext); - default: - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Unable to pause a " + job.getSummary().getStatus() + " job " + jobId)) - .build(); - } - } - - @Override - public Response reStartJob(String jobId, SecurityContext securityContext) throws NotFoundException { - //TODO dont' forget to wait for real end of the job - return Response.status(Response.Status.NOT_IMPLEMENTED) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "not implemented yet")) - .build(); - } - - @Override - public Response shutdownJob(String jobId, SecurityContext securityContext) throws NotFoundException { - - - Job job = null; - try { - - job = kafkaRegistry.getJob(jobId); - - - if (job == null) - throw new RegistryException("job " + jobId + "not found !"); - - - // find the scheduler type - final String[] scheduler = {"local"}; - job.getEngine().getConfig().forEach(prop -> { - if (prop.getKey().equals("spark.master")) { - if (prop.getValue().contains("yarn")) - scheduler[0] = "yarn"; - else if (prop.getValue().contains("mesos")) - scheduler[0] = "mesos"; - else - scheduler[0] = "local"; - } - }); - - if (scheduler[0].equals("yarn")) { - logger.info("retrieving yarn application"); - CommandLine cmdLine = new CommandLine("yarn"); - cmdLine.addArgument("application"); - cmdLine.addArgument("-list"); - ByteArrayOutputStream stdout = new ByteArrayOutputStream(); - PumpStreamHandler psh = new PumpStreamHandler(stdout); - Executor executor = new DefaultExecutor(); - executor.setExitValue(0); - executor.setStreamHandler(psh); - try { - executor.execute(cmdLine); - } catch (IOException e) { - logger.error(e.toString()); - } - YarnApplicationWrapper wrapper = new YarnApplicationWrapper(stdout.toString()); - YarnApplication app = wrapper.getApplication(job.getName()); - if (app != null) { - logger.info("Killing Yarn application {}", app.getId()); - CommandLine killCmdLine = new CommandLine("yarn"); - killCmdLine.addArgument("application"); - killCmdLine.addArgument("-kill"); - killCmdLine.addArgument(app.getId()); - Executor killExecutor = new DefaultExecutor(); - killExecutor.setExitValue(0); - try { - killExecutor.execute(killCmdLine); - } catch (IOException e) { - logger.error(e.toString()); - } - } else - logger.error("Yarn application {} not found, may it wasn't running", job.getName()); - - try { - stdout.close(); - } catch (IOException e) { - e.printStackTrace(); - } - - updateJobStatus(job, STOPPED); - return updateJob(jobId, job, securityContext); - } - - - } catch (RegistryException e) { - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Unable to shutdown job " + jobId)) - .build(); - } - - return Response.ok().entity("done nothing").build(); - } - - @Override - public Response startJob(String jobId, SecurityContext securityContext) throws NotFoundException { - - Job job = null; - try { - job = kafkaRegistry.getJob(jobId); - if (job == null) - throw new RegistryException("job " + jobId + "not found !"); - } catch (RegistryException e) { - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "Unable to start job " + jobId)) - .build(); - } - - CommandLine cmdLine = new CommandLine("bin/logisland-launch-spark-job"); - cmdLine.addArgument("--agent"); - cmdLine.addArgument("http://0.0.0.0:8081"); - cmdLine.addArgument("--job"); - cmdLine.addArgument(jobId); - - DefaultExecuteResultHandler resultHandler = new DefaultExecuteResultHandler(); - - - // kill launching after 5' - ExecuteWatchdog watchdog = new ExecuteWatchdog(5 * 60 * 1000); - Executor executor = new DaemonExecutor(); - executor.setWatchdog(watchdog); - - ByteArrayOutputStream stdout = new ByteArrayOutputStream(); - PumpStreamHandler psh = new PumpStreamHandler(stdout); - executor.setExitValue(0); - executor.setStreamHandler(psh); - try { - executor.execute(cmdLine, resultHandler); - } catch (IOException e) { - e.printStackTrace(); - } - - updateJobStatus(job, RUNNING); - - - return updateJob(jobId, job, securityContext); - } - - private void updateJobStatus(Job job, JobSummary.StatusEnum newStatus) { - JobSummary summary = job.getSummary(); - summary.dateModified(new Date()) - .status(newStatus); - job.setSummary(summary); - } - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/MetricsApiServiceImpl.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/MetricsApiServiceImpl.java deleted file mode 100644 index 0419cdab6..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/MetricsApiServiceImpl.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.hurence.logisland.agent.rest.api.impl; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.hurence.logisland.agent.rest.api.MetricsApiService; -import com.hurence.logisland.agent.rest.api.NotFoundException; -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import org.apache.kafka.clients.consumer.*; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.errors.WakeupException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import java.io.IOException; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-07-28T16:23:56.034+02:00") -public class MetricsApiServiceImpl extends MetricsApiService { - - private static final int DEFAULT_ZK_SESSION_TIMEOUT_MS = 10 * 1000; - private static final int DEFAULT_ZK_CONNECTION_TIMEOUT_MS = 8 * 1000; - private static final int DEFAULT_POLLING_TIMEOUT_MS = 2 * 1000; - - private static final String DEFAULT_GROUP_IP = "LogislandAgent"; - - - private static Logger logger = LoggerFactory.getLogger(MetricsApiServiceImpl.class); - - - private Map metrics = new HashMap<>(); - - - public MetricsApiServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - - ConsumerThread consumerThread = new ConsumerThread(kafkaRegistry, metrics); - consumerThread.start(); - } - - - private static class ConsumerThread extends Thread { - private KafkaRegistry kafkaRegistry; - private KafkaConsumer kafkaConsumer; - private Map metrics; - - public ConsumerThread(KafkaRegistry kafkaRegistry, Map metrics) { - this.kafkaRegistry = kafkaRegistry; - this.metrics = metrics; - } - - @Override - public void run() { - - Pattern mainPattern = Pattern.compile("(.*?)[.](.*?)[.](.*)"); - Pattern logislandPattern = Pattern.compile("(.*?)[.](.*?)[.](.*?)[.]partition(.*?)[.](.*?)[.](.*)"); - ObjectMapper objectMapper = new ObjectMapper(); - Properties configProperties = new Properties(); - configProperties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); - configProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, - kafkaRegistry.getConfig().getString(KafkaRegistryConfig.KAFKA_METADATA_BROKER_LIST_CONFIG)); - configProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, - "org.apache.kafka.common.serialization.StringDeserializer"); - configProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - "org.apache.kafka.common.serialization.StringDeserializer"); - configProperties.put(ConsumerConfig.GROUP_ID_CONFIG, DEFAULT_GROUP_IP); - - //Figure out where to start processing messages from - kafkaConsumer = new KafkaConsumer(configProperties); - kafkaConsumer.subscribe(Arrays.asList( - kafkaRegistry.getConfig().getString(KafkaRegistryConfig.KAFKASTORE_TOPIC_METRICS_CONFIG)), - new ConsumerRebalanceListener() { - public void onPartitionsRevoked(Collection partitions) { - logger.info("{} topic-partitions are revoked from this consumer\n", - Arrays.toString(partitions.toArray())); - } - - public void onPartitionsAssigned(Collection partitions) { - logger.info("{} topic-partitions are assigned to this consumer\n", - Arrays.toString(partitions.toArray())); - } - }); - //Start processing messages - try { - while (true) { - ConsumerRecords records = kafkaConsumer.poll(DEFAULT_POLLING_TIMEOUT_MS); - for (ConsumerRecord record : records) { - String jsonValue = record.value(); - - JsonNode jsonNode = objectMapper.readTree(jsonValue); - String type = jsonNode.get("type").asText(); - String value = ""; - - - - - switch (type) { - case "counter": - case "gauge": - value = jsonNode.get("value").asText(); - break; - case "meter": - case "histogram": - case "timer": - value = jsonNode.get("value").get("n").asText(); - break; - default: - value = ""; - break; - } - - - Matcher mainMatcher = mainPattern.matcher(record.key()); - if (mainMatcher.matches()) { - StringBuilder sbuf = new StringBuilder(); - Formatter fmt = new Formatter(sbuf); - - if(mainMatcher.group(3).contains("partition")){ - Matcher secondaryMatcher = logislandPattern.matcher(mainMatcher.group(3)); - if(secondaryMatcher.matches()){ - fmt.format("logisland_%s{ app_id=\"%s\", app_handler=\"%s\", job=\"%s\", pipeline=\"%s\", component=\"%s\", partition=\"%s\" } %s", - secondaryMatcher.group(6), - mainMatcher.group(1), - mainMatcher.group(2), - secondaryMatcher.group(1), - secondaryMatcher.group(3), - secondaryMatcher.group(5), - secondaryMatcher.group(4), - value); - } - }else { - String metricName = mainMatcher.group(3).replaceAll("\\.", "_"); - fmt.format("%s{ app_id=\"%s\", app_handler=\"%s\" } %s", metricName, mainMatcher.group(1), mainMatcher.group(2), value); - } - - - metrics.put(record.key(), sbuf.toString()); - } - - - - } - } - } catch (WakeupException ex) { - System.out.println("Exception caught " + ex.getMessage()); - } catch (JsonParseException e) { - e.printStackTrace(); - } catch (JsonMappingException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - kafkaConsumer.close(); - System.out.println("After closing KafkaConsumer"); - } - } - - public KafkaConsumer getKafkaConsumer() { - return this.kafkaConsumer; - } - } - - private String mapToString(Map map) { - StringBuilder stringBuilder = new StringBuilder(); - - - SortedSet sortedKeys =new TreeSet(map.keySet()); - - - for (String key : sortedKeys) { - if (stringBuilder.length() > 0) { - stringBuilder.append("\n"); - } - String value = map.get(key); - stringBuilder.append(value != null ? value : ""); - - } - stringBuilder.append("\n"); - return stringBuilder.toString(); - } - - @Override - public Response getMetrics(SecurityContext securityContext) throws NotFoundException { - - - return Response.ok() - .type("text/plain") - .entity(mapToString(metrics)) - .build(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ProcessorsApiServiceImpl.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ProcessorsApiServiceImpl.java deleted file mode 100644 index cfe2549ba..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/ProcessorsApiServiceImpl.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.api.impl; - -import com.hurence.logisland.agent.rest.api.NotFoundException; -import com.hurence.logisland.agent.rest.api.ProcessorsApiService; -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.util.file.FileUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; - -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-03-23T11:53:12.750+01:00") -public class ProcessorsApiServiceImpl extends ProcessorsApiService { - - - private static Logger logger = LoggerFactory.getLogger(ProcessorsApiServiceImpl.class); - - public ProcessorsApiServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - } - - @Override - public Response getProcessors(SecurityContext securityContext) throws NotFoundException { - String jsonPlugins = FileUtil.loadFileContentAsString(JSON_PLUGINS_FILE, "UTF-8"); - // do some magic! - return Response.ok().entity(jsonPlugins).build(); - } - - - private static String JSON_PLUGINS_FILE = "components.json"; - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/TopicsApiServiceImpl.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/TopicsApiServiceImpl.java deleted file mode 100644 index cd504a5ff..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/api/impl/TopicsApiServiceImpl.java +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.api.impl; - -import com.hurence.logisland.agent.rest.api.ApiResponseMessage; -import com.hurence.logisland.agent.rest.api.NotFoundException; -import com.hurence.logisland.agent.rest.api.TopicsApiService; -import com.hurence.logisland.agent.rest.model.Error; -import com.hurence.logisland.agent.rest.model.Topic; -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import com.hurence.logisland.kafka.registry.exceptions.RegistryException; -import kafka.admin.AdminUtils; -import kafka.admin.RackAwareMode; -import kafka.utils.ZkUtils; -import org.apache.kafka.common.security.JaasUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import java.util.List; -import java.util.Properties; - -@javax.annotation.Generated(value = "class io.swagger.codegen.languages.JavaJerseyServerCodegen", date = "2017-02-17T11:14:18.946+01:00") -public class TopicsApiServiceImpl extends TopicsApiService { - - private final ZkUtils zkUtils; - private static final int DEFAULT_ZK_SESSION_TIMEOUT_MS = 10 * 1000; - private static final int DEFAULT_ZK_CONNECTION_TIMEOUT_MS = 8 * 1000; - - public TopicsApiServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - - zkUtils = ZkUtils.apply( - kafkaRegistry.getConfig().getString(KafkaRegistryConfig.KAFKASTORE_CONNECTION_URL_CONFIG), - DEFAULT_ZK_SESSION_TIMEOUT_MS, - DEFAULT_ZK_CONNECTION_TIMEOUT_MS, - JaasUtils.isZkSecurityEnabled()); - } - - private static Logger logger = LoggerFactory.getLogger(TopicsApiServiceImpl.class); - - @Override - public Response addNewTopic(Topic body, SecurityContext securityContext) throws NotFoundException { - - - logger.debug("adding topic " + body); - - try { - Topic newTopic = kafkaRegistry.addTopic(body); - createTopic(newTopic.getName(), newTopic.getPartitions(), newTopic.getReplicationFactor()); - return Response.ok().entity(newTopic).build(); - } catch (RegistryException e) { - String error = "unable to add topic into kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.SERVICE_UNAVAILABLE) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - } - - @Override - public Response deleteTopic(String topicId, SecurityContext securityContext) throws NotFoundException { - logger.debug("delete topic"); - try { - kafkaRegistry.deleteTopic(topicId); - deleteTopic(topicId); - - return Response.ok().build(); - } catch (RegistryException e) { - String error = "unable to delete topic" + topicId + " from kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - } - - @Override - public Response getAllTopics(SecurityContext securityContext) throws NotFoundException { - logger.debug("get all topics"); - try { - List topics = kafkaRegistry.getAllTopics(); - return Response.ok().entity(topics).build(); - } catch (RegistryException e) { - String error = "unable to get alls topics from kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.BAD_REQUEST) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - } - - @Override - public Response getTopic(String topicId, SecurityContext securityContext) throws NotFoundException { - logger.debug("get topic " + topicId); - - - Topic topic = null; - try { - topic = kafkaRegistry.getTopic(topicId); - } catch (RegistryException e) { - logger.error("topic not found {}. {}", topicId, e); - } - - if (topic == null) - return Response.serverError() - .status(Response.Status.NOT_FOUND) - .entity(new Error().code(404).message("Topic not found for id: " + topicId)) - .build(); - else - return Response.ok().entity(topic).build(); - } - - @Override - public Response updateTopic(Topic body, String topicId, SecurityContext securityContext) throws NotFoundException { - logger.debug("update topic " + body); - - try { - Topic newTopic = kafkaRegistry.updateTopic(body); - - createTopic(newTopic.getName(), newTopic.getPartitions(), newTopic.getReplicationFactor()); - return Response.ok().entity(newTopic).build(); - } catch (RegistryException e) { - String error = "unable to update topic into kafkastore " + e; - logger.error(error); - return Response.status(Response.Status.NOT_FOUND) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, error)) - .build(); - } - } - - @Override - public Response checkTopicKeySchemaCompatibility(String body, String topicId, SecurityContext securityContext) throws NotFoundException { - return Response.status(Response.Status.NOT_IMPLEMENTED) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "not implemented yet")) - .build(); - } - - @Override - public Response checkTopicValueSchemaCompatibility(String topicId, String body, SecurityContext securityContext) throws NotFoundException { - return Response.status(Response.Status.NOT_IMPLEMENTED) - .entity(new ApiResponseMessage(ApiResponseMessage.ERROR, "not implemented yet")) - .build(); - } - - @Override - public Response getTopicKeySchema(String topicId, String version, SecurityContext securityContext) throws NotFoundException { - // do some magic! - return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); - } - - @Override - public Response getTopicValueSchema(String topicId, String version, SecurityContext securityContext) throws NotFoundException { - // do some magic! - return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); - } - - - @Override - public Response updateTopicKeySchema(String body, String topicId, SecurityContext securityContext) throws NotFoundException { - // do some magic! - return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); - } - - @Override - public Response updateTopicValueSchema(String body, String topicId, SecurityContext securityContext) throws NotFoundException { - // do some magic! - return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); - } - - - private void deleteTopic(String topic) { - if (AdminUtils.topicExists(zkUtils, topic)) { - AdminUtils.deleteTopic(zkUtils, topic); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - logger.info("deleted topic"); - } - } - - private void createTopic(String topic, int partitions, int replicationFactor) { - if (!AdminUtils.topicExists(zkUtils, topic)) { - AdminUtils.createTopic( - zkUtils, - topic, - partitions, - replicationFactor, - new Properties(), - RackAwareMode.Enforced$.MODULE$); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - logger.info("created topic $topic with " + partitions + - " partitions and " + replicationFactor + - " replicas"); - } - } - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/ConfigsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/ConfigsApiClient.java deleted file mode 100644 index 49f5269bd..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/ConfigsApiClient.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.client.exceptions.RestClientException; -import com.hurence.logisland.agent.rest.model.Property; - -import java.util.List; - -public interface ConfigsApiClient { - - List getConfigs() throws RestClientException; -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/JobsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/JobsApiClient.java deleted file mode 100644 index efba415ab..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/JobsApiClient.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.client.exceptions.RestClientException; -import com.hurence.logisland.agent.rest.model.Job; - -public interface JobsApiClient { - Job addJob(Job job) throws RestClientException; - - Job getJob(String name) throws RestClientException; - - Integer getJobVersion(String name) throws RestClientException; -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockConfigsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockConfigsApiClient.java deleted file mode 100644 index 588789b3f..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockConfigsApiClient.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.model.Property; - -import java.util.ArrayList; -import java.util.List; - - -public class MockConfigsApiClient implements ConfigsApiClient { - - - public MockConfigsApiClient(String zkPort, String kafkaPort) { - - properties.add(new Property().key("kafka.metadata.broker.list").value("localhost:" + kafkaPort)); - properties.add(new Property().key("kafka.zookeeper.quorum").value("localhost:" + zkPort)); - properties.add(new Property().key("kafka.topic.autoCreate").value("true")); - properties.add(new Property().key("kafka.topic.default.partitions").value("1")); - properties.add(new Property().key("kafka.topic.default.replicationFactor").value("1")); - } - - - - - - - - List properties = new ArrayList<>(); - - - @Override - public List getConfigs() { - return properties; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockJobsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockJobsApiClient.java deleted file mode 100644 index a663d6fa6..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockJobsApiClient.java +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.util.runner.MockProcessor; -import com.hurence.logisland.processor.SplitText; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - - -/** - * http://www.hascode.com/2013/12/jax-rs-2-0-rest-client-features-by-example/ - */ -public class MockJobsApiClient implements JobsApiClient { - - public static final String MAGIC_STRING = "the world is so big"; - public static final String APACHE_PARSING_JOB = "apache_parsing_job"; - public static final String MOCK_PROCESSING_JOB = "mock_processing_job"; - - public MockJobsApiClient() { - - JobSummary summary = new JobSummary() - .dateModified(new Date()) - .documentation("sample job") - .status(JobSummary.StatusEnum.RUNNING) - .usedCores(2) - .usedMemory(24); - - - Processor apacheParserProcessor = new Processor() - .name("apacheParser") - .component(SplitText.class.getCanonicalName()) - .addConfigItem(new Property() - .key(SplitText.RECORD_TYPE.getName()) - .value("apache_log")) - .addConfigItem(new Property() - .key(SplitText.VALUE_REGEX.getName()) - .value("(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+\\[([\\w:\\/]+\\s[+\\-]\\d{4})\\]\\s+\"(\\S+)\\s+(\\S+)\\s*(\\S*)\"\\s+(\\S+)\\s+(\\S+)")) - .addConfigItem(new Property() - .key(SplitText.VALUE_FIELDS.getName()) - .value("src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out")); - - - addJob(new Job() - .id(1234L) - .name(APACHE_PARSING_JOB) - .version(1) - .engine(new Engine() - .name("apache parser engine") - .component("com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine") - .addConfigItem(new Property().key("spark.app.name").value(APACHE_PARSING_JOB)) - .addConfigItem(new Property().key("spark.master").value("local[4]")) - .addConfigItem(new Property().key("spark.streaming.batchDuration").value("500")) - .addConfigItem(new Property().key("spark.streaming.timeout").value("12000"))) - .addStreamsItem(new Stream() - .name("apacheStream") - .component("com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing") - .addConfigItem(new Property().key("kafka.input.topics").value("apache_raw")) - .addConfigItem(new Property().key("kafka.output.topics").value("apache_records")) - .addConfigItem(new Property().key("kafka.error.topics").value("_errors")) - .addProcessorsItem(apacheParserProcessor)) - .summary(summary)); - - addJob(new Job() - .id(1235L) - .name(MOCK_PROCESSING_JOB) - .version(1) - .engine(new Engine() - .name("apache parser engine") - .component("com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine") - .addConfigItem(new Property().key("spark.app.name").value(MOCK_PROCESSING_JOB)) - .addConfigItem(new Property().key("spark.master").value("local[4]")) - .addConfigItem(new Property().key("spark.streaming.batchDuration").value("500")) - .addConfigItem(new Property().key("spark.streaming.timeout").value("12000"))) - .addStreamsItem(new Stream() - .name("mockStream") - .component("com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing") - .addConfigItem(new Property().key("kafka.input.topics").value("mock_in")) - .addConfigItem(new Property().key("kafka.output.topics").value("mock_out")) - .addConfigItem(new Property().key("kafka.error.topics").value("_errors")) - .addProcessorsItem(new Processor() - .name("mockProcessor") - .component(MockProcessor.class.getCanonicalName()) - .addConfigItem(new Property() - .key(MockProcessor.FAKE_MESSAGE.getName()) - .value(MAGIC_STRING)))) - .summary(summary)); - - } - - Map jobs = new HashMap<>(); - - @Override - public Job addJob(Job job) { - jobs.put(job.getName(), job); - return job; - } - - @Override - public Job getJob(String name) { - return jobs.get(name); - } - - @Override - public Integer getJobVersion(String name) { - return getJob(name).getVersion(); - } - - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockTopicsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockTopicsApiClient.java deleted file mode 100644 index 701a5b4cf..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/MockTopicsApiClient.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.model.Topic; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - - -public class MockTopicsApiClient implements TopicsApiClient { - - public static final String APACHE_RAW = "apache_raw"; - public static final String APACHE_RECORDS = "apache_records"; - public static final String ERRORS = "_errors"; - public static final String METRICS = "_metrics"; - public static final String MOCK_IN = "mock_in"; - public static final String MOCK_OUT = "mock_out"; - - public MockTopicsApiClient() { - - addTopic(new Topic().id(1234L) - .partitions(2) - .replicationFactor(0) - .documentation("apache logs") - .dateModified(new Date()) - .name(APACHE_RAW) - .serializer("none")); - - addTopic(new Topic().id(1235L) - .partitions(2) - .replicationFactor(0) - .documentation("apache records") - .dateModified(new Date()) - .name(APACHE_RECORDS) - .serializer("com.hurence.logisland.serializer.KryoSerializer")); - - addTopic(new Topic().id(1236L) - .partitions(1) - .replicationFactor(0) - .documentation("errors") - .dateModified(new Date()) - .name(ERRORS) - .serializer("com.hurence.logisland.serializer.KryoSerializer")); - - addTopic(new Topic().id(1236L) - .partitions(1) - .replicationFactor(0) - .documentation("metrics") - .dateModified(new Date()) - .name(METRICS) - .serializer("com.hurence.logisland.serializer.KryoSerializer")); - - - addTopic(new Topic().id(1237L) - .partitions(1) - .replicationFactor(0) - .documentation(MOCK_IN) - .dateModified(new Date()) - .name(MOCK_IN) - .serializer("com.hurence.logisland.serializer.KryoSerializer")); - - addTopic(new Topic().id(1237L) - .partitions(1) - .replicationFactor(0) - .documentation(MOCK_OUT) - .dateModified(new Date()) - .name(MOCK_OUT) - .serializer("com.hurence.logisland.serializer.KryoSerializer")); - } - - Map topics = new HashMap<>(); - - @Override - public Topic addTopic(Topic topic) { - topics.put(topic.getName(), topic); - - return topic; - } - - @Override - public Topic getTopic(String name) { - return topics.get(name); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestConfigsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestConfigsApiClient.java deleted file mode 100644 index 9728defd2..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestConfigsApiClient.java +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.model.Property; -import org.glassfish.jersey.jackson.JacksonFeature; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import java.util.HashMap; -import java.util.List; -import java.util.stream.Collectors; - - -/** - * http://www.hascode.com/2013/12/jax-rs-2-0-rest-client-features-by-example/ - */ -public class RestConfigsApiClient implements ConfigsApiClient { - - Client client = ClientBuilder.newClient().register(JacksonFeature.class); - private final String restServiceUrl; - - public RestConfigsApiClient() { - this("http://localhost:8081"); - } - - public RestConfigsApiClient(String baseUrl) { - this.restServiceUrl = baseUrl + "/configs"; - } - - - @Override - public List getConfigs() { - List> props = client.target(restServiceUrl) - .request() - .get(List.class); - - if (props != null) - return props.stream() - .map(prop -> new Property().key(prop.get("key")).value(prop.get("value")).type(prop.get("type"))) - .collect(Collectors.toList()); - else return null; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestJobsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestJobsApiClient.java deleted file mode 100644 index 973dc7870..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestJobsApiClient.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.model.Job; -import org.glassfish.jersey.jackson.JacksonFeature; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; - - -/** - * http://www.hascode.com/2013/12/jax-rs-2-0-rest-client-features-by-example/ - */ -public class RestJobsApiClient implements JobsApiClient { - - Client client = ClientBuilder.newClient().register(JacksonFeature.class); - private final String restServiceUrl; - private static Logger logger = LoggerFactory.getLogger(RestJobsApiClient.class); - - public RestJobsApiClient() { - this("http://localhost:8081"); - } - - public RestJobsApiClient(String baseUrl) { - - this.restServiceUrl = baseUrl + "/jobs"; - } - - @Override - public Job addJob(Job job) { - return client.target(restServiceUrl) - .request() - .post(Entity.entity(job, MediaType.APPLICATION_JSON), Job.class); - - } - - @Override - public Job getJob(String name) { - - Job job = null; - try { - job = client.target(restServiceUrl) - .path("/{jobId}") - .resolveTemplate("jobId", name) - .request() - .get(Job.class); - } catch (Exception ex) { - logger.debug("unable to get Job {} from REST agent", name); - } - return job; - - } - - @Override - public Integer getJobVersion(String name) { - Job job = getJob(name); - if (job == null) - return -1; - else - return job.getVersion(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestTopicsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestTopicsApiClient.java deleted file mode 100644 index 3e890c807..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/RestTopicsApiClient.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.model.*; -import org.glassfish.jersey.jackson.JacksonFeature; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; -import java.util.Date; - - -/** - * http://www.hascode.com/2013/12/jax-rs-2-0-rest-client-features-by-example/ - */ -public class RestTopicsApiClient implements TopicsApiClient { - - private static Logger logger = LoggerFactory.getLogger(RestTopicsApiClient.class); - - Client client = ClientBuilder.newClient().register(JacksonFeature.class); - private final String restServiceUrl; - - public RestTopicsApiClient() { - this("http://localhost:8081"); - } - - public RestTopicsApiClient(String baseUrl) { - this.restServiceUrl = baseUrl + "/topics"; - } - - @Override - public Topic addTopic(Topic topic) { - return client.target(restServiceUrl) - .request() - .post(Entity.entity(topic, MediaType.APPLICATION_JSON), Topic.class); - - } - - @Override - public Topic getTopic(String name) { - Topic t = null; - try{ - t = client.target(restServiceUrl) - .path("/{topicId}") - .resolveTemplate("topicId", name) - .request() - .get(Topic.class); - }catch (Exception e){ - logger.error("topic {} not found : {}", name, e.toString()); - } - return t; - - } - - public static void main(String[] args) { - RestTopicsApiClient client = new RestTopicsApiClient(); - - Topic t = client.getTopic("aze"); - - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/TopicsApiClient.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/TopicsApiClient.java deleted file mode 100644 index 8f0debd01..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/TopicsApiClient.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client; - -import com.hurence.logisland.agent.rest.client.exceptions.RestClientException; -import com.hurence.logisland.agent.rest.model.Topic; - -public interface TopicsApiClient { - Topic addTopic(Topic topic) throws RestClientException; - - Topic getTopic(String name) throws RestClientException; -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/exceptions/RestClientException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/exceptions/RestClientException.java deleted file mode 100644 index 4c12c3d23..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/rest/client/exceptions/RestClientException.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.rest.client.exceptions; - -public class RestClientException extends Exception { - private final int status; - private final int errorCode; - - public RestClientException(final String message, final int status, final int errorCode) { - super(message + "; error code: " + errorCode); - this.status = status; - this.errorCode = errorCode; - } - - public int getStatus() { - return status; - } - - public int getErrorCode() { - return errorCode; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplication.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplication.java deleted file mode 100644 index 45368201f..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplication.java +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.utils; - - -public class YarnApplication { - - - private String id; - private String name; - private String type; - private String user; - private String yarnQueue; - private String state; - private String finalState; - private String progress; - private String trackingUrl; - - public YarnApplication(String yarnCmdLine) { - // remove multiple withspace from string - String[] tokens = yarnCmdLine.trim().replaceAll(" ", "").split("\t"); - if(tokens.length != 9) - throw new IllegalArgumentException("wrong yarn line : " + yarnCmdLine); - - this.id = tokens[0]; - this.name = tokens[1]; - this.type = tokens[2]; - this.user = tokens[3]; - this.yarnQueue = tokens[4]; - this.state = tokens[5]; - this.finalState = tokens[6]; - this.progress = tokens[7]; - this.trackingUrl = tokens[8]; - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getUser() { - return user; - } - - public void setUser(String user) { - this.user = user; - } - - public String getYarnQueue() { - return yarnQueue; - } - - public void setYarnQueue(String yarnQueue) { - this.yarnQueue = yarnQueue; - } - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public String getFinalState() { - return finalState; - } - - public void setFinalState(String finalState) { - this.finalState = finalState; - } - - public String getProgress() { - return progress; - } - - public void setProgress(String progress) { - this.progress = progress; - } - - public String getTrackingUrl() { - return trackingUrl; - } - - public void setTrackingUrl(String trackingUrl) { - this.trackingUrl = trackingUrl; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplicationWrapper.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplicationWrapper.java deleted file mode 100644 index b07ad00c1..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/agent/utils/YarnApplicationWrapper.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.utils; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - - - -public class YarnApplicationWrapper { - - private static Logger logger = LoggerFactory.getLogger(YarnApplication.class); - private Map apps = new HashMap<>(); - - public YarnApplicationWrapper(String yarnLogs) { - String[] lines = yarnLogs.split("\n"); - for (String line : lines) { - if (line.contains("application_")) { - try { - YarnApplication app = new YarnApplication(line); - apps.put(app.getName(), app); - } catch (Exception e) { - logger.error("unable to parse Yarn log line {} : {}", line, e.toString()); - } - } - } - } - - public YarnApplication getApplication(String name) { - return apps.get(name); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityChecker.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityChecker.java deleted file mode 100644 index 9bb129ec4..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityChecker.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.avro; - -import org.apache.avro.Schema; -import org.apache.avro.SchemaValidationException; -import org.apache.avro.SchemaValidator; -import org.apache.avro.SchemaValidatorBuilder; - -import java.util.ArrayList; -import java.util.List; - -public class AvroCompatibilityChecker { - - // Check if the new schema can be used to read data produced by the latest schema - private static SchemaValidator BACKWARD_VALIDATOR = - new SchemaValidatorBuilder().canReadStrategy().validateLatest(); - public static AvroCompatibilityChecker BACKWARD_CHECKER = new AvroCompatibilityChecker( - BACKWARD_VALIDATOR); - // Check if data produced by the new schema can be read by the latest schema - private static SchemaValidator FORWARD_VALIDATOR = - new SchemaValidatorBuilder().canBeReadStrategy().validateLatest(); - public static AvroCompatibilityChecker FORWARD_CHECKER = new AvroCompatibilityChecker( - FORWARD_VALIDATOR); - // Check if the new schema is both forward and backward compatible with the latest schema - private static SchemaValidator FULL_VALIDATOR = - new SchemaValidatorBuilder().mutualReadStrategy().validateLatest(); - public static AvroCompatibilityChecker FULL_CHECKER = new AvroCompatibilityChecker( - FULL_VALIDATOR); - private static SchemaValidator NO_OP_VALIDATOR = new SchemaValidator() { - @Override - public void validate(Schema schema, Iterable schemas) throws SchemaValidationException { - // do nothing - } - }; - public static AvroCompatibilityChecker NO_OP_CHECKER = new AvroCompatibilityChecker( - NO_OP_VALIDATOR); - private final SchemaValidator validator; - - private AvroCompatibilityChecker(SchemaValidator validator) { - this.validator = validator; - } - - /** - * Check the compatibility between the new schema and the latest schema - */ - public boolean isCompatible(Schema newSchema, Schema latestSchema) { - List schemas = new ArrayList(); - schemas.add(latestSchema); - - try { - validator.validate(newSchema, schemas); - } catch (SchemaValidationException e) { - return false; - } - - return true; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityLevel.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityLevel.java deleted file mode 100644 index 7d3966a62..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroCompatibilityLevel.java +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hurence.logisland.avro; - -public enum AvroCompatibilityLevel { - NONE("NONE", AvroCompatibilityChecker.NO_OP_CHECKER), - BACKWARD("BACKWARD", AvroCompatibilityChecker.BACKWARD_CHECKER), - FORWARD("FORWARD", AvroCompatibilityChecker.FORWARD_CHECKER), - FULL("FULL", AvroCompatibilityChecker.FULL_CHECKER); - - public final String name; - public final AvroCompatibilityChecker compatibilityChecker; - - private AvroCompatibilityLevel(String name, AvroCompatibilityChecker compatibilityChecker) { - this.name = name; - this.compatibilityChecker = compatibilityChecker; - } - - public static AvroCompatibilityLevel forName(String name) { - if (name == null) { - return null; - } - - name = name.toUpperCase(); - if (NONE.name.equals(name)) { - return NONE; - } else if (BACKWARD.name.equals(name)) { - return BACKWARD; - } else if (FORWARD.name.equals(name)) { - return FORWARD; - } else if (FULL.name.equals(name)) { - return FULL; - } else { - return null; - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroSchema.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroSchema.java deleted file mode 100644 index 66ebff505..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroSchema.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.avro; - -import org.apache.avro.Schema; - -public class AvroSchema { - - public final Schema schemaObj; - public final String canonicalString; - - public AvroSchema(Schema schemaObj, String canonicalString) { - this.schemaObj = schemaObj; - this.canonicalString = canonicalString; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroUtils.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroUtils.java deleted file mode 100644 index 94ae48448..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/avro/AvroUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hurence.logisland.avro; - -import org.apache.avro.Schema; -import org.apache.avro.SchemaParseException; - -public class AvroUtils { - - /** - * Convert a schema string into a schema object and a canonical schema string. - * - * @return A schema object and a canonical representation of the schema string. Return null if - * there is any parsing error. - */ - public static AvroSchema parseSchema(String schemaString) { - try { - Schema.Parser parser1 = new Schema.Parser(); - Schema schema = parser1.parse(schemaString); - //TODO: schema.toString() is not canonical (issue-28) - return new AvroSchema(schema, schema.toString()); - } catch (SchemaParseException e) { - return null; - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/component/RestComponentFactory.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/component/RestComponentFactory.java deleted file mode 100644 index 674f200d3..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/component/RestComponentFactory.java +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.component; - -import com.hurence.logisland.agent.rest.client.*; -import com.hurence.logisland.agent.rest.client.exceptions.RestClientException; -import com.hurence.logisland.agent.rest.model.*; -import com.hurence.logisland.engine.EngineContext; -import com.hurence.logisland.engine.ProcessingEngine; -import com.hurence.logisland.engine.StandardEngineContext; -import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.processor.StandardProcessContext; -import com.hurence.logisland.stream.RecordStream; -import com.hurence.logisland.stream.StandardStreamContext; -import com.hurence.logisland.stream.StreamContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.Optional; - - -public final class RestComponentFactory { - - private final JobsApiClient restJobsApiClient; - private final TopicsApiClient topicsApiClient; - private final ConfigsApiClient configsApiClient; - private final String agentQuorum; - - - public RestComponentFactory(String agentQuorum) { - this.restJobsApiClient = new RestJobsApiClient(agentQuorum); - this.topicsApiClient = new RestTopicsApiClient(agentQuorum); - this.configsApiClient = new RestConfigsApiClient(agentQuorum); - this.agentQuorum = agentQuorum; - } - - public RestComponentFactory(JobsApiClient jobsApiClient, TopicsApiClient topicsApiClient, ConfigsApiClient configsApiClient) { - this.restJobsApiClient = jobsApiClient; - this.topicsApiClient = topicsApiClient; - this.configsApiClient = configsApiClient; - this.agentQuorum = "http://localhost:8081"; - } - - private Logger logger = LoggerFactory.getLogger(RestComponentFactory.class); - - - public Optional getEngineContext(String jobName) { - try { - - // get job from api - Job job = restJobsApiClient.getJob(jobName); - if (job != null) { - - - final ProcessingEngine engine = - (ProcessingEngine) Class.forName(job.getEngine().getComponent()).newInstance(); - final EngineContext engineContext = - new StandardEngineContext(engine, job.getId().toString()); - - - // instanciate each related processorChainContext - job.getStreams().forEach(stream -> { - Optional processorChainContext = getStreamContext(stream); - if (processorChainContext.isPresent()) - engineContext.addStreamContext(processorChainContext.get()); - }); - - job.getEngine().getConfig().forEach(e -> engineContext.setProperty(e.getKey(), e.getValue())); - - engineContext.setName(jobName); - logger.info("created engine {}", job.getEngine()); - - return Optional.of(engineContext); - } else - return Optional.empty(); - - - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | RestClientException e) { - logger.error("unable to instanciate job {} : {}", jobName, e); - } - return Optional.empty(); - } - - /** - * Instanciates a stream from of configuration - * - * @param stream - * @return - */ - public Optional getStreamContext(Stream stream) { - try { - final RecordStream recordStream = - (RecordStream) Class.forName(stream.getComponent()).newInstance(); - final StreamContext instance = - new StandardStreamContext(recordStream, stream.getName()); - - // instanciate each related processor - stream.getProcessors().forEach(processor -> { - Optional processorContext = getProcessContext(processor); - if (processorContext.isPresent()) - instance.addProcessContext(processorContext.get()); - }); - - // set the config properties - stream.getConfig().forEach(e -> { - - String key = e.getKey(); - String value = e.getValue(); - switch (key) { - case "kafka.input.topics": { - Topic topic = null; - try { - topic = topicsApiClient.getTopic(value); - if (topic == null) { - logger.error("{} topic was not found", value); - } else { - instance.setProperty("kafka.input.topics.serializer", topic.getSerializer()); - instance.setProperty(key, value); - } - } catch (RestClientException e1) { - logger.error("{} topic was not found", e1.toString()); - } - - break; - } - case "kafka.output.topics": { - try { - Topic topic = topicsApiClient.getTopic(value); - if (topic == null) { - logger.error("{} topic was not found", value); - } else { - instance.setProperty("kafka.output.topics.serializer", topic.getSerializer()); - instance.setProperty(key, value); - } - } catch (RestClientException e1) { - logger.error("{} topic was not found", e1.toString()); - } - break; - } - } - try { - List configs = configsApiClient.getConfigs(); - configs.forEach(conf -> { - instance.setProperty(conf.getKey(), conf.getValue()); - }); - } catch (RestClientException e1) { - logger.error("{} topic was not found", e1.toString()); - } - - instance.setProperty("logisland.agent.quorum", this.agentQuorum); - - }); - logger.info("created stream {}", stream); - return Optional.of(instance); - - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instanciate stream {} : {}", stream, e.toString()); - } - return Optional.empty(); - } - - public Optional getProcessContext(Processor processor) { - try { - final com.hurence.logisland.processor.Processor processorInstance = - (com.hurence.logisland.processor.Processor) Class.forName(processor.getComponent()).newInstance(); - final ProcessContext processContext = - new StandardProcessContext(processorInstance, processor.getName()); - - // set all properties - processor.getConfig().forEach(e -> processContext.setProperty(e.getKey(), e.getValue())); - - logger.info("created processor {}", processor); - return Optional.of(processContext); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instanciate processor {} : {}", processor.getComponent(), e.toString()); - } - - return Optional.empty(); - } - - - public static void main(String[] args) { - RestComponentFactory factory = new RestComponentFactory("http://localhost:8081"); - - factory.getEngineContext("IndexApacheLogsDemo"); - } - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistry.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistry.java deleted file mode 100644 index 5e89f1573..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistry.java +++ /dev/null @@ -1,596 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hurence.logisland.kafka.registry; - -/** - * Copyright 2014 Confluent Inc. - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import com.hurence.logisland.agent.rest.RestService; -import com.hurence.logisland.agent.rest.model.Job; -import com.hurence.logisland.agent.rest.model.JobSummary; -import com.hurence.logisland.agent.rest.model.Topic; -import com.hurence.logisland.kafka.registry.exceptions.RegistryException; -import com.hurence.logisland.kafka.registry.exceptions.RegistryInitializationException; -import com.hurence.logisland.kafka.registry.exceptions.RegistryStoreException; -import com.hurence.logisland.kafka.registry.exceptions.RegistryTimeoutException; -import com.hurence.logisland.kafka.serialization.Serializer; -import com.hurence.logisland.kafka.store.*; -import com.hurence.logisland.kafka.zookeeper.RegistryIdentity; -import com.hurence.logisland.kafka.zookeeper.ZookeeperMasterElector; -import io.confluent.common.metrics.*; -import io.confluent.common.metrics.stats.Gauge; -import io.confluent.common.utils.SystemTime; -import io.confluent.rest.Application; -import io.confluent.rest.RestConfig; -import kafka.utils.ZkUtils; -import org.I0Itec.zkclient.exception.ZkNodeExistsException; -import org.apache.avro.reflect.Nullable; -import org.apache.commons.collections.IteratorUtils; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.common.security.JaasUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import scala.Tuple2; - -import java.net.URI; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.concurrent.TimeUnit; - - -public class KafkaRegistry { - - /** - * Schema versions under a particular subject are indexed from MIN_VERSION. - */ - public static final int ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE = 20; - public static final String ZOOKEEPER_SCHEMA_ID_COUNTER = "/schema_id_counter"; - private static final int ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_WRITE_RETRY_BACKOFF_MS = 50; - private static final Logger log = LoggerFactory.getLogger(KafkaRegistry.class); - - private final RegistryIdentity myIdentity; - private final KafkaRegistryConfig config; - private final Object masterLock = new Object(); - private final String schemaRegistryZkNamespace; - private final String kafkaClusterZkUrl; - private final int zkSessionTimeoutMs; - private final boolean isEligibleForMasterElector; - private String schemaRegistryZkUrl; - private ZkUtils zkUtils; - private RegistryIdentity masterIdentity; - private RestService masterRestService; - private ZookeeperMasterElector masterElector = null; - private Metrics metrics; - private Sensor masterNodeSensor; - - private final KafkaStoreService jobsKafkaStore; - private final KafkaStoreService topicsKafkaStore; - - - // Hand out this id during the next schema registration. Indexed from 1. - private int nextAvailableSchemaId; - // Tracks the upper bound of the current id batch (inclusive). When nextAvailableSchemaId goes - // above this value, it's time to allocate a new batch of ids - private int idBatchInclusiveUpperBound; - // Track the largest id in the kafka store so far (-1 indicates none in the store) - // This is automatically updated by the KafkaStoreReaderThread every time a new Schema is added - // Used to ensure that any newly allocated batch of ids does not overlap - // with any id in the kafkastore. Primarily for bootstrapping the SchemaRegistry when - // data is already in the kafkastore. - private int maxIdInKafkaStore = -1; - - public KafkaRegistry(KafkaRegistryConfig config, - Serializer serializer) - throws RegistryException { - - this.config = config; - - String host = config.getString(KafkaRegistryConfig.HOST_NAME_CONFIG); - int port = getPortForIdentity( - config.getInt(KafkaRegistryConfig.PORT_CONFIG), - config.getList(RestConfig.LISTENERS_CONFIG)); - this.schemaRegistryZkNamespace = config.getString(KafkaRegistryConfig.SCHEMAREGISTRY_ZK_NAMESPACE); - this.isEligibleForMasterElector = config.getBoolean(KafkaRegistryConfig.MASTER_ELIGIBILITY); - this.myIdentity = new RegistryIdentity(host, port, isEligibleForMasterElector); - this.kafkaClusterZkUrl = config.getString(KafkaRegistryConfig.KAFKASTORE_CONNECTION_URL_CONFIG); - this.zkSessionTimeoutMs = config.getInt(KafkaRegistryConfig.KAFKASTORE_ZK_SESSION_TIMEOUT_MS_CONFIG); - - - /** - * setup 2 stores for jobs and topics - */ - jobsKafkaStore = - new KafkaStoreService(this, KafkaRegistryConfig.KAFKASTORE_TOPIC_JOBS_CONFIG, config, serializer); - topicsKafkaStore = - new KafkaStoreService(this, KafkaRegistryConfig.KAFKASTORE_TOPIC_TOPICS_CONFIG, config, serializer); - - - /** - * metrology section - */ - MetricConfig metricConfig = - new MetricConfig().samples(config.getInt(ProducerConfig.METRICS_NUM_SAMPLES_CONFIG)) - .timeWindow(config.getLong(ProducerConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), - TimeUnit.MILLISECONDS); - List reporters = - config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG, - MetricsReporter.class); - String jmxPrefix = "logisland.kafka.registry"; - reporters.add(new JmxReporter(jmxPrefix)); - this.metrics = new Metrics(metricConfig, reporters, new SystemTime()); - this.masterNodeSensor = metrics.sensor("master-slave-role"); - MetricName m = new MetricName("master-slave-role", "master-slave-role", - "1.0 indicates the node is the active master in the cluster and is the" - + " node where all register schema and config update requests are " - + "served."); - this.masterNodeSensor.add(m, new Gauge()); - } - - - //////////////////////////////////////////////////// - // Topics management - //////////////////////////////////////////////////// - public Topic addTopic(Topic topic) throws RegistryException { - - - Long topicId = topic.getId(); - if (topicId != null && topicId >= 0) { - topic.setId(topicId); - } else { - topic.setId((long) nextAvailableSchemaId); - topic.setVersion(1); - nextAvailableSchemaId++; - } - if (reachedEndOfIdBatch()) { - idBatchInclusiveUpperBound = getInclusiveUpperBound(nextSchemaIdCounterBatch()); - } - - topic.dateModified(new Date()); - - - TopicKey key = new TopicKey(topic.getName(), 1); - TopicValue value = new TopicValue(topic.getName(), 1, topic.getId(), topic); - - TopicValue topicValue = (TopicValue) topicsKafkaStore.create(key, value); - if (topicValue == null) - throw new RegistryException("unbale to create Topic " + topic); - - return topicValue.getTopic(); - } - - public Topic updateTopic(Topic topic) throws RegistryException { - topic.version(topic.getVersion() + 1).dateModified(new Date()); - - TopicKey key = new TopicKey(topic.getName(), 1); - TopicValue value = new TopicValue(topic.getName(), topic.getVersion(), topic.getId(), topic); - - TopicValue topicValue = (TopicValue) topicsKafkaStore.update(key, value); - if (topicValue == null) - throw new RegistryException("unable to update Topic " + topic); - - return topicValue.getTopic(); - } - - public Topic getTopic(String topicId) throws RegistryException { - - TopicKey key = new TopicKey(topicId, 1); - TopicValue value = (TopicValue) topicsKafkaStore.get(key); - if (value == null) - throw new RegistryException("Topic " + topicId + " not found"); - return value.getTopic(); - } - - public void deleteTopic(String topicId) throws RegistryException { - - TopicKey key = new TopicKey(topicId, 1); - topicsKafkaStore.delete(key); - } - - public List getAllTopics() throws RegistryException { - return IteratorUtils.toList( - topicsKafkaStore.getAll() - .stream() - .map(value -> ((TopicValue) value).getTopic()) - .iterator() - ); - } - - - //////////////////////////////////////////////////// - // Job management - //////////////////////////////////////////////////// - public Job addJob(Job job) throws RegistryException { - - - Long jobId = job.getId(); - if (jobId != null && jobId >= 0) { - job.setId(jobId); - } else { - job.setId((long) nextAvailableSchemaId); - job.setVersion(1); - nextAvailableSchemaId++; - } - if (reachedEndOfIdBatch()) { - idBatchInclusiveUpperBound = getInclusiveUpperBound(nextSchemaIdCounterBatch()); - } - - if (job.getSummary() == null) - job.setSummary(new JobSummary()); - job.getSummary().dateModified(new Date()); - - - JobKey key = new JobKey(job.getName(), 1); - JobValue value = new JobValue(job.getName(), job.getVersion(), job.getId(), job); - - JobValue jobAdded = (JobValue) jobsKafkaStore.create(key, value); - if (jobAdded == null) - throw new RegistryException("unbale to create Job " + job); - - return jobAdded.getJob(); - } - - public Job updateJob(Job job) throws RegistryException { - job.version(job.getVersion() + 1); - - if (job.getSummary() == null) - job.setSummary(new JobSummary()); - job.getSummary().dateModified(new Date()); - - JobKey key = new JobKey(job.getName(), 1); - JobValue value = new JobValue(job.getName(), job.getVersion(), job.getId(), job); - - JobValue jobAdded = (JobValue) jobsKafkaStore.update(key, value); - if (jobAdded == null) - throw new RegistryException("unable to update Job " + job); - - return jobAdded.getJob(); - } - - public Job getJob(String jobId) throws RegistryException { - - JobKey key = new JobKey(jobId, 1); - JobValue value = (JobValue) jobsKafkaStore.get(key); - if (value == null) - throw new RegistryException("Job " + jobId + " not found"); - return value.getJob(); - } - - public void deleteJob(String jobId) throws RegistryException { - - JobKey key = new JobKey(jobId, 1); - jobsKafkaStore.delete(key); - } - - public List getAllJobs() throws RegistryException { - return IteratorUtils.toList( - jobsKafkaStore.getAll() - .stream() - .map(value -> ((JobValue) value).getJob()) - .iterator() - ); - } - - /** - * A Schema Registry instance's identity is in part the port it listens on. Currently the - * port can either be configured via the deprecated `port` configuration, or via the `listeners` - * configuration. - *

- * This method uses `Application.parseListeners()` from `rest-utils` to get a list of listeners, and - * returns the port of the first listener to be used for the instance's identity. - *

- * In theory, any port from any listener would be sufficient. Choosing the first, instead of say the last, - * is arbitrary. - */ - // TODO: once RestConfig.PORT_CONFIG is deprecated, remove the port parameter. - public static int getPortForIdentity(int port, List configuredListeners) { - List listeners = Application.parseListeners(configuredListeners, port, - Arrays.asList("http", "https"), "http"); - return listeners.get(0).getPort(); - } - - - public void init() throws RegistryInitializationException { - try { - jobsKafkaStore.init(); - topicsKafkaStore.init(); - } catch (Exception e) { - throw new RegistryInitializationException( - "Error initializing kafka store while initializing schema registry", e); - } - - try { - createZkNamespace(); - masterElector = new ZookeeperMasterElector(zkUtils, myIdentity, this, - isEligibleForMasterElector); - } catch (RegistryStoreException e) { - throw new RegistryInitializationException( - "Error electing master while initializing schema registry", e); - } catch (RegistryTimeoutException e) { - throw new RegistryInitializationException(e); - } - } - - private void createZkNamespace() { - int kafkaNamespaceIndex = kafkaClusterZkUrl.indexOf("/"); - String zkConnForNamespaceCreation = kafkaNamespaceIndex > 0 ? - kafkaClusterZkUrl.substring(0, kafkaNamespaceIndex) : - kafkaClusterZkUrl; - - String schemaRegistryNamespace = "/" + schemaRegistryZkNamespace; - schemaRegistryZkUrl = zkConnForNamespaceCreation + schemaRegistryNamespace; - - ZkUtils zkUtilsForNamespaceCreation = ZkUtils.apply( - zkConnForNamespaceCreation, - zkSessionTimeoutMs, zkSessionTimeoutMs, - JaasUtils.isZkSecurityEnabled()); - // create the zookeeper namespace using cluster.name if it doesn't already exist - zkUtilsForNamespaceCreation.makeSurePersistentPathExists( - schemaRegistryNamespace, - zkUtilsForNamespaceCreation.DefaultAcls()); - log.info("Created schema registry namespace " + - zkConnForNamespaceCreation + schemaRegistryNamespace); - zkUtilsForNamespaceCreation.close(); - this.zkUtils = ZkUtils.apply( - schemaRegistryZkUrl, zkSessionTimeoutMs, zkSessionTimeoutMs, - JaasUtils.isZkSecurityEnabled()); - } - - public ZkUtils zkUtils() { - return zkUtils; - } - - public boolean isMaster() { - synchronized (masterLock) { - if (masterIdentity != null && masterIdentity.equals(myIdentity)) { - return true; - } else { - return false; - } - } - } - - /** - * 'Inform' this SchemaRegistry instance which SchemaRegistry is the current master. - * If this instance is set as the new master, ensure it is up-to-date with data in - * the kafka store, and tell Zookeeper to allocate the next batch of schema IDs. - * - * @param newMaster Identity of the current master. null means no master is alive. - * @throws RegistryException - */ - public void setMaster(@Nullable RegistryIdentity newMaster) - throws RegistryTimeoutException, RegistryStoreException { - log.debug("Setting the master to " + newMaster); - - // Only schema registry instances eligible for master can be set to master - if (newMaster != null && !newMaster.getMasterEligibility()) { - throw new IllegalStateException( - "Tried to set an ineligible node to master: " + newMaster); - } - - synchronized (masterLock) { - RegistryIdentity previousMaster = masterIdentity; - masterIdentity = newMaster; - - if (masterIdentity == null) { - masterRestService = null; - } else { - masterRestService = new RestService(String.format("http://%s:%d", - masterIdentity.getHost(), masterIdentity.getPort())); - } - - if (masterIdentity != null && !masterIdentity.equals(previousMaster) && isMaster()) { - nextAvailableSchemaId = nextSchemaIdCounterBatch(); - idBatchInclusiveUpperBound = getInclusiveUpperBound(nextAvailableSchemaId); - - // The new master may not know the exact last offset in the Kafka log. So, mark the - // last offset invalid here and let the logic in register() deal with it later. - //jobsKafkaStore.markLastWrittenOffsetInvalid(); - //throw new SchemaRegistryStoreException("TODO handle here jobsKafkaStore.markLastWrittenOffsetInvalid();"); - } - - masterNodeSensor.record(isMaster() ? 1.0 : 0.0); - } - } - - /** - * Return json data encoding basic information about this SchemaRegistry instance, such as - * host, port, etc. - */ - public RegistryIdentity myIdentity() { - return myIdentity; - } - - /** - * Return the identity of the SchemaRegistry that this instance thinks is current master. - * Any request that requires writing new data gets forwarded to the master. - */ - public RegistryIdentity masterIdentity() { - synchronized (masterLock) { - return masterIdentity; - } - } - - - public void close() { - log.info("Shutting down schema registry"); - jobsKafkaStore.close(); - topicsKafkaStore.close(); - if (masterElector != null) { - masterElector.close(); - } - if (zkUtils != null) { - zkUtils.close(); - } - } - - /** - * Allocate and lock the next batch of schema ids. Signal a global lock over the next batch by - * writing the inclusive upper bound of the batch to ZooKeeper. I.e. the value stored in - * ZOOKEEPER_SCHEMA_ID_COUNTER in ZooKeeper indicates the current max allocated id for assignment. - *

- * When a schema registry server is initialized, kafka may have preexisting persistent - * schema -> id assignments, and zookeeper may have preexisting counter data. - * Therefore, when allocating the next batch of ids, it's necessary to ensure the entire new batch - * is greater than the greatest id in kafka and also greater than the previously recorded batch - * in zookeeper. - *

- * Return the first available id in the newly allocated batch of ids. - */ - protected Integer nextSchemaIdCounterBatch() throws RegistryStoreException { - int nextIdBatchLowerBound = 1; - - while (true) { - - if (!zkUtils.zkClient().exists(ZOOKEEPER_SCHEMA_ID_COUNTER)) { - // create ZOOKEEPER_SCHEMA_ID_COUNTER if it already doesn't exist - - try { - nextIdBatchLowerBound = getNextBatchLowerBoundFromKafkaStore(); - int nextIdBatchUpperBound = getInclusiveUpperBound(nextIdBatchLowerBound); - zkUtils.createPersistentPath(ZOOKEEPER_SCHEMA_ID_COUNTER, - String.valueOf(nextIdBatchUpperBound), - zkUtils.DefaultAcls()); - return nextIdBatchLowerBound; - } catch (ZkNodeExistsException ignore) { - // A zombie master may have created this zk node after the initial existence check - // Ignore and try again - } - } else { // ZOOKEEPER_SCHEMA_ID_COUNTER exists - - // read the latest counter value - final Tuple2 counterValue = zkUtils.readData(ZOOKEEPER_SCHEMA_ID_COUNTER); - final String counterData = counterValue._1(); - final org.apache.zookeeper.data.Stat counterStat = counterValue._2(); - if (counterData == null) { - throw new RegistryStoreException( - "Failed to read schema id counter " + ZOOKEEPER_SCHEMA_ID_COUNTER + - " from zookeeper"); - } - - // Compute the lower bound of next id batch based on zk data and kafkastore data - int zkIdCounterValue = Integer.valueOf(counterData); - int zkNextIdBatchLowerBound = zkIdCounterValue + 1; - if (zkIdCounterValue % ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE != 0) { - // ZooKeeper id counter should be an integer multiple of id batch size in normal - // operation; handle corrupted/stale id counter data gracefully by bumping - // up to the next id batch - - // fixedZkIdCounterValue is the smallest multiple of - // ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE greater than the bad zkIdCounterValue - int fixedZkIdCounterValue = ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE * - (1 + zkIdCounterValue / ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE); - zkNextIdBatchLowerBound = fixedZkIdCounterValue + 1; - - log.warn( - "Zookeeper schema id counter is not an integer multiple of id batch size." + - " Zookeeper may have stale id counter data.\n" + - "zk id counter: " + zkIdCounterValue + "\n" + - "id batch size: " + ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE); - } - nextIdBatchLowerBound = - Math.max(zkNextIdBatchLowerBound, getNextBatchLowerBoundFromKafkaStore()); - String nextIdBatchUpperBound = String.valueOf(getInclusiveUpperBound(nextIdBatchLowerBound)); - - // conditionally update the zookeeper path with the upper bound of the new id batch. - // newSchemaIdCounterDataVersion < 0 indicates a failed conditional update. - // Most probable cause is the existence of another master which tries to do the same - // counter batch allocation at the same time. If this happens, re-read the value and - // continue until one master is determined to be the zombie master. - // NOTE: The handling of multiple masters is still a TODO - int newSchemaIdCounterDataVersion = - (Integer) zkUtils.conditionalUpdatePersistentPath( - ZOOKEEPER_SCHEMA_ID_COUNTER, - nextIdBatchUpperBound, - counterStat.getVersion(), - null)._2(); - if (newSchemaIdCounterDataVersion >= 0) { - break; - } - } - try { - // Wait a bit and attempt id batch allocation again - Thread.sleep(ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_WRITE_RETRY_BACKOFF_MS); - } catch (InterruptedException ignored) { - } - } - - return nextIdBatchLowerBound; - } - - int getMaxIdInKafkaStore() { - return this.maxIdInKafkaStore; - } - - /** - * This should only be updated by the KafkastoreReaderThread. - */ - void setMaxIdInKafkaStore(int id) { - this.maxIdInKafkaStore = id; - } - - /** - * If true, it's time to allocate a new batch of ids with a call to nextSchemaIdCounterBatch() - */ - private boolean reachedEndOfIdBatch() { - return nextAvailableSchemaId > idBatchInclusiveUpperBound; - } - - - /** - * E.g. if inclusiveLowerBound is 61, and BATCH_SIZE is 20, the inclusiveUpperBound should be 80. - */ - private int getInclusiveUpperBound(int inclusiveLowerBound) { - return inclusiveLowerBound + ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE - 1; - } - - - /** - * Return a minimum lower bound on the next batch of ids based on ids currently in the - * kafka store. - */ - private int getNextBatchLowerBoundFromKafkaStore() { - if (this.getMaxIdInKafkaStore() <= 0) { - return 1; - } - - int nextBatchLowerBound = 1 + this.getMaxIdInKafkaStore() / ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE; - return 1 + nextBatchLowerBound * ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE; - } - - - public KafkaRegistryConfig getConfig() { - return config; - } - - public KafkaStore getJobsKafkaStore() { - return this.jobsKafkaStore.getKafkaStore(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryConfig.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryConfig.java deleted file mode 100644 index c6569fec9..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryConfig.java +++ /dev/null @@ -1,402 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry; - -import com.hurence.logisland.agent.rest.Versions; -import com.hurence.logisland.avro.AvroCompatibilityLevel; -import io.confluent.common.config.ConfigDef; -import io.confluent.common.config.ConfigException; -import io.confluent.rest.RestConfig; -import io.confluent.rest.RestConfigException; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Map; -import java.util.Properties; - -import static io.confluent.common.config.ConfigDef.Range.atLeast; - -public class KafkaRegistryConfig extends RestConfig { - - private static final int SCHEMAREGISTRY_PORT_DEFAULT = 8081; - // TODO: change this to "http://0.0.0.0:8081" when PORT_CONFIG is deleted. - private static final String SCHEMAREGISTRY_LISTENERS_DEFAULT = ""; - - public static final String KAFKASTORE_SECURITY_PROTOCOL_SSL = "SSL"; - public static final String KAFKASTORE_SECURITY_PROTOCOL_PLAINTEXT = "PLAINTEXT"; - - public static final String KAFKASTORE_CONNECTION_URL_CONFIG = "kafkastore.connection.url"; - public static final String KAFKASTORE_BOOTSTRAP_SERVERS_CONFIG = "kafkastore.bootstrap.servers"; - /** - * kafkastore.zk.session.timeout.ms - */ - public static final String KAFKASTORE_ZK_SESSION_TIMEOUT_MS_CONFIG - = "kafkastore.zk.session.timeout.ms"; - - /** - * kafkastore.topic.topics - */ - public static final String KAFKASTORE_TOPIC_TOPICS_CONFIG = "kafkastore.topic.topics"; - public static final String DEFAULT_KAFKASTORE_TOPIC_TOPICS = "_topics"; - - /** - * kafkastore.topic.jobs - */ - public static final String KAFKASTORE_TOPIC_JOBS_CONFIG = "kafkastore.topic.jobs"; - public static final String DEFAULT_KAFKASTORE_TOPIC_JOBS = "_jobs"; - - - /** - * kafkastore.topic.metrics - */ - public static final String KAFKASTORE_TOPIC_METRICS_CONFIG = "kafkastore.topic.metrics"; - public static final String DEFAULT_KAFKASTORE_TOPIC_METRICS = "_metrics"; - - - /** - * kafka.metadata.broker.list - */ - public static final String KAFKA_METADATA_BROKER_LIST_CONFIG = "kafka.metadata.broker.list"; - public static final String DEFAULT_KAFKA_METADATA_BROKER_LIST = "localhost:9092"; - protected static final String KAFKA_METADATA_BROKER_LIST_DOC = - "The list of kafka brokers host1:port1,host2:port2"; - - /** - * kafka.zookeeper.quorum - */ - public static final String KAFKA_ZOOKEEPER_QUORUM_CONFIG = "kafka.zookeeper.quorum"; - public static final String DEFAULT_KAFKA_ZOOKEEPER_QUORUM = "localhost:2181"; - protected static final String KAFKA_ZOOKEEPER_QUORUM_DOC = - "The list of zookeeper nodes host1:port1,host2:port2"; - /** - * kafka.zookeeper.quorum - */ - public static final String KAFKA_TOPIC_AUTOCREATE_CONFIG = "kafka.topic.autoCreate"; - public static final boolean DEFAULT_KAFKA_TOPIC_AUTOCREATE = true; - protected static final String KAFKA_TOPIC_AUTOCREATE_DOC = - "do we create the topic if not exists ?"; - - /** - * kafka.topic.default.partition - */ - public static final String KAFKA_TOPIC_DEFAULT_PARTITION_CONFIG = "kafka.topic.default.partition"; - public static final int DEFAULT_KAFKA_TOPIC_DEFAULT_PARTITION = 4; - protected static final String KAFKA_TOPIC_DEFAULT_PARTITION_DOC = - "the default number of partition per topic"; - /** - * kafka.topic.default.replicationFactor - */ - public static final String KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR_CONFIG = "kafka.topic.default.replicationFactor"; - public static final int DEFAULT_KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR = 1; - protected static final String KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR_DOC = - "the default number of replica for a topic"; - - - - - - /** - * kafkastore.topic.replication.factor - */ - public static final String KAFKASTORE_TOPIC_REPLICATION_FACTOR_CONFIG = - "kafkastore.topic.replication.factor"; - public static final int DEFAULT_KAFKASTORE_TOPIC_REPLICATION_FACTOR = 3; - /** - * kafkastore.timeout.ms - */ - public static final String KAFKASTORE_TIMEOUT_CONFIG = "kafkastore.timeout.ms"; - /** - * kafkastore.init.timeout.ms - */ - public static final String KAFKASTORE_INIT_TIMEOUT_CONFIG = "kafkastore.init.timeout.ms"; - - /** - * master.eligibility* - */ - public static final String MASTER_ELIGIBILITY = "master.eligibility"; - public static final boolean DEFAULT_MASTER_ELIGIBILITY = true; - /** - * schema.registry.zk.name* - */ - public static final String SCHEMAREGISTRY_ZK_NAMESPACE = "schema.registry.zk.namespace"; - public static final String DEFAULT_SCHEMAREGISTRY_ZK_NAMESPACE = "schema_registry"; - /** - * host.name - */ - public static final String HOST_NAME_CONFIG = "host.name"; - /** - * avro.compatibility.level - */ - public static final String COMPATIBILITY_CONFIG = "avro.compatibility.level"; - public static final String KAFKASTORE_SECURITY_PROTOCOL_CONFIG = - "kafkastore.security.protocol"; - public static final String KAFKASTORE_SSL_TRUSTSTORE_LOCATION_CONFIG = - "kafkastore.ssl.truststore.location"; - public static final String KAFKASTORE_SSL_TRUSTSTORE_PASSWORD_CONFIG = - "kafkastore.ssl.truststore.password"; - public static final String KAFKASTORE_SSL_KEYSTORE_LOCATION_CONFIG = - "kafkastore.ssl.keystore.location"; - public static final String KAFKASTORE_SSL_TRUSTSTORE_TYPE_CONFIG = - "kafkastore.ssl.truststore.type"; - public static final String KAFKASTORE_SSL_TRUSTMANAGER_ALGORITHM_CONFIG = - "kafkastore.ssl.trustmanager.algorithm"; - public static final String KAFKASTORE_SSL_KEYSTORE_PASSWORD_CONFIG = - "kafkastore.ssl.keystore.password"; - public static final String KAFKASTORE_SSL_KEYSTORE_TYPE_CONFIG = - "kafkastore.ssl.keystore.type"; - public static final String KAFKASTORE_SSL_KEYMANAGER_ALGORITHM_CONFIG = - "kafkastore.ssl.keymanager.algorithm"; - public static final String KAFKASTORE_SSL_KEY_PASSWORD_CONFIG = - "kafkastore.ssl.key.password"; - public static final String KAFKASTORE_SSL_ENABLED_PROTOCOLS_CONFIG = - "kafkastore.ssl.enabled.protocols"; - public static final String KAFKASTORE_SSL_PROTOCOL_CONFIG = - "kafkastore.ssl.protocol"; - public static final String KAFKASTORE_SSL_PROVIDER_CONFIG = - "kafkastore.ssl.provider"; - public static final String KAFKASTORE_SSL_CIPHER_SUITES_CONFIG = - "kafkastore.ssl.cipher.suites"; - public static final String KAFKASTORE_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG = - "kafkastore.ssl.endpoint.identification.algorithm"; - protected static final String KAFKASTORE_CONNECTION_URL_DOC = - "Zookeeper url for the Kafka cluster"; - protected static final String KAFKASTORE_BOOTSTRAP_SERVERS_DOC = - "A list of Kafka brokers to connect to. For example, `PLAINTEXT://hostname:9092,SSL://hostname2:9092`\n" - + "\n" - + "If this configuration is not specified, the Schema Registry's internal Kafka clients will get their Kafka bootstrap server list\n" - + "from ZooKeeper (configured with `kafkastore.connection.url`). Note that if `kafkastore.bootstrap.servers` is configured,\n" - + "`kafkastore.connection.url` still needs to be configured, too.\n" - + "\n" - + "This configuration is particularly important when Kafka security is enabled, because Kafka may expose multiple endpoints that\n" - + "all will be stored in ZooKeeper, but the Schema Registry may need to be configured with just one of those endpoints."; - protected static final String SCHEMAREGISTRY_ZK_NAMESPACE_DOC = - "The string that is used as the zookeeper namespace for storing schema registry " - + "metadata. SchemaRegistry instances which are part of the same schema registry service " - + "should have the same ZooKeeper namespace."; - protected static final String KAFKASTORE_ZK_SESSION_TIMEOUT_MS_DOC = - "Zookeeper session timeout"; - protected static final String KAFKASTORE_TOPIC_DOC = - "The durable single partition topic that acts" + - "as the durable log for the data"; - protected static final String KAFKASTORE_TOPIC_REPLICATION_FACTOR_DOC = - "The desired replication factor of the schema topic. The actual replication factor " + - "will be the smaller of this value and the number of live Kafka brokers."; - protected static final String KAFKASTORE_WRITE_RETRIES_DOC = - "Retry a failed register schema request to the underlying Kafka store up to this many times, " - + " for example in case of a Kafka broker failure"; - protected static final String KAFKASTORE_WRITE_RETRY_BACKOFF_MS_DOC = - "The amount of time in milliseconds to wait before attempting to retry a failed write " - + "to the Kafka store"; - protected static final String KAFKASTORE_INIT_TIMEOUT_DOC = - "The timeout for initialization of the Kafka store, including creation of the Kafka topic " - + "that stores schema data."; - protected static final String KAFKASTORE_TIMEOUT_DOC = - "The timeout for an operation on the Kafka store"; - protected static final String HOST_DOC = - "The host name advertised in Zookeeper. Make sure to set this if running SchemaRegistry " - + "with multiple nodes."; - protected static final String COMPATIBILITY_DOC = - "The Avro compatibility type. Valid values are: " - + "none (new schema can be any valid Avro schema), " - + "backward (new schema can read data produced by latest registered schema), " - + "forward (latest registered schema can read data produced by the new schema), " - + "full (new schema is backward and forward compatible with latest registered schema)"; - protected static final String MASTER_ELIGIBILITY_DOC = - "If true, this node can participate in master election. In a multi-colo setup, turn this off " - + "for clusters in the slave data center."; - protected static final String KAFKASTORE_SECURITY_PROTOCOL_DOC = - "The security protocol to use when connecting with Kafka, the underlying persistent storage. " - + "Values can be `PLAINTEXT` or `SSL`."; - protected static final String KAFKASTORE_SSL_TRUSTSTORE_LOCATION_DOC = - "The location of the SSL trust store file."; - protected static final String KAFKASTORE_SSL_TRUSTSTORE_PASSWORD_DOC = - "The password to access the trust store."; - protected static final String KAFAKSTORE_SSL_TRUSTSTORE_TYPE_DOC = - "The file format of the trust store."; - protected static final String KAFKASTORE_SSL_TRUSTMANAGER_ALGORITHM_DOC = - "The algorithm used by the trust manager factory for SSL connections."; - protected static final String KAFKASTORE_SSL_KEYSTORE_LOCATION_DOC = - "The location of the SSL keystore file."; - protected static final String KAFKASTORE_SSL_KEYSTORE_PASSWORD_DOC = - "The password to access the keystore."; - protected static final String KAFAKSTORE_SSL_KEYSTORE_TYPE_DOC = - "The file format of the keystore."; - protected static final String KAFKASTORE_SSL_KEYMANAGER_ALGORITHM_DOC = - "The algorithm used by key manager factory for SSL connections."; - protected static final String KAFKASTORE_SSL_KEY_PASSWORD_DOC = - "The password of the key contained in the keystore."; - protected static final String KAFAKSTORE_SSL_ENABLED_PROTOCOLS_DOC = - "Protocols enabled for SSL connections."; - protected static final String KAFAKSTORE_SSL_PROTOCOL_DOC = - "The SSL protocol used."; - protected static final String KAFAKSTORE_SSL_PROVIDER_DOC = - "The name of the security provider used for SSL."; - protected static final String KAFKASTORE_SSL_CIPHER_SUITES_DOC = - "A list of cipher suites used for SSL."; - protected static final String KAFKASTORE_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_DOC = - "The endpoint identification algorithm to validate the server hostname using the server certificate."; - private static final String COMPATIBILITY_DEFAULT = "backward"; - private static final String METRICS_JMX_PREFIX_DEFAULT_OVERRIDE = "kafka.schema.registry"; - - // TODO: move to Apache's ConfigDef - private static final ConfigDef config; - - static { - config = baseConfigDef() - .defineOverride(PORT_CONFIG, ConfigDef.Type.INT, SCHEMAREGISTRY_PORT_DEFAULT, - ConfigDef.Importance.LOW, PORT_CONFIG_DOC) - .defineOverride(LISTENERS_CONFIG, ConfigDef.Type.LIST, SCHEMAREGISTRY_LISTENERS_DEFAULT, - ConfigDef.Importance.HIGH, LISTENERS_DOC + "\n\n" + - "Schema Registry identities are stored in ZooKeeper and are made up of a hostname and port. " + - "If multiple listeners are configured, the first listener's port is used for its identity.") - .defineOverride(RESPONSE_MEDIATYPE_PREFERRED_CONFIG, ConfigDef.Type.LIST, - Versions.PREFERRED_RESPONSE_TYPES, - ConfigDef.Importance.HIGH, - RESPONSE_MEDIATYPE_PREFERRED_CONFIG_DOC) - .defineOverride(RESPONSE_MEDIATYPE_DEFAULT_CONFIG, ConfigDef.Type.STRING, - Versions.KAFKA_REGISTRY_MOST_SPECIFIC_DEFAULT, - ConfigDef.Importance.HIGH, - RESPONSE_MEDIATYPE_DEFAULT_CONFIG_DOC) - .define(KAFKASTORE_CONNECTION_URL_CONFIG, ConfigDef.Type.STRING, ConfigDef.Importance.HIGH, - KAFKASTORE_CONNECTION_URL_DOC) - .define(KAFKASTORE_BOOTSTRAP_SERVERS_CONFIG, ConfigDef.Type.LIST, "", ConfigDef.Importance.MEDIUM, - KAFKASTORE_CONNECTION_URL_DOC) - .define(SCHEMAREGISTRY_ZK_NAMESPACE, ConfigDef.Type.STRING, - DEFAULT_SCHEMAREGISTRY_ZK_NAMESPACE, - ConfigDef.Importance.LOW, SCHEMAREGISTRY_ZK_NAMESPACE_DOC) - .define(KAFKASTORE_ZK_SESSION_TIMEOUT_MS_CONFIG, ConfigDef.Type.INT, 30000, atLeast(0), - ConfigDef.Importance.LOW, KAFKASTORE_ZK_SESSION_TIMEOUT_MS_DOC) - .define(KAFKASTORE_TOPIC_TOPICS_CONFIG, ConfigDef.Type.STRING, DEFAULT_KAFKASTORE_TOPIC_TOPICS, - ConfigDef.Importance.HIGH, KAFKASTORE_TOPIC_DOC) - .define(KAFKASTORE_TOPIC_METRICS_CONFIG, ConfigDef.Type.STRING, DEFAULT_KAFKASTORE_TOPIC_METRICS, - ConfigDef.Importance.MEDIUM, KAFKASTORE_TOPIC_DOC) - .define(KAFKA_METADATA_BROKER_LIST_CONFIG, ConfigDef.Type.STRING, DEFAULT_KAFKA_METADATA_BROKER_LIST, - ConfigDef.Importance.HIGH, KAFKA_METADATA_BROKER_LIST_DOC) - .define(KAFKA_TOPIC_AUTOCREATE_CONFIG, ConfigDef.Type.BOOLEAN, DEFAULT_KAFKA_TOPIC_AUTOCREATE, - ConfigDef.Importance.HIGH, KAFKA_TOPIC_AUTOCREATE_DOC) - .define(KAFKA_TOPIC_DEFAULT_PARTITION_CONFIG, ConfigDef.Type.INT, DEFAULT_KAFKA_TOPIC_DEFAULT_PARTITION, - ConfigDef.Importance.HIGH, KAFKA_TOPIC_DEFAULT_PARTITION_DOC) - .define(KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR_CONFIG, ConfigDef.Type.INT, DEFAULT_KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR, - ConfigDef.Importance.HIGH, KAFKA_TOPIC_DEFAULT_REPLICATION_FACTOR_DOC) - .define(KAFKA_ZOOKEEPER_QUORUM_CONFIG, ConfigDef.Type.STRING, DEFAULT_KAFKA_ZOOKEEPER_QUORUM, - ConfigDef.Importance.HIGH, KAFKA_ZOOKEEPER_QUORUM_DOC) - - .define(KAFKASTORE_TOPIC_JOBS_CONFIG, ConfigDef.Type.STRING, DEFAULT_KAFKASTORE_TOPIC_JOBS, - ConfigDef.Importance.HIGH, KAFKASTORE_TOPIC_DOC) - .define(KAFKASTORE_TOPIC_REPLICATION_FACTOR_CONFIG, ConfigDef.Type.INT, - DEFAULT_KAFKASTORE_TOPIC_REPLICATION_FACTOR, - ConfigDef.Importance.HIGH, KAFKASTORE_TOPIC_REPLICATION_FACTOR_DOC) - .define(KAFKASTORE_INIT_TIMEOUT_CONFIG, ConfigDef.Type.INT, 60000, atLeast(0), - ConfigDef.Importance.MEDIUM, KAFKASTORE_INIT_TIMEOUT_DOC) - .define(KAFKASTORE_TIMEOUT_CONFIG, ConfigDef.Type.INT, 500, atLeast(0), - ConfigDef.Importance.MEDIUM, KAFKASTORE_TIMEOUT_DOC) - .define(HOST_NAME_CONFIG, ConfigDef.Type.STRING, getDefaultHost(), - ConfigDef.Importance.HIGH, HOST_DOC) - .define(COMPATIBILITY_CONFIG, ConfigDef.Type.STRING, COMPATIBILITY_DEFAULT, - ConfigDef.Importance.HIGH, COMPATIBILITY_DOC) - .define(MASTER_ELIGIBILITY, ConfigDef.Type.BOOLEAN, DEFAULT_MASTER_ELIGIBILITY, - ConfigDef.Importance.MEDIUM, MASTER_ELIGIBILITY_DOC) - .defineOverride(METRICS_JMX_PREFIX_CONFIG, ConfigDef.Type.STRING, - METRICS_JMX_PREFIX_DEFAULT_OVERRIDE, ConfigDef.Importance.LOW, - METRICS_JMX_PREFIX_DOC) - .define(KAFKASTORE_SECURITY_PROTOCOL_CONFIG, ConfigDef.Type.STRING, - KAFKASTORE_SECURITY_PROTOCOL_PLAINTEXT, ConfigDef.Importance.MEDIUM, - KAFKASTORE_SECURITY_PROTOCOL_DOC) - .define(KAFKASTORE_SSL_TRUSTSTORE_LOCATION_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.HIGH, - KAFKASTORE_SSL_TRUSTSTORE_LOCATION_DOC) - .define(KAFKASTORE_SSL_TRUSTSTORE_PASSWORD_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.HIGH, - KAFKASTORE_SSL_TRUSTSTORE_PASSWORD_DOC) - .define(KAFKASTORE_SSL_TRUSTSTORE_TYPE_CONFIG, ConfigDef.Type.STRING, - "JKS", ConfigDef.Importance.MEDIUM, - KAFAKSTORE_SSL_TRUSTSTORE_TYPE_DOC) - .define(KAFKASTORE_SSL_TRUSTMANAGER_ALGORITHM_CONFIG, ConfigDef.Type.STRING, - "PKIX", ConfigDef.Importance.LOW, - KAFKASTORE_SSL_TRUSTMANAGER_ALGORITHM_DOC) - .define(KAFKASTORE_SSL_KEYSTORE_LOCATION_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.HIGH, - KAFKASTORE_SSL_KEYSTORE_LOCATION_DOC) - .define(KAFKASTORE_SSL_KEYSTORE_PASSWORD_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.HIGH, - KAFKASTORE_SSL_KEYSTORE_PASSWORD_DOC) - .define(KAFKASTORE_SSL_KEYSTORE_TYPE_CONFIG, ConfigDef.Type.STRING, - "JKS", ConfigDef.Importance.MEDIUM, - KAFAKSTORE_SSL_KEYSTORE_TYPE_DOC) - .define(KAFKASTORE_SSL_KEYMANAGER_ALGORITHM_CONFIG, ConfigDef.Type.STRING, - "SunX509", ConfigDef.Importance.LOW, - KAFKASTORE_SSL_KEYMANAGER_ALGORITHM_DOC) - .define(KAFKASTORE_SSL_KEY_PASSWORD_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.HIGH, - KAFKASTORE_SSL_KEY_PASSWORD_DOC) - .define(KAFKASTORE_SSL_ENABLED_PROTOCOLS_CONFIG, ConfigDef.Type.STRING, - "TLSv1.2,TLSv1.1,TLSv1", ConfigDef.Importance.MEDIUM, - KAFAKSTORE_SSL_ENABLED_PROTOCOLS_DOC) - .define(KAFKASTORE_SSL_PROTOCOL_CONFIG, ConfigDef.Type.STRING, - "TLS", ConfigDef.Importance.MEDIUM, - KAFAKSTORE_SSL_PROTOCOL_DOC) - .define(KAFKASTORE_SSL_PROVIDER_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.MEDIUM, - KAFAKSTORE_SSL_PROVIDER_DOC) - .define(KAFKASTORE_SSL_CIPHER_SUITES_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.LOW, - KAFKASTORE_SSL_CIPHER_SUITES_DOC) - .define(KAFKASTORE_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, ConfigDef.Type.STRING, - "", ConfigDef.Importance.LOW, - KAFKASTORE_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_DOC); - } - - private final AvroCompatibilityLevel compatibilityType; - - public KafkaRegistryConfig(Map props) - throws RestConfigException { - super(config, props); - String compatibilityTypeString = getString(KafkaRegistryConfig.COMPATIBILITY_CONFIG); - compatibilityType = AvroCompatibilityLevel.forName(compatibilityTypeString); - if (compatibilityType == null) { - throw new RestConfigException("Unknown Avro compatibility level: " + compatibilityTypeString); - } - } - - public KafkaRegistryConfig(String propsFile) throws RestConfigException { - this(getPropsFromFile(propsFile)); - } - - public KafkaRegistryConfig(Properties props) throws RestConfigException { - super(config, props); - String compatibilityTypeString = getString(KafkaRegistryConfig.COMPATIBILITY_CONFIG); - compatibilityType = AvroCompatibilityLevel.forName(compatibilityTypeString); - if (compatibilityType == null) { - throw new RestConfigException("Unknown Avro compatibility level: " + compatibilityTypeString); - } - } - - private static String getDefaultHost() { - try { - return InetAddress.getLocalHost().getCanonicalHostName(); - } catch (UnknownHostException e) { - throw new ConfigException("Unknown local hostname", e); - } - } - - public static void main(String[] args) { - System.out.println(config.toRst()); - } - - public AvroCompatibilityLevel compatibilityType() { - return compatibilityType; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryRestApplication.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryRestApplication.java deleted file mode 100644 index 5cc47c998..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/KafkaRegistryRestApplication.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry; - -import com.hurence.logisland.agent.rest.api.*; -import com.hurence.logisland.kafka.registry.exceptions.RegistryException; -import com.hurence.logisland.kafka.serialization.RegistrySerializer; -import io.confluent.rest.Application; -import io.confluent.rest.RestConfigException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.ws.rs.core.Configurable; -import java.util.Properties; - -public class KafkaRegistryRestApplication extends Application { - - private static final Logger log = LoggerFactory.getLogger(KafkaRegistryRestApplication.class); - private KafkaRegistry kafkaRegistry = null; - - public KafkaRegistryRestApplication(Properties props) throws RestConfigException { - this(new KafkaRegistryConfig(props)); - } - - public KafkaRegistryRestApplication(KafkaRegistryConfig config) { - super(config); - } - - @Override - public void setupResources(Configurable config, KafkaRegistryConfig schemaRegistryConfig) { - try { - kafkaRegistry = new KafkaRegistry(schemaRegistryConfig, new RegistrySerializer()); - kafkaRegistry.init(); - } catch (RegistryException e) { - log.error("Error starting the schema registry", e); - System.exit(1); - } - - config.register(new DefaultApi(kafkaRegistry)); - config.register(new JobsApi(kafkaRegistry)); - config.register(new TopicsApi(kafkaRegistry)); - - config.register(new ProcessorsApi(kafkaRegistry)); - config.register(new MetricsApi(kafkaRegistry)); - config.register(new ConfigsApi(kafkaRegistry)); - } - - @Override - public void onShutdown() { - kafkaRegistry.close(); - } - - // for testing purpose only - public KafkaRegistry schemaRegistry() { - return kafkaRegistry; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/IncompatibleException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/IncompatibleException.java deleted file mode 100644 index e646d7f40..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/IncompatibleException.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates the schema is incompatible with the registered schema - */ -public class IncompatibleException extends RegistryException { - public IncompatibleException(String message, Throwable cause) { - super(message, cause); - } - - public IncompatibleException(String message) { - super(message); - } - - public IncompatibleException(Throwable cause) { - super(cause); - } - - public IncompatibleException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidException.java deleted file mode 100644 index 37f7ba332..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidException.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates an invalid schema that does not conform to the expected format of the schema - */ -public class InvalidException extends RegistryException { - public InvalidException(String message, Throwable cause) { - super(message, cause); - } - - public InvalidException(String message) { - super(message); - } - - public InvalidException(Throwable cause) { - super(cause); - } - - public InvalidException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidVersionException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidVersionException.java deleted file mode 100644 index a2a650db9..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/InvalidVersionException.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates that the version is not a valid version id. Allowed values are between [1, - * 2^31-1] and the string "latest" - */ -public class InvalidVersionException extends RegistryException { - public InvalidVersionException(String message, Throwable cause) { - super(message, cause); - } - - public InvalidVersionException(String message) { - super(message); - } - - public InvalidVersionException(Throwable cause) { - super(cause); - } - - public InvalidVersionException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryException.java deleted file mode 100644 index 43ac200da..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryException.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates some error while performing a schema registry operation - */ -public class RegistryException extends Exception { - - public RegistryException(String message, Throwable cause) { - super(message, cause); - } - - public RegistryException(String message) { - super(message); - } - - public RegistryException(Throwable cause) { - super(cause); - } - - public RegistryException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryInitializationException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryInitializationException.java deleted file mode 100644 index a1379a772..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryInitializationException.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates an error while initializing schema registry - */ -public class RegistryInitializationException extends RegistryException { - - public RegistryInitializationException(String message, Throwable cause) { - super(message, cause); - } - - public RegistryInitializationException(String message) { - super(message); - } - - public RegistryInitializationException(Throwable cause) { - super(cause); - } - - public RegistryInitializationException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryRequestForwardingException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryRequestForwardingException.java deleted file mode 100644 index f0053d551..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryRequestForwardingException.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates an error while forwarding a write request to the master node in a schema - * registry cluster - */ -public class RegistryRequestForwardingException extends RegistryException { - - public RegistryRequestForwardingException(String message, Throwable cause) { - super(message, cause); - } - - public RegistryRequestForwardingException(String message) { - super(message); - } - - public RegistryRequestForwardingException(Throwable cause) { - super(cause); - } - - public RegistryRequestForwardingException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryStoreException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryStoreException.java deleted file mode 100644 index 0af975b3a..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryStoreException.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates an error while performing an operation on the underlying data store that - * stores all schemas in the registry - */ -public class RegistryStoreException extends RegistryException { - - public RegistryStoreException(String message, Throwable cause) { - super(message, cause); - } - - public RegistryStoreException(String message) { - super(message); - } - - public RegistryStoreException(Throwable cause) { - super(cause); - } - - public RegistryStoreException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryTimeoutException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryTimeoutException.java deleted file mode 100644 index f0df28429..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/RegistryTimeoutException.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates that some schema registry operation timed out. - */ -public class RegistryTimeoutException extends RegistryException { - - public RegistryTimeoutException(String message, Throwable cause) { - super(message, cause); - } - - public RegistryTimeoutException(String message) { - super(message); - } - - public RegistryTimeoutException(Throwable cause) { - super(cause); - } - - public RegistryTimeoutException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/UnknownMasterException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/UnknownMasterException.java deleted file mode 100644 index ac24d9eae..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/registry/exceptions/UnknownMasterException.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry.exceptions; - -/** - * Indicates that the node that is asked to serve the request is not the current master and - * is not aware of the master node to forward the request to - */ -public class UnknownMasterException extends RegistryException { - - public UnknownMasterException(String message, Throwable cause) { - super(message, cause); - } - - public UnknownMasterException(String message) { - super(message); - } - - public UnknownMasterException(Throwable cause) { - super(cause); - } - - public UnknownMasterException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/RegistrySerializer.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/RegistrySerializer.java deleted file mode 100644 index 92c24798e..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/RegistrySerializer.java +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.serialization; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.hurence.logisland.kafka.store.*; -import com.hurence.logisland.kafka.store.exceptions.SerializationException; - -import java.io.IOException; -import java.util.Map; - -public class RegistrySerializer implements Serializer { - - public RegistrySerializer() { - - } - - /** - * @param key Typed key - * @return bytes of the serialized key - */ - @Override - public byte[] serializeKey(RegistryKey key) throws SerializationException { - try { - return new ObjectMapper().writeValueAsBytes(key); - } catch (IOException e) { - throw new SerializationException("Error while serializing key" + key.toString(), - e); - } - } - - /** - * @param value Typed value - * @return bytes of the serialized value - */ - @Override - public byte[] serializeValue(RegistryValue value) throws SerializationException { - try { - return new ObjectMapper().writeValueAsBytes(value); - } catch (IOException e) { - throw new SerializationException( - "Error while serializing value value " + value.toString(), - e); - } - } - - @Override - public RegistryKey deserializeKey(byte[] key) throws SerializationException { - RegistryKey registryKey = null; - RegistryKeyType keyType = null; - try { - try { - Map keyObj = null; - keyObj = new ObjectMapper().readValue(key, - new TypeReference>() { - }); - keyType = RegistryKeyType.forName((String) keyObj.get("keytype")); - if (keyType == RegistryKeyType.JOB) { - registryKey = new ObjectMapper().readValue(key, JobKey.class); - } else if (keyType == RegistryKeyType.NOOP) { - registryKey = new ObjectMapper().readValue(key, NoopKey.class); - } else if (keyType == RegistryKeyType.TOPIC) { - registryKey = new ObjectMapper().readValue(key, TopicKey.class); - } - } catch (JsonProcessingException e) { - - String type = "unknown"; - if (keyType == RegistryKeyType.JOB) { - type = RegistryKeyType.JOB.name(); - } else if (keyType == RegistryKeyType.TOPIC) { - type = RegistryKeyType.TOPIC.name(); - } else if (keyType == RegistryKeyType.NOOP) { - type = RegistryKeyType.NOOP.name(); - } - - throw new SerializationException("Failed to deserialize " + type + " key", e); - } - } catch (IOException e) { - throw new SerializationException("Error while deserializing schema key", e); - } - return registryKey; - } - - /** - * @param key Typed key corresponding to this value - * @param value Bytes of the serialized value - * @return Typed deserialized value. Must be one of {@link JobValue} - * or {@link TopicValue} - */ - @Override - public RegistryValue deserializeValue(RegistryKey key, byte[] value) - throws SerializationException { - RegistryValue schemaRegistryValue = null; - if (key.getKeyType().equals(RegistryKeyType.JOB)) { - try { - schemaRegistryValue = new ObjectMapper().readValue(value, JobValue.class); - } catch (IOException e) { - throw new SerializationException("Error while deserializing job", e); - } - } else if (key.getKeyType().equals(RegistryKeyType.TOPIC)) { - try { - schemaRegistryValue = new ObjectMapper().readValue(value, TopicValue.class); - } catch (IOException e) { - throw new SerializationException("Error while deserializing topic", e); - } - } else { - throw new SerializationException("Unrecognized key type. Must be one of schema or config"); - } - return schemaRegistryValue; - } - - @Override - public void close() { - - } - - @Override - public void configure(Map stringMap) { - - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/Serializer.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/Serializer.java deleted file mode 100644 index a3bcc29b8..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/serialization/Serializer.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.serialization; - -import com.hurence.logisland.kafka.store.exceptions.SerializationException; -import io.confluent.common.Configurable; - -/** - * @param Key type to be serialized from.

A class that implements this interface is - * expected to have a constructor with no parameter. - */ -public interface Serializer extends Configurable { - - /** - * @param key Typed key - * @return bytes of the serialized key - */ - public byte[] serializeKey(K key) throws SerializationException; - - /** - * @param value Typed value - * @return bytes of the serialized value - */ - public byte[] serializeValue(V value) throws SerializationException; - - /** - * @param key Bytes of the serialized key - * @return Typed deserialized key - */ - public K deserializeKey(byte[] key) throws SerializationException; - - /** - * @param key Typed key corresponding to this value - * @param value Bytes of the serialized value - * @return Typed deserialized value - */ - public V deserializeValue(K key, byte[] value) throws SerializationException; - - /** - * Close this serializer - */ - public void close(); -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/InMemoryStore.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/InMemoryStore.java deleted file mode 100644 index a1fb6371a..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/InMemoryStore.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - - -import com.hurence.logisland.kafka.store.exceptions.StoreException; -import com.hurence.logisland.kafka.store.exceptions.StoreInitializationException; - -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.ConcurrentNavigableMap; -import java.util.concurrent.ConcurrentSkipListMap; - - -/** - * In-memory store based on maps - */ -public class InMemoryStore implements Store { - - private final ConcurrentSkipListMap store; - - public InMemoryStore() { - store = new ConcurrentSkipListMap(); - } - - public void init() throws StoreInitializationException { - // do nothing - } - - @Override - public V get(K key) { - return store.get(key); - } - - @Override - public void put(K key, V value) throws StoreException { - store.put(key, value); - } - - @Override - public Iterator getAll(K key1, K key2) { - ConcurrentNavigableMap subMap = (key1 == null && key2 == null) ? - store : store.subMap(key1, key2); - return subMap.values().iterator(); - } - - @Override - public void putAll(Map entries) { - store.putAll(entries); - } - - @Override - public void delete(K key) throws StoreException { - store.remove(key); - } - - @Override - public Iterator getAllKeys() throws StoreException { - return store.keySet().iterator(); - } - - @Override - public void close() { - store.clear(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobKey.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobKey.java deleted file mode 100644 index 5319ec7bb..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobKey.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.codehaus.jackson.annotate.JsonPropertyOrder; -import org.hibernate.validator.constraints.NotEmpty; - -import javax.validation.constraints.Min; - -@JsonPropertyOrder(value = {"keytype", "name", "version", "magic"}) -public class JobKey extends RegistryKey { - - private static final int MAGIC_BYTE = 0; - @NotEmpty - private String name = ""; - @Min(1) - @NotEmpty - private Integer version; - - public JobKey(@JsonProperty("name") String name, - @JsonProperty("version") int version) { - super(RegistryKeyType.JOB); - this.magicByte = MAGIC_BYTE; - if (name != null) - this.name = name; - this.version = version; - } - - @JsonProperty("name") - public String getName() { - return this.name; - } - - @JsonProperty("name") - public void setName(String name) { - if (name != null) - this.name = name; - } - - @JsonProperty("version") - public int getVersion() { - return this.version; - } - - @JsonProperty("version") - public void setVersion(int version) { - this.version = version; - } - - @Override - public boolean equals(Object o) { - if (!super.equals(o)) { - return false; - } - - JobKey that = (JobKey) o; - if (!name.equals(that.name)) { - return false; - } - if (version != that.version) { - return false; - } - return true; - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + name.hashCode(); - result = 31 * result + version; - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{magic=" + this.magicByte + ","); - sb.append("keytype=" + this.keyType.keyType + ","); - sb.append("subject=" + this.name + ","); - sb.append("version=" + this.version + "}"); - return sb.toString(); - } - - @Override - public int compareTo(RegistryKey o) { - int compare = super.compareTo(o); - if (compare == 0) { - JobKey otherKey = (JobKey) o; - int subjectComp = this.name.compareTo(otherKey.name); - return subjectComp == 0 ? this.version - otherKey.version : subjectComp; - } else { - return compare; - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobValue.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobValue.java deleted file mode 100644 index 20864ae79..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/JobValue.java +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.hurence.logisland.agent.rest.model.Job; -import org.hibernate.validator.constraints.NotEmpty; - -import javax.validation.constraints.Min; - -public class JobValue implements Comparable, RegistryValue { - - @NotEmpty - private String name; - @Min(1) - private Integer version; - @Min(0) - private Long id; - @NotEmpty - private Job job; - - public JobValue(@JsonProperty("name") String name, - @JsonProperty("version") Integer version, - @JsonProperty("id") Long id, - @JsonProperty("job") Job job) { - this.name = name; - this.version = version; - this.id = id; - this.job = job; - } - - public JobValue(Job job) { - this.name = job.getName(); - this.version = job.getVersion(); - this.id = job.getId(); - this.job = job; - } - - @JsonProperty("name") - public String getName() { - return name; - } - - @JsonProperty("name") - public void setName(String name) { - this.name = name; - } - - @JsonProperty("version") - public Integer getVersion() { - return this.version; - } - - @JsonProperty("version") - public void setVersion(Integer version) { - this.version = version; - } - - @JsonProperty("id") - public Long getId() { - return this.id; - } - - @JsonProperty("id") - public void setId(Long id) { - this.id = id; - } - - @JsonProperty("job") - public Job getJob() { - return this.job; - } - - @JsonProperty("schema") - public void setJob(Job job) { - this.job = job; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - JobValue that = (JobValue) o; - - if (!this.name.equals(that.name)) { - return false; - } - if (!this.version.equals(that.version)) { - return false; - } - if (!this.id.equals(that.getId())) { - return false; - } - if (!this.job.equals(that.job)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + version; - result = 31 * result + id.intValue(); - result = 31 * result + job.hashCode(); - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{name=" + this.name + ","); - sb.append("version=" + this.version + ","); - sb.append("id=" + this.id + ","); - sb.append("job=" + this.job + "}"); - return sb.toString(); - } - - @Override - public int compareTo(JobValue that) { - int result = this.name.compareTo(that.name); - if (result != 0) { - return result; - } - result = this.version - that.version; - return result; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStore.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStore.java deleted file mode 100644 index e836e3611..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStore.java +++ /dev/null @@ -1,457 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import com.hurence.logisland.kafka.serialization.Serializer; -import com.hurence.logisland.kafka.store.exceptions.SerializationException; -import com.hurence.logisland.kafka.store.exceptions.StoreException; -import com.hurence.logisland.kafka.store.exceptions.StoreInitializationException; -import com.hurence.logisland.kafka.store.exceptions.StoreTimeoutException; -import io.confluent.rest.RestConfig; -import kafka.admin.AdminUtils; -import kafka.admin.RackAwareMode; -import kafka.cluster.Broker; -import kafka.cluster.EndPoint; -import kafka.common.TopicExistsException; -import kafka.log.LogConfig; -import kafka.server.ConfigType; -import kafka.utils.ZkUtils; -import org.apache.kafka.clients.CommonClientConfigs; -import org.apache.kafka.clients.producer.KafkaProducer; -import org.apache.kafka.clients.producer.ProducerConfig; -import org.apache.kafka.clients.producer.ProducerRecord; -import org.apache.kafka.clients.producer.RecordMetadata; -import org.apache.kafka.common.KafkaException; -import org.apache.kafka.common.config.ConfigException; -import org.apache.kafka.common.config.SslConfigs; -import org.apache.kafka.common.security.JaasUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import scala.collection.JavaConversions; -import scala.collection.Seq; - -import java.util.*; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; - -public class KafkaStore implements Store { - - private static final Logger log = LoggerFactory.getLogger(KafkaStore.class); - - private final String kafkaClusterZkUrl; - private final String topic; - private final int desiredReplicationFactor; - private final String groupId; - private final StoreUpdateHandler storeUpdateHandler; - private final Serializer serializer; - private final Store localStore; - private final AtomicBoolean initialized = new AtomicBoolean(false); - private final int initTimeout; - private final int timeout; - private final Seq brokerSeq; - private final String bootstrapBrokers; - private final ZkUtils zkUtils; - private KafkaProducer producer; - private KafkaStoreReaderThread kafkaTopicReader; - // Noop key is only used to help reliably determine last offset; reader thread ignores - // messages with this key - private final K noopKey; - private volatile long lastWrittenOffset = -1L; - private final KafkaRegistryConfig config; - - public KafkaStore(String kafkaStoreTopicConfig, - KafkaRegistryConfig config, - StoreUpdateHandler storeUpdateHandler, - Serializer serializer, - Store localStore, - K noopKey) { - this.kafkaClusterZkUrl = - config.getString(KafkaRegistryConfig.KAFKASTORE_CONNECTION_URL_CONFIG); - - - this.topic = config.getString(kafkaStoreTopicConfig); - - this.desiredReplicationFactor = - config.getInt(KafkaRegistryConfig.KAFKASTORE_TOPIC_REPLICATION_FACTOR_CONFIG); - int port = KafkaRegistry.getPortForIdentity(config.getInt(KafkaRegistryConfig.PORT_CONFIG), - config.getList(RestConfig.LISTENERS_CONFIG)); - this.groupId = String.format("schema-registry-%s-%d", - config.getString(KafkaRegistryConfig.HOST_NAME_CONFIG), - port); - initTimeout = config.getInt(KafkaRegistryConfig.KAFKASTORE_INIT_TIMEOUT_CONFIG); - timeout = config.getInt(KafkaRegistryConfig.KAFKASTORE_TIMEOUT_CONFIG); - this.storeUpdateHandler = storeUpdateHandler; - this.serializer = serializer; - this.localStore = localStore; - this.noopKey = noopKey; - - int zkSessionTimeoutMs = - config.getInt(KafkaRegistryConfig.KAFKASTORE_ZK_SESSION_TIMEOUT_MS_CONFIG); - this.zkUtils = ZkUtils.apply( - kafkaClusterZkUrl, zkSessionTimeoutMs, zkSessionTimeoutMs, - JaasUtils.isZkSecurityEnabled()); - this.brokerSeq = zkUtils.getAllBrokersInCluster(); - - List bootstrapServersConfig = config.getList(KafkaRegistryConfig.KAFKASTORE_BOOTSTRAP_SERVERS_CONFIG); - List endpoints; - if (bootstrapServersConfig.isEmpty()) { - endpoints = brokersToEndpoints(JavaConversions.seqAsJavaList(this.brokerSeq)); - } else { - endpoints = bootstrapServersConfig; - } - this.bootstrapBrokers = filterBrokerEndpoints(endpoints); - log.info("Initializing KafkaStore with broker endpoints: " + this.bootstrapBrokers); - - this.config = config; - } - - @Override - public void init() throws StoreInitializationException { - if (initialized.get()) { - throw new StoreInitializationException( - "Illegal state while initializing store. Store was already initialized"); - } - - // create the schema topic if needed - createSchemaTopic(); - - // set the producer properties and initialize a Kafka producer client - Properties props = new Properties(); - props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapBrokers); - props.put(ProducerConfig.ACKS_CONFIG, "-1"); - props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, - org.apache.kafka.common.serialization.ByteArraySerializer.class); - props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, - org.apache.kafka.common.serialization.ByteArraySerializer.class); - props.put(ProducerConfig.RETRIES_CONFIG, 0); // Producer should not retry - - addSslConfigsToClientProperties(this.config, props); - - producer = new KafkaProducer(props); - - // start the background thread that subscribes to the Kafka topic and applies updates. - // the thread must be created after the schema topic has been created. - this.kafkaTopicReader = - new KafkaStoreReaderThread<>(this.bootstrapBrokers, topic, groupId, - this.storeUpdateHandler, serializer, this.localStore, - this.noopKey, this.config); - this.kafkaTopicReader.start(); - - try { - waitUntilKafkaReaderReachesLastOffset(initTimeout); - } catch (StoreException e) { - throw new StoreInitializationException(e); - } - - boolean isInitialized = initialized.compareAndSet(false, true); - if (!isInitialized) { - throw new StoreInitializationException("Illegal state while initializing store. Store " - + "was already initialized"); - } - } - - public static void addSslConfigsToClientProperties(KafkaRegistryConfig config, Properties props) { - props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_CONFIG)); - if (config.getString(KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_CONFIG).equals( - KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_SSL)) { - props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_TRUSTSTORE_LOCATION_CONFIG)); - props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_TRUSTSTORE_PASSWORD_CONFIG)); - props.put(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_TRUSTSTORE_TYPE_CONFIG)); - props.put(SslConfigs.SSL_TRUSTMANAGER_ALGORITHM_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_TRUSTMANAGER_ALGORITHM_CONFIG)); - putIfNotEmptyString(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_KEYSTORE_LOCATION_CONFIG), props); - putIfNotEmptyString(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_KEYSTORE_PASSWORD_CONFIG), props); - props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_KEYSTORE_TYPE_CONFIG)); - props.put(SslConfigs.SSL_KEYMANAGER_ALGORITHM_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_KEYMANAGER_ALGORITHM_CONFIG)); - putIfNotEmptyString(SslConfigs.SSL_KEY_PASSWORD_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_KEY_PASSWORD_CONFIG), props); - putIfNotEmptyString(SslConfigs.SSL_ENABLED_PROTOCOLS_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_ENABLED_PROTOCOLS_CONFIG), props); - props.put(SslConfigs.SSL_PROTOCOL_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_PROTOCOL_CONFIG)); - putIfNotEmptyString(SslConfigs.SSL_PROVIDER_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_PROVIDER_CONFIG), props); - putIfNotEmptyString(SslConfigs.SSL_CIPHER_SUITES_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_CIPHER_SUITES_CONFIG), props); - putIfNotEmptyString(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG, - config.getString(KafkaRegistryConfig.KAFKASTORE_SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG), props); - } - } - - // helper method to only add a property if its not the empty string. This is required - // because some Kafka client configs expect a null default value, yet ConfigDef doesn't - // support null default values. - private static void putIfNotEmptyString(String parameter, String value, Properties props) { - if (!value.trim().isEmpty()) { - props.put(parameter, value); - } - } - - private void createSchemaTopic() throws StoreInitializationException { - if (AdminUtils.topicExists(zkUtils, topic)) { - verifySchemaTopic(); - return; - } - int numLiveBrokers = brokerSeq.size(); - if (numLiveBrokers <= 0) { - throw new StoreInitializationException("No live Kafka brokers"); - } - int schemaTopicReplicationFactor = Math.min(numLiveBrokers, desiredReplicationFactor); - if (schemaTopicReplicationFactor < desiredReplicationFactor) { - log.warn("Creating the schema topic " + topic + " using a replication factor of " + - schemaTopicReplicationFactor + ", which is less than the desired one of " - + desiredReplicationFactor + ". If this is a production environment, it's " + - "crucial to add more brokers and increase the replication factor of the topic."); - } - Properties schemaTopicProps = new Properties(); - schemaTopicProps.put(LogConfig.CleanupPolicyProp(), "compact"); - - try { - AdminUtils.createTopic(zkUtils, topic, 1, schemaTopicReplicationFactor, schemaTopicProps, - RackAwareMode.Enforced$.MODULE$); - } catch (TopicExistsException e) { - // This is ok. - } - } - - static List brokersToEndpoints(List brokers) { - List endpoints = new LinkedList(); - for (Broker broker : brokers) { - for (EndPoint ep : JavaConversions.asJavaCollection(broker.endPoints().values())) { - endpoints.add(ep.connectionString()); - } - } - return endpoints; - } - - static String filterBrokerEndpoints(List endpoints) { - StringBuilder sb = new StringBuilder(); - - for (String endpoint : endpoints) { - if (endpoint.startsWith(KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_SSL + "://") - || endpoint.startsWith(KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_PLAINTEXT + "://")) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(endpoint); - } else { - log.warn("Ignoring non-plaintext and non-SSL Kafka endpoint: " + endpoint); - } - } - - if (sb.length() == 0) { - throw new ConfigException("Only plaintext and SSL Kafka endpoints are supported and " + - "none are configured."); - } - - return sb.toString(); - } - - private void verifySchemaTopic() { - Set topics = new HashSet(); - topics.add(topic); - - // check # partition and the replication factor - scala.collection.Map partitionAssignment = zkUtils.getPartitionAssignmentForTopics( - JavaConversions.asScalaSet(topics).toSeq()) - .get(topic).get(); - - if (partitionAssignment.size() != 1) { - log.warn("The schema topic " + topic + " should have only 1 partition."); - } - - if (((Seq) partitionAssignment.get(0).get()).size() < desiredReplicationFactor) { - log.warn("The replication factor of the schema topic " + topic + " is less than the " + - "desired one of " + desiredReplicationFactor + ". If this is a production " + - "environment, it's crucial to add more brokers and increase the replication " + - "factor of the topic."); - } - - // check the retention policy - Properties prop = AdminUtils.fetchEntityConfig(zkUtils, ConfigType.Topic(), topic); - String retentionPolicy = prop.getProperty(LogConfig.CleanupPolicyProp()); - if (retentionPolicy == null || "compact".compareTo(retentionPolicy) != 0) { - log.warn("The retention policy of the schema topic " + topic + " may be incorrect. " + - "Please configure it with compact."); - } - } - - /** - * Wait until the KafkaStore catches up to the last message in the Kafka topic. - */ - public void waitUntilKafkaReaderReachesLastOffset(int timeoutMs) throws StoreException { - long offsetOfLastMessage = getLatestOffset(timeoutMs); - log.info("Wait to catch up until the offset of the last message at " + offsetOfLastMessage); - kafkaTopicReader.waitUntilOffset(offsetOfLastMessage, timeoutMs, TimeUnit.MILLISECONDS); - log.debug("Reached offset at " + offsetOfLastMessage); - } - - public void markLastWrittenOffsetInvalid() { - lastWrittenOffset = -1L; - } - - @Override - public V get(K key) throws StoreException { - assertInitialized(); - return localStore.get(key); - } - - @Override - public void put(K key, V value) throws StoreTimeoutException, StoreException { - assertInitialized(); - if (key == null) { - throw new StoreException("Key should not be null"); - } - - // write to the Kafka topic - ProducerRecord producerRecord = null; - try { - producerRecord = - new ProducerRecord(topic, 0, this.serializer.serializeKey(key), - value == null ? null : this.serializer.serializeValue( - value)); - } catch (SerializationException e) { - throw new StoreException("Error serializing schema while creating the Kafka produce " - + "record", e); - } - - boolean knownSuccessfulWrite = false; - try { - log.trace("Sending record to KafkaStore topic: " + producerRecord); - Future ack = producer.send(producerRecord); - RecordMetadata recordMetadata = ack.get(timeout, TimeUnit.MILLISECONDS); - - log.trace("Waiting for the local store to catch up to offset " + recordMetadata.offset()); - this.lastWrittenOffset = recordMetadata.offset(); - kafkaTopicReader.waitUntilOffset(this.lastWrittenOffset, timeout, TimeUnit.MILLISECONDS); - knownSuccessfulWrite = true; - } catch (InterruptedException e) { - throw new StoreException("Put operation interrupted while waiting for an ack from Kafka", e); - } catch (ExecutionException e) { - throw new StoreException("Put operation failed while waiting for an ack from Kafka", e); - } catch (TimeoutException e) { - throw new StoreTimeoutException( - "Put operation timed out while waiting for an ack from Kafka", e); - } catch (KafkaException ke) { - throw new StoreException("Put operation to Kafka failed", ke); - } finally { - if (!knownSuccessfulWrite) { - this.lastWrittenOffset = -1L; - } - } - } - - @Override - public Iterator getAll(K key1, K key2) throws StoreException { - assertInitialized(); - return localStore.getAll(key1, key2); - } - - @Override - public void putAll(Map entries) throws StoreException { - assertInitialized(); - // TODO: write to the Kafka topic as a batch - for (Map.Entry entry : entries.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } - - @Override - public void delete(K key) throws StoreException { - assertInitialized(); - // delete from the Kafka topic by writing a null value for the key - put(key, null); - } - - @Override - public Iterator getAllKeys() throws StoreException { - return localStore.getAllKeys(); - } - - @Override - public void close() { - kafkaTopicReader.shutdown(); - log.debug("Kafka store reader thread shut down"); - producer.close(); - log.debug("Kafka store producer shut down"); - zkUtils.close(); - log.debug("Kafka store zookeeper client shut down"); - localStore.close(); - log.debug("Kafka store shut down complete"); - } - - /** - * For testing. - */ - KafkaStoreReaderThread getKafkaStoreReaderThread() { - return this.kafkaTopicReader; - } - - private void assertInitialized() throws StoreException { - if (!initialized.get()) { - throw new StoreException("Illegal state. Store not initialized yet"); - } - } - - /** - * Return the latest offset of the store topic. - *

- * The most reliable way to do so in face of potential Kafka broker failure is to produce - * successfully to the Kafka topic and get the offset of the returned metadata. - *

- * If the most recent write to Kafka was successful (signaled by lastWrittenOffset >= 0), - * immediately return that offset. Otherwise write a "Noop key" to Kafka in order to find the - * latest offset. - */ - private long getLatestOffset(int timeoutMs) throws StoreException { - ProducerRecord producerRecord = null; - - if (this.lastWrittenOffset >= 0) { - return this.lastWrittenOffset; - } - - try { - producerRecord = - new ProducerRecord(topic, 0, this.serializer.serializeKey(noopKey), null); - } catch (SerializationException e) { - throw new StoreException("Failed to serialize noop key.", e); - } - - try { - log.trace("Sending Noop record to KafkaStore to find last offset."); - Future ack = producer.send(producerRecord); - RecordMetadata metadata = ack.get(timeoutMs, TimeUnit.MILLISECONDS); - this.lastWrittenOffset = metadata.offset(); - log.trace("Noop record's offset is " + this.lastWrittenOffset); - return this.lastWrittenOffset; - } catch (Exception e) { - throw new StoreException("Failed to write Noop record to kafka store.", e); - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThread.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThread.java deleted file mode 100644 index bf6167933..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThread.java +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import com.hurence.logisland.kafka.serialization.Serializer; -import com.hurence.logisland.kafka.store.exceptions.*; -import kafka.utils.ShutdownableThread; -import org.apache.kafka.clients.consumer.*; -import org.apache.kafka.common.PartitionInfo; -import org.apache.kafka.common.TopicPartition; -import org.apache.kafka.common.errors.RecordTooLargeException; -import org.apache.kafka.common.errors.WakeupException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -/** - * Thread that reads schema registry state from the Kafka compacted topic and modifies - * the local store to be consistent. - * - * On startup, this thread will always read from the beginning of the topic. We assume - * the topic will always be small, hence the startup time to read the topic won't take - * too long. Because the topic is always read from the beginning, the consumer never - * commits offsets. - */ -public class KafkaStoreReaderThread extends ShutdownableThread { - - private static final Logger log = LoggerFactory.getLogger(KafkaStoreReaderThread.class); - - private final String topic; - private final TopicPartition topicPartition; - private final String groupId; - private final StoreUpdateHandler storeUpdateHandler; - private final Serializer serializer; - private final Store localStore; - private final ReentrantLock offsetUpdateLock; - private final Condition offsetReachedThreshold; - private Consumer consumer; - private long offsetInSchemasTopic = -1L; - // Noop key is only used to help reliably determine last offset; reader thread ignores - // messages with this key - private final K noopKey; - - public KafkaStoreReaderThread(String bootstrapBrokers, - String topic, - String groupId, - StoreUpdateHandler storeUpdateHandler, - Serializer serializer, - Store localStore, - K noopKey, - KafkaRegistryConfig config) { - super("kafka-store-reader-thread-" + topic, false); // this thread is not interruptible - offsetUpdateLock = new ReentrantLock(); - offsetReachedThreshold = offsetUpdateLock.newCondition(); - this.topic = topic; - this.groupId = groupId; - this.storeUpdateHandler = storeUpdateHandler; - this.serializer = serializer; - this.localStore = localStore; - this.noopKey = noopKey; - - Properties consumerProps = new Properties(); - consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, this.groupId); - consumerProps.put(ConsumerConfig.CLIENT_ID_CONFIG, "KafkaStore-reader-" + this.topic); - - consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapBrokers); - consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); - consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); - consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, - org.apache.kafka.common.serialization.ByteArrayDeserializer.class); - consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, - org.apache.kafka.common.serialization.ByteArrayDeserializer.class); - - KafkaStore.addSslConfigsToClientProperties(config, consumerProps); - - this.consumer = new KafkaConsumer<>(consumerProps); - - List partitions = this.consumer.partitionsFor(this.topic); - if (partitions == null || partitions.size() < 1) { - throw new IllegalArgumentException("Unable to subscribe to the Kafka topic " + topic + - " backing this data store. Topic may not exist."); - } else if (partitions.size() > 1) { - throw new IllegalStateException("Unexpected number of partitions in the " + topic + - " topic. Expected 1 and instead got " + partitions.size()); - } - - this.topicPartition = new TopicPartition(topic, 0); - this.consumer.assign(Arrays.asList(this.topicPartition)); - this.consumer.seekToBeginning(Arrays.asList(this.topicPartition)); - - log.info("Initialized last consumed offset to " + offsetInSchemasTopic); - - log.debug("Kafka store reader thread started with consumer properties " + - consumerProps.toString()); - } - - @Override - public void doWork() { - try { - ConsumerRecords records = consumer.poll(Long.MAX_VALUE); - for (ConsumerRecord record : records) { - K messageKey = null; - try { - messageKey = this.serializer.deserializeKey(record.key()); - } catch (SerializationException e) { - log.error("Failed to deserialize the schema or config key", e); - continue; - } - - if (messageKey.equals(noopKey)) { - // If it's a noop, update local offset counter and do nothing else - try { - offsetUpdateLock.lock(); - offsetInSchemasTopic = record.offset(); - offsetReachedThreshold.signalAll(); - } finally { - offsetUpdateLock.unlock(); - } - } else { - V message = null; - try { - message = - record.value() == null ? null : serializer.deserializeValue(messageKey, record.value()); - } catch (SerializationException e) { - log.error("Failed to deserialize a schema or config update", e); - continue; - } - try { - log.trace("Applying update (" + messageKey + "," + message + ") to the local " + - "store"); - if (message == null) { - localStore.delete(messageKey); - } else { - localStore.put(messageKey, message); - } - this.storeUpdateHandler.handleUpdate(messageKey, message); - try { - offsetUpdateLock.lock(); - offsetInSchemasTopic = record.offset(); - offsetReachedThreshold.signalAll(); - } finally { - offsetUpdateLock.unlock(); - } - } catch (StoreException se) { - log.error("Failed to add record from the Kafka topic" + topic + " the local store"); - } - } - } - } catch (WakeupException we) { - // do nothing because the thread is closing -- see shutdown() - } catch (RecordTooLargeException rtle) { - throw new IllegalStateException( - "Consumer threw RecordTooLargeException. A schema has been written that " - + "exceeds the default maximum fetch size.", rtle); - } catch (RuntimeException e) { - log.error("KafkaStoreReader thread has died for an unknown reason."); - throw new RuntimeException(e); - } - } - - @Override - public void shutdown() { - log.debug("Starting shutdown of KafkaStoreReaderThread."); - - super.initiateShutdown(); - if (consumer != null) { - consumer.wakeup(); - } - if (localStore != null) { - localStore.close(); - } - super.awaitShutdown(); - consumer.close(); - log.info("KafkaStoreReaderThread shutdown complete."); - } - - public void waitUntilOffset(long offset, long timeout, TimeUnit timeUnit) throws StoreException { - if (offset < 0) { - throw new StoreException("KafkaStoreReaderThread can't wait for a negative offset."); - } - - log.trace("Waiting to read offset {}. Currently at offset {}", offset, offsetInSchemasTopic); - - try { - offsetUpdateLock.lock(); - long timeoutNs = TimeUnit.NANOSECONDS.convert(timeout, timeUnit); - while ((offsetInSchemasTopic < offset) && (timeoutNs > 0)) { - try { - timeoutNs = offsetReachedThreshold.awaitNanos(timeoutNs); - } catch (InterruptedException e) { - log.debug("Interrupted while waiting for the background store reader thread to reach" - + " the specified offset: " + offset, e); - } - } - } finally { - offsetUpdateLock.unlock(); - } - - if (offsetInSchemasTopic < offset) { - throw new StoreTimeoutException( - "KafkaStoreReaderThread failed to reach target offset within the timeout interval. " - + "targetOffset: " + offset + ", offsetReached: " + offsetInSchemasTopic - + ", timeout(ms): " + TimeUnit.MILLISECONDS.convert(timeout, timeUnit)); - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreService.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreService.java deleted file mode 100644 index 9dbf6785f..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/KafkaStoreService.java +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hurence.logisland.kafka.store; - - -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import com.hurence.logisland.kafka.serialization.Serializer; -import com.hurence.logisland.kafka.registry.exceptions.*; -import com.hurence.logisland.kafka.store.exceptions.*; -import org.apache.commons.collections.IteratorUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - - -/** - * Wraps a Kafka store and handle all the versionning, - * id allocation and master forwarding stuff - */ -public class KafkaStoreService { - - private static final Logger log = LoggerFactory.getLogger(KafkaStoreService.class); - - protected final Object masterLock = new Object(); - - private final Map guidToSchemaKey; - private final Map valueHashToGuid; - - protected final KafkaStore kafkaStore; - protected final Serializer serializer; - protected final KafkaRegistry kafkaRegistry; - protected final int kafkaStoreTimeoutMs; - - public KafkaStore getKafkaStore() { - return kafkaStore; - } - - public KafkaStoreService(KafkaRegistry kafkaRegistry, - String kafkaStoreTopicConfig, - KafkaRegistryConfig config, - Serializer serializer) throws RegistryException { - - this.kafkaStoreTimeoutMs = config.getInt(KafkaRegistryConfig.KAFKASTORE_TIMEOUT_CONFIG); - this.kafkaRegistry = kafkaRegistry; - this.serializer = serializer; - this.guidToSchemaKey = new HashMap<>(); - this.valueHashToGuid = new HashMap<>(); - - kafkaStore = - new KafkaStore<>( - kafkaStoreTopicConfig, - config, - (key, value) -> { - - }, - this.serializer, - new InMemoryStore<>(), - new NoopKey()); - - - } - - - public void init() throws RegistryInitializationException { - try { - kafkaStore.init(); - } catch (StoreInitializationException e) { - throw new RegistryInitializationException( - "Error initializing kafka store while initializing schema registry", e); - } - } - - public void close() { - log.info("Shutting down kafka store wrapper"); - kafkaStore.close(); - } - - - public RegistryValue create(RegistryKey key, RegistryValue value) throws RegistryException { - try { - // Ensure cache is up-to-date before any potential writes - kafkaStore.waitUntilKafkaReaderReachesLastOffset(kafkaStoreTimeoutMs); - - // see if the schema to be registered already exists - MD5 md5 = MD5.ofString(value.toString()); - if (this.valueHashToGuid.containsKey(md5)) { - return this.valueHashToGuid.get(md5); - } - - kafkaStore.put(key, value); - - } catch (StoreTimeoutException te) { - throw new RegistryTimeoutException("Write to the Kafka store timed out while", te); - } catch (StoreException e) { - throw new RegistryStoreException("Error while registering the schema in the" + - " backend Kafka store", e); - } - return value; - } - - public RegistryValue createOrForward(RegistryKey key, RegistryValue value, Map headerProperties) throws RegistryException { - - synchronized (masterLock) { - if (kafkaRegistry.isMaster()) { - return create(key, value); - } else { - // forward registering request to the master - if (kafkaRegistry.masterIdentity() != null) { - return forwardCreateRequestToMaster(key, value, headerProperties); - } else { - throw new UnknownMasterException("Register schema request failed since master is " - + "unknown"); - } - } - } - } - - - private RegistryValue forwardCreateRequestToMaster(RegistryKey key, - RegistryValue value, - Map headerProperties) - throws RegistryRequestForwardingException { - - throw new RegistryRequestForwardingException("not implemented yet => time to do it ?"); - /* - UrlList baseUrl = masterRestService.getBaseUrls(); - - RegisterSchemaRequest registerSchemaRequest = new RegisterSchemaRequest(); - registerSchemaRequest.setSchema(schemaString); - log.debug(String.format("Forwarding registering schema request %s to %s", - registerSchemaRequest, baseUrl)); - try { - int id = masterRestService.registerSchema(headerProperties, registerSchemaRequest, subject); - return id; - } catch (IOException e) { - throw new SchemaRegistryRequestForwardingException( - String.format("Unexpected error while forwarding the registering schema request %s to %s", - registerSchemaRequest, baseUrl), - e); - } catch (RestClientException e) { - throw new RestException(e.getMessage(), e.getStatus(), e.getErrorCode(), e); - }*/ - } - - - public RegistryValue get(RegistryKey key) throws RegistryException { - try { - - return kafkaStore.get(key); - } catch (StoreException e) { - throw new RegistryStoreException( - "Error while retrieving schema from the backend Kafka" + - " store", e); - } - - } - - public List getAll() throws RegistryException { - try { - - return IteratorUtils.toList(kafkaStore.getAll(null,null)); - } catch (StoreException e) { - throw new RegistryStoreException( - "Error while retrieving schema from the backend Kafka" + - " store", e); - } - - } - - public void delete(RegistryKey key) throws RegistryException { - try { - kafkaStore.delete(key); - - } catch (StoreTimeoutException te) { - throw new RegistryTimeoutException("Write to the Kafka store timed out while", te); - } catch (StoreException e) { - throw new RegistryStoreException("Error while registering the schema in the" + - " backend Kafka store", e); - } - } - - - public RegistryValue update(RegistryKey key, RegistryValue newValue) throws RegistryException { - try { - // Ensure cache is up-to-date before any potential writes - kafkaStore.waitUntilKafkaReaderReachesLastOffset(kafkaStoreTimeoutMs); - kafkaStore.put(key, newValue); - } catch (StoreTimeoutException te) { - throw new RegistryTimeoutException("Write to the Kafka store timed out while", te); - } catch (StoreException e) { - throw new RegistryStoreException("Error while registering the schema in the" + - " backend Kafka store", e); - } - return newValue; - } - - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/MD5.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/MD5.java deleted file mode 100644 index 34e3e2e96..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/MD5.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import jersey.repackaged.com.google.common.base.Preconditions; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -/** - * Simple wrapper for 16 byte MD5 hash. - */ -public class MD5 { - - private final byte[] md5; - - public MD5(byte[] md5) { - Preconditions.checkNotNull(md5, "Tried to instantiate MD5 object with null byte array."); - Preconditions.checkArgument( - md5.length == 16, "Tried to instantiate MD5 object with invalid byte array."); - - this.md5 = md5; - } - - /** - * Factory method converts String into MD5 object. - */ - public static MD5 ofString(String str) { - try { - MessageDigest md = MessageDigest.getInstance("MD5"); - md.update(str.getBytes()); - return new MD5(md.digest()); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.md5); - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - if (o == null) { - return false; - } - - if (!(o instanceof MD5)) { - return false; - } - - MD5 otherMd5 = (MD5) o; - return Arrays.equals(this.md5, otherMd5.md5); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/NoopKey.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/NoopKey.java deleted file mode 100644 index cf31d5bb9..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/NoopKey.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import org.codehaus.jackson.annotate.JsonPropertyOrder; - -/** - * - */ -@JsonPropertyOrder(value = {"keytype", "magic"}) -public class NoopKey extends RegistryKey { - private static final int MAGIC_BYTE = 0; - - public NoopKey() { - super(RegistryKeyType.NOOP); - this.magicByte = MAGIC_BYTE; - } - - @Override - public boolean equals(Object o) { - return super.equals(o); - } - - @Override - public int hashCode() { - return super.hashCode(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{magic=" + this.magicByte + ","); - sb.append("keytype=" + this.keyType.keyType + "}"); - return sb.toString(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKey.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKey.java deleted file mode 100644 index ded2c9c7a..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKey.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.hibernate.validator.constraints.NotEmpty; - -import javax.validation.constraints.Min; - -public abstract class RegistryKey implements Comparable { - - @Min(0) - protected int magicByte; - @NotEmpty - protected RegistryKeyType keyType; - - public RegistryKey(@JsonProperty("keytype") RegistryKeyType keyType) { - this.keyType = keyType; - } - - @JsonProperty("magic") - public int getMagicByte() { - return this.magicByte; - } - - @JsonProperty("magic") - public void setMagicByte(int magicByte) { - this.magicByte = magicByte; - } - - @JsonProperty("keytype") - public RegistryKeyType getKeyType() { - return this.keyType; - } - - @JsonProperty("keytype") - public void setKeyType(RegistryKeyType keyType) { - this.keyType = keyType; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - RegistryKey that = (RegistryKey) o; - - if (this.magicByte != that.magicByte) { - return false; - } - if (!this.keyType.equals(that.keyType)) { - return false; - } - return true; - } - - @Override - public int hashCode() { - int result = 31 * this.magicByte; - result = 31 * result + this.keyType.hashCode(); - return result; - } - - @Override - public int compareTo(RegistryKey otherKey) { - return this.keyType.compareTo(otherKey.keyType); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKeyType.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKeyType.java deleted file mode 100644 index 4fa1bee19..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryKeyType.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -public enum RegistryKeyType { - JOB("JOB"), - TOPIC("TOPIC"), - NOOP("NOOP"); - - public final String keyType; - - private RegistryKeyType(String keyType) { - this.keyType = keyType; - } - - public static RegistryKeyType forName(String keyType) { - if (JOB.keyType.equals(keyType)) { - return JOB; - } else if (TOPIC.keyType.equals(keyType)) { - return TOPIC; - } else if (NOOP.keyType.equals(keyType)) { - return NOOP; - } else { - throw new IllegalArgumentException("Unknown registry key type : " + keyType - + " Valid key types are {config, schema}"); - } - } -} - diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryValue.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryValue.java deleted file mode 100644 index 98fa2ffb2..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/RegistryValue.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -public interface RegistryValue { - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/Store.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/Store.java deleted file mode 100644 index a2060ecd0..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/Store.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - -import com.hurence.logisland.kafka.store.exceptions.StoreException; -import com.hurence.logisland.kafka.store.exceptions.StoreInitializationException; - -import java.util.Iterator; -import java.util.Map; - -public interface Store { - - public void init() throws StoreInitializationException; - - public V get(K key) throws StoreException; - - public void put(K key, V value) throws StoreException; - - /** - * Iterator over keys in the specified range - * - * @param key1 If key1 is null, start from the first key in sorted order - * @param key2 If key2 is null, end at the last key - * @return Iterator over keys in the half-open interval [key1, key2). If both keys are null, - * return an iterator over all keys in the database - */ - public Iterator getAll(K key1, K key2) throws StoreException; - - public void putAll(Map entries) throws StoreException; - - public void delete(K key) throws StoreException; - - public Iterator getAllKeys() throws StoreException; - - public void close(); -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/StoreUpdateHandler.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/StoreUpdateHandler.java deleted file mode 100644 index bfe9202ed..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/StoreUpdateHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -public interface StoreUpdateHandler { - - /** - * Invoked on every new K,V pair written to the store - * - * @param key Key associated with the data - * @param value Data written to the store - */ - public void handleUpdate(K key, V value); - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicKey.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicKey.java deleted file mode 100644 index 97176e548..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicKey.java +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.fasterxml.jackson.annotation.JsonProperty; -import org.codehaus.jackson.annotate.JsonPropertyOrder; -import org.hibernate.validator.constraints.NotEmpty; - -import javax.validation.constraints.Min; - -@JsonPropertyOrder(value = {"keytype", "name", "version", "magic"}) -public class TopicKey extends RegistryKey { - - private static final int MAGIC_BYTE = 0; - @NotEmpty - private String name; - @Min(1) - @NotEmpty - private Integer version; - - public TopicKey(@JsonProperty("name") String name, - @JsonProperty("version") int version) { - super(RegistryKeyType.TOPIC); - this.magicByte = MAGIC_BYTE; - this.name = name; - this.version = version; - } - - @JsonProperty("name") - public String getName() { - return this.name; - } - - @JsonProperty("name") - public void setName(String name) { - this.name = name; - } - - @JsonProperty("version") - public int getVersion() { - return this.version; - } - - @JsonProperty("version") - public void setVersion(int version) { - this.version = version; - } - - @Override - public boolean equals(Object o) { - if (!super.equals(o)) { - return false; - } - - TopicKey that = (TopicKey) o; - if (!name.equals(that.name)) { - return false; - } - if (version != that.version) { - return false; - } - return true; - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + name.hashCode(); - result = 31 * result + version; - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{magic=" + this.magicByte + ","); - sb.append("keytype=" + this.keyType.keyType + ","); - sb.append("subject=" + this.name + ","); - sb.append("version=" + this.version + "}"); - return sb.toString(); - } - - @Override - public int compareTo(RegistryKey o) { - int compare = super.compareTo(o); - if (compare == 0) { - TopicKey otherKey = (TopicKey) o; - int subjectComp = this.name.compareTo(otherKey.name); - return subjectComp == 0 ? this.version - otherKey.version : subjectComp; - } else { - return compare; - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicValue.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicValue.java deleted file mode 100644 index 5fa9b63af..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/TopicValue.java +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.hurence.logisland.agent.rest.model.Topic; -import org.hibernate.validator.constraints.NotEmpty; - -import javax.validation.constraints.Min; - -public class TopicValue implements Comparable, RegistryValue { - - @NotEmpty - private String name; - @Min(1) - private Integer version; - @Min(0) - private Long id; - @NotEmpty - private Topic topic; - - public TopicValue(@JsonProperty("name") String name, - @JsonProperty("version") Integer version, - @JsonProperty("id") Long id, - @JsonProperty("topic") Topic topic) { - this.name = name; - this.version = version; - this.id = id; - this.topic = topic; - } - - public TopicValue(Topic topic) { - this.name = topic.getName(); - this.version = topic.getVersion(); - this.id = topic.getId(); - this.topic = topic; - } - - @JsonProperty("name") - public String getName() { - return name; - } - - @JsonProperty("name") - public void setName(String name) { - this.name = name; - } - - @JsonProperty("version") - public Integer getVersion() { - return this.version; - } - - @JsonProperty("version") - public void setVersion(Integer version) { - this.version = version; - } - - @JsonProperty("id") - public Long getId() { - return this.id; - } - - @JsonProperty("id") - public void setId(Long id) { - this.id = id; - } - - @JsonProperty("topic") - public Topic getTopic() { - return this.topic; - } - - @JsonProperty("schema") - public void setTopic(Topic topic) { - this.topic = topic; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - TopicValue that = (TopicValue) o; - - if (!this.name.equals(that.name)) { - return false; - } - if (!this.version.equals(that.version)) { - return false; - } - if (!this.id.equals(that.getId())) { - return false; - } - if (!this.topic.equals(that.topic)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result = name.hashCode(); - result = 31 * result + version; - result = 31 * result + id.intValue(); - result = 31 * result + topic.hashCode(); - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{name=" + this.name + ","); - sb.append("version=" + this.version + ","); - sb.append("id=" + this.id + ","); - sb.append("topic=" + this.topic + "}"); - return sb.toString(); - } - - @Override - public int compareTo(TopicValue that) { - int result = this.name.compareTo(that.name); - if (result != 0) { - return result; - } - result = this.version - that.version; - return result; - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/SerializationException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/SerializationException.java deleted file mode 100644 index 339a5747b..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/SerializationException.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store.exceptions; - -/** - * Error while (de)serializing data while reading from or writing to a - * * io.confluent.kafka.schemaregistry.storage.Store - */ -public class SerializationException extends Exception { - - public SerializationException(String message, Throwable cause) { - super(message, cause); - } - - public SerializationException(String message) { - super(message); - } - - public SerializationException(Throwable cause) { - super(cause); - } - - public SerializationException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreException.java deleted file mode 100644 index 0d8cb0a6a..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreException.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store.exceptions; - -/** - * Error while performing an operation on a - * * io.confluent.kafka.schemaregistry.storage.Store - */ -public class StoreException extends Exception { - - public StoreException(String message, Throwable cause) { - super(message, cause); - } - - public StoreException(String message) { - super(message); - } - - public StoreException(Throwable cause) { - super(cause); - } - - public StoreException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreInitializationException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreInitializationException.java deleted file mode 100644 index 7345eb488..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreInitializationException.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store.exceptions; - -/** - * Error while initializing a io.confluent.kafka.schemaregistry.storage.Store - */ -public class StoreInitializationException extends Exception { - - public StoreInitializationException(String message, Throwable cause) { - super(message, cause); - } - - public StoreInitializationException(String message) { - super(message); - } - - public StoreInitializationException(Throwable cause) { - super(cause); - } - - public StoreInitializationException() { - super(); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreTimeoutException.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreTimeoutException.java deleted file mode 100644 index 5d499167a..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/store/exceptions/StoreTimeoutException.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store.exceptions; - -/** - * Indicates either a write, read or a bootstrap operation on the underlying Kafka store timed out. - */ -public class StoreTimeoutException extends StoreException { - - public StoreTimeoutException(String message) { - super(message); - } - - public StoreTimeoutException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/utils/KafkaOffsetUtils.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/utils/KafkaOffsetUtils.java deleted file mode 100644 index 26b8fe6f1..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/utils/KafkaOffsetUtils.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.hurence.logisland.kafka.utils; - - -import kafka.api.FetchRequestBuilder; -import kafka.api.PartitionOffsetRequestInfo; -import kafka.cluster.Broker; -import kafka.common.ErrorMapping; -import kafka.common.TopicAndPartition; - -import kafka.javaapi.FetchRequest; -import kafka.javaapi.FetchResponse; -import kafka.javaapi.OffsetRequest; -import kafka.javaapi.OffsetResponse; -import kafka.javaapi.consumer.SimpleConsumer; -import kafka.javaapi.message.ByteBufferMessageSet; -import kafka.utils.ZkUtils; -import org.apache.kafka.common.KafkaException; -import org.apache.kafka.common.security.JaasUtils; -import org.apache.kafka.connect.errors.ConnectException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import scala.Option; -import scala.util.parsing.json.JSON; - -import java.io.IOException; -import java.nio.channels.UnresolvedAddressException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@SuppressWarnings("unchecked") -public class KafkaOffsetUtils { - - private static final int AVG_LINE_SIZE_IN_BYTES = 1000; - - private static final int DEFAULT_ZK_SESSION_TIMEOUT_MS = 30 * 1000; - private static final int DEFAULT_ZK_CONNECTION_TIMEOUT_MS = 30 * 1000; - private ZkUtils zkUtils; - private SimpleConsumer consumer = null; - private Broker leaderBroker; - - private static final Logger log = LoggerFactory.getLogger(KafkaOffsetUtils.class); - - - public KafkaOffsetUtils(String zkUrl) { - - this.zkUtils = ZkUtils.apply( - zkUrl, - DEFAULT_ZK_SESSION_TIMEOUT_MS, - DEFAULT_ZK_CONNECTION_TIMEOUT_MS, - JaasUtils.isZkSecurityEnabled()); - } - - - public void close() { - zkUtils.close(); - } - - public List list() { - return (List) zkUtils.getConsumerGroups(); - } - - - public long getLogEndOffset(String topic, int partition) { - try { - SimpleConsumer consumer = findLeaderConsumer(topic, partition); - TopicAndPartition topicAndPartition = new TopicAndPartition(topic, partition); - Map infoMap = new HashMap<>(); - infoMap.put(topicAndPartition, new PartitionOffsetRequestInfo(kafka.api.OffsetRequest.LatestTime(), 1)); - - OffsetRequest request = new OffsetRequest(infoMap, kafka.api.OffsetRequest.CurrentVersion(), consumer.clientId()); - OffsetResponse response = consumer.getOffsetsBefore(request); - - // Retrieve offsets from response - long[] offsets = response.hasError() ? null : response.offsets(topicAndPartition.topic(), topicAndPartition.partition()); - if (offsets == null || offsets.length <= 0) { - short errorCode = response.errorCode(topicAndPartition.topic(), topicAndPartition.partition()); - - // If the topic partition doesn't exists, use offset 0 without logging error. - if (errorCode != ErrorMapping.UnknownTopicOrPartitionCode()) { - log.warn("Failed to fetch latest offset for {}. Error: {}. Default offset to 0.", - topicAndPartition, errorCode); - } - return 0L; - } - - - consumer.close(); - - return offsets[0]; - - - } catch (Exception ex) { - log.error("unable to retrieve offset {}", new Object[]{ex}); - return -1; - } - } - - private SimpleConsumer findLeaderConsumer(String topic, int partition) { - Option leaderForPartition = zkUtils.getLeaderForPartition(topic, partition); - - return getZkConsumer((Integer) leaderForPartition.get()); - } - - - private SimpleConsumer getZkConsumer(int brokerId) { - try { - Option data = zkUtils.readDataMaybeNull(ZkUtils.BrokerIdsPath() + "/" + brokerId)._1; - - Map brokerInfo = (Map) JSON.parseFull(data.get()).get(); - String host = (String) brokerInfo.get("host"); - int port = (int) brokerInfo.get("port"); - return new SimpleConsumer(host, port, 10000, 100000, "ConsumerGroupCommand"); - - } catch (Exception ex) { - System.out.println("Could not parse broker info due to " + ex.getMessage()); - return null; - } - } - - - - - -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/RegistryIdentity.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/RegistryIdentity.java deleted file mode 100644 index 09fcdd3da..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/RegistryIdentity.java +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.zookeeper; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.IOException; - -/** - * The identity of a schema registry instance. The master will store the json representation of its - * identity in Zookeeper. - */ -public class RegistryIdentity { - - public static int CURRENT_VERSION = 1; - - private Integer version; - private String host; - private Integer port; - private Boolean masterEligibility; - - public RegistryIdentity(@JsonProperty("host") String host, - @JsonProperty("port") Integer port, - @JsonProperty("master_eligibility") Boolean masterEligibility) { - this.version = CURRENT_VERSION; - this.host = host; - this.port = port; - this.masterEligibility = masterEligibility; - } - - public static RegistryIdentity fromJson(String json) throws IOException { - return new ObjectMapper().readValue(json, RegistryIdentity.class); - } - - @JsonProperty("version") - public Integer getVersion() { - return this.version; - } - - @JsonProperty("version") - public void setVersion(Integer version) { - this.version = version; - } - - @JsonProperty("host") - public String getHost() { - return this.host; - } - - @JsonProperty("host") - public void setHost(String host) { - this.host = host; - } - - @JsonProperty("port") - public Integer getPort() { - return this.port; - } - - @JsonProperty("port") - public void setPort(Integer port) { - this.port = port; - } - - @JsonProperty("master_eligibility") - public boolean getMasterEligibility() { return this.masterEligibility; } - - @JsonProperty("master_eligibility") - public void setMasterEligibility(Boolean eligibility) { this.masterEligibility = eligibility; } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - RegistryIdentity that = (RegistryIdentity) o; - - if (!this.version.equals(that.version)) { - return false; - } - if (!this.host.equals(that.host)) { - return false; - } - if (!this.port.equals(that.port)) { - return false; - } - if (!this.masterEligibility.equals(that.masterEligibility)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - int result = port; - result = 31 * result + host.hashCode(); - result = 31 * result + version; - result = 31 * result + masterEligibility.hashCode(); - return result; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("version=" + this.version + ","); - sb.append("host=" + this.host + ","); - sb.append("port=" + this.port + ","); - sb.append("masterEligibility=" + this.masterEligibility); - return sb.toString(); - } - - public String toJson() throws IOException { - return new ObjectMapper().writeValueAsString(this); - } -} diff --git a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/ZookeeperMasterElector.java b/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/ZookeeperMasterElector.java deleted file mode 100644 index ec22276e4..000000000 --- a/logisland-framework/logisland-agent/src/main/java/com/hurence/logisland/kafka/zookeeper/ZookeeperMasterElector.java +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.zookeeper; - - -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.registry.exceptions.*; -import kafka.utils.ZkUtils; -import org.I0Itec.zkclient.IZkDataListener; -import org.I0Itec.zkclient.IZkStateListener; -import org.I0Itec.zkclient.ZkClient; -import org.I0Itec.zkclient.exception.ZkNoNodeException; -import org.I0Itec.zkclient.exception.ZkNodeExistsException; -import org.apache.zookeeper.Watcher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -public class ZookeeperMasterElector { - - private static final Logger log = LoggerFactory.getLogger(ZookeeperMasterElector.class); - private static final String MASTER_PATH = "/schema_registry_master"; - - private final ZkClient zkClient; - private final ZkUtils zkUtils; - private final RegistryIdentity myIdentity; - private final String myIdentityString; - private final KafkaRegistry schemaRegistry; - - - public ZookeeperMasterElector(ZkUtils zkUtils, - RegistryIdentity myIdentity, - KafkaRegistry schemaRegistry, - boolean isEligibleForMasterElection) - throws RegistryTimeoutException, RegistryStoreException { - this.zkClient = zkUtils.zkClient(); - this.zkUtils = zkUtils; - this.myIdentity = myIdentity; - try { - this.myIdentityString = myIdentity.toJson(); - } catch (IOException e) { - throw new RegistryStoreException(String.format( - "Error while serializing schema registry identity %s to json", myIdentity.toString()), e); - } - this.schemaRegistry = schemaRegistry; - - zkClient.subscribeStateChanges(new SessionExpirationListener(isEligibleForMasterElection)); - zkClient.subscribeDataChanges(MASTER_PATH, - new MasterChangeListener(isEligibleForMasterElection)); - if (isEligibleForMasterElection) { - electMaster(); - } else { - readCurrentMaster(); - } - } - - public void close() { - zkClient.unsubscribeAll(); - } - - public void electMaster() throws - RegistryStoreException, RegistryTimeoutException { - RegistryIdentity masterIdentity = null; - try { - zkUtils.createEphemeralPathExpectConflict(MASTER_PATH, myIdentityString, - zkUtils.DefaultAcls()); - log.info("Successfully elected the new master: " + myIdentityString); - masterIdentity = myIdentity; - schemaRegistry.setMaster(masterIdentity); - } catch (ZkNodeExistsException znee) { - readCurrentMaster(); - } - } - - public void readCurrentMaster() - throws RegistryTimeoutException, RegistryStoreException { - RegistryIdentity masterIdentity = null; - // If someone else has written the path, read the new master back - try { - String masterIdentityString = zkUtils.readData(MASTER_PATH)._1(); - try { - masterIdentity = RegistryIdentity.fromJson(masterIdentityString); - } catch (IOException ioe) { - log.error("Can't parse schema registry identity json string " + masterIdentityString); - } - } catch (ZkNoNodeException znne) { - // NOTE: masterIdentity is already initialized to null. The master will then be updated to - // null so register requests directed to this node can throw the right error code back - } - schemaRegistry.setMaster(masterIdentity); - } - - private class MasterChangeListener implements IZkDataListener { - private final boolean isEligibleForMasterElection; - - public MasterChangeListener(boolean isEligibleForMasterElection) { - this.isEligibleForMasterElection = isEligibleForMasterElection; - } - - /** - * Called when the master information stored in ZooKeeper has changed (or, in some cases, - * deleted). - * - * ** Note ** The ZkClient library has unexpected behavior - under certain conditions, - * handleDataChange may be called instead of handleDataDeleted when the ephemeral node holding - * MASTER_PATH is deleted. Therefore it is necessary to call electMaster() here to ensure - * every eligible node participates in election after a deletion event. - * - * @throws Exception On any error. - */ - @Override - public void handleDataChange(String dataPath, Object data) { - try { - if (isEligibleForMasterElection) { - electMaster(); - } else { - readCurrentMaster(); - } - } catch (RegistryException e) { - log.error("Error while reading the schema registry master", e); - } - } - - /** - * Called when the master information stored in zookeeper has been deleted. Try to elect as the - * leader - * - * @throws Exception On any error. - */ - @Override - public void handleDataDeleted(String dataPath) throws Exception { - if (isEligibleForMasterElection) { - electMaster(); - } else { - schemaRegistry.setMaster(null); - } - } - } - - private class SessionExpirationListener implements IZkStateListener { - - private final boolean isEligibleForMasterElection; - - public SessionExpirationListener(boolean isEligibleForMasterElection) { - this.isEligibleForMasterElection = isEligibleForMasterElection; - } - - @Override - public void handleStateChanged(Watcher.Event.KeeperState state) { - // do nothing, since zkclient will do reconnect for us. - } - - /** - * Called after the zookeeper session has expired and a new session has been created. You would - * have to re-create any ephemeral nodes here. - * - * @throws Exception On any error. - */ - @Override - public void handleNewSession() throws Exception { - if (isEligibleForMasterElection) { - electMaster(); - } else { - readCurrentMaster(); - } - } - - @Override - public void handleSessionEstablishmentError(Throwable t) throws Exception { - log.error("Failed to re-establish Zookeeper connection: ", t); - throw new RegistryStoreException("Couldn't establish Zookeeper connection", t); - } - } -} diff --git a/logisland-framework/logisland-agent/src/main/resources/components.json b/logisland-framework/logisland-agent/src/main/resources/components.json deleted file mode 100644 index 2e45c727e..000000000 --- a/logisland-framework/logisland-agent/src/main/resources/components.json +++ /dev/null @@ -1,43 +0,0 @@ -[ -{"name":"AddFields","description":"Add one or more field with a default value\n...","component":"com.hurence.logisland.processor.AddFields","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field value":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, -{"name":"ApplyRegexp","description":"This processor is used to create a new set of fields from one field (using regexp).","component":"com.hurence.logisland.processor.ApplyRegexp","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"This processor is used to create a new set of fields from one field (using regexp).","isExpressionLanguageSupported":true}]}, -{"name":"BulkAddElasticsearch","description":"Indexes the content of a Record in Elasticsearch using elasticsearch's bulk processor","component":"com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.index","isRequired":true,"description":"The name of the index to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"default.type","isRequired":true,"description":"The type of this document (used by Elasticsearch for indexing and searching)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.index","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":false,"description":"the name of the event field containing es doc type => will override type value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"BulkPut","description":"Indexes the content of a Record in a Datastore using bulk processor","component":"com.hurence.logisland.processor.datastore.BulkPut","type":"processor","tags":["datastore","record","put","bulk"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.collection","isRequired":true,"description":"The name of the collection/index/table to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.collection","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"date.format","isRequired":false,"description":"simple date format for date suffix. default : yyyy.MM.dd","defaultValue":"yyyy.MM.dd","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, -{"name":"ConsolidateSession","description":"The ConsolidateSession processor is the Logisland entry point to get and process events from the Web Analytics.As an example here is an incoming event from the Web Analytics:\n\n\"fields\": [{ \"name\": \"timestamp\", \"type\": \"long\" },{ \"name\": \"remoteHost\", \"type\": \"string\"},{ \"name\": \"record_type\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"record_id\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"location\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"hitType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventCategory\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventAction\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventLabel\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"localPath\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"q\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"n\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"referer\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"viewportPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"viewportPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"partyId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"sessionId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageViewId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_newSession\", \"type\": [\"null\", \"boolean\"],\"default\": null },{ \"name\": \"userAgentString\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"UserId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"B2Bunit\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pointOfService\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"companyID\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"GroupCode\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"userRoles\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_PunchOut\", \"type\": [\"null\", \"string\"], \"default\": null }]The ConsolidateSession processor groups the records by sessions and compute the duration between now and the last received event. If the distance from the last event is beyond a given threshold (by default 30mn), then the session is considered closed.The ConsolidateSession is building an aggregated session object for each active session.This aggregated object includes: - The actual session duration. - A boolean representing wether the session is considered active or closed. Note: it is possible to ressurect a session if for instance an event arrives after a session has been marked closed. - User related infos: userId, B2Bunit code, groupCode, userRoles, companyId - First visited page: URL - Last visited page: URL The properties to configure the processor are: - sessionid.field: Property name containing the session identifier (default: sessionId). - timestamp.field: Property name containing the timestamp of the event (default: timestamp). - session.timeout: Timeframe of inactivity (in seconds) after which a session is considered closed (default: 30mn). - visitedpage.field: Property name containing the page visited by the customer (default: location). - fields.to.return: List of fields to return in the aggregated object. (default: N/A)","component":"com.hurence.logisland.processor.webAnalytics.ConsolidateSession","type":"processor","tags":["analytics","web","session"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"session.timeout","isRequired":false,"description":"session timeout in sec","defaultValue":"1800","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionid.field","isRequired":false,"description":"the name of the field containing the session id => will override default value if set","defaultValue":"sessionId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timestamp.field","isRequired":false,"description":"the name of the field containing the timestamp => will override default value if set","defaultValue":"h2kTimestamp","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"visitedpage.field","isRequired":false,"description":"the name of the field containing the visited page => will override default value if set","defaultValue":"location","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"userid.field","isRequired":false,"description":"the name of the field containing the userId => will override default value if set","defaultValue":"userId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.return","isRequired":false,"description":"the list of fields to return","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the first visited page => will override default value if set","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the last visited page => will override default value if set","defaultValue":"lastVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"isSessionActive.out.field","isRequired":false,"description":"the name of the field stating whether the session is active or not => will override default value if set","defaultValue":"is_sessionActive","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionDuration.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"sessionDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"eventsCounter.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"eventsCounter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the first event => will override default value if set","defaultValue":"firstEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the last event => will override default value if set","defaultValue":"lastEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionInactivityDuration.out.field","isRequired":false,"description":"the name of the field containing the session inactivity duration => will override default value if set","defaultValue":"sessionInactivityDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"ConvertFieldsType","description":"Converts a field value into the given type. does nothing if conversion is not possible","component":"com.hurence.logisland.processor.ConvertFieldsType","type":"processor","tags":["type","fields","update","convert"],"dynamicProperties":[{"name":"field","value":"the new type","description":"convert field value into new type","isExpressionLanguageSupported":true}]}, -{"name":"DebugStream","description":"This is a processor that logs incoming records","component":"com.hurence.logisland.processor.DebugStream","type":"processor","tags":["record","debug"],"properties":[{"name":"event.serializer","isRequired":true,"description":"the way to serialize event","Json serialization":"serialize events as json blocs","String serialization":"serialize events as toString() blocs","defaultValue":"json","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"DetectOutliers","description":"Outlier Analysis: A Hybrid Approach\n\nIn order to function at scale, a two-phase approach is taken\n\nFor every data point\n\n- Detect outlier candidates using a robust estimator of variability (e.g. median absolute deviation) that uses distributional sketching (e.g. Q-trees)\n- Gather a biased sample (biased by recency)\n- Extremely deterministic in space and cheap in computation\n\nFor every outlier candidate\n\n- Use traditional, more computationally complex approaches to outlier analysis (e.g. Robust PCA) on the biased sample\n- Expensive computationally, but run infrequently\n\nThis becomes a data filter which can be attached to a timeseries data stream within a distributed computational framework (i.e. Storm, Spark, Flink, NiFi) to detect outliers.","component":"com.hurence.logisland.processor.DetectOutliers","type":"processor","tags":["analytic","outlier","record","iot","timeseries"],"properties":[{"name":"value.field","isRequired":true,"description":"the numeric field to get the value","defaultValue":"record_value","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"time.field","isRequired":true,"description":"the numeric field to get the value","defaultValue":"record_time","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"alert_match","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rotation.policy.type","isRequired":true,"description":"...","by_amount":null,"by_time":null,"never":null,"defaultValue":"by_amount","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rotation.policy.amount","isRequired":true,"description":"...","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rotation.policy.unit","isRequired":true,"description":"...","milliseconds":null,"seconds":null,"hours":null,"days":null,"months":null,"years":null,"points":null,"defaultValue":"points","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"chunking.policy.type","isRequired":true,"description":"...","by_amount":null,"by_time":null,"never":null,"defaultValue":"by_amount","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"chunking.policy.amount","isRequired":true,"description":"...","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"chunking.policy.unit","isRequired":true,"description":"...","milliseconds":null,"seconds":null,"hours":null,"days":null,"months":null,"years":null,"points":null,"defaultValue":"points","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sketchy.outlier.algorithm","isRequired":false,"description":"...","SKETCHY_MOVING_MAD":null,"defaultValue":"SKETCHY_MOVING_MAD","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"batch.outlier.algorithm","isRequired":false,"description":"...","RAD":null,"defaultValue":"RAD","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.min","isRequired":false,"description":"minimum value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.max","isRequired":false,"description":"maximum value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.mean","isRequired":false,"description":"mean value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.stddev","isRequired":false,"description":"standard deviation value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.normal","isRequired":true,"description":"zscoreCutoffs level for normal outlier","defaultValue":"0.000000000000001","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.moderate","isRequired":true,"description":"zscoreCutoffs level for moderate outlier","defaultValue":"1.5","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.severe","isRequired":true,"description":"zscoreCutoffs level for severe outlier","defaultValue":"10.0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.notEnoughData","isRequired":false,"description":"zscoreCutoffs level for notEnoughData outlier","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smooth","isRequired":false,"description":"do smoothing ?","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"decay","isRequired":false,"description":"the decay","defaultValue":"0.1","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"min.amount.to.predict","isRequired":true,"description":"minAmountToPredict","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"min_zscore_percentile","isRequired":false,"description":"minZscorePercentile","defaultValue":"50.0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"reservoir_size","isRequired":false,"description":"the size of points reservoir","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.force.diff","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.lpenalty","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.min.records","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.spenalty","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.threshold","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"EnrichRecords","description":"Enrich input records with content indexed in datastore using multiget queries.\nEach incoming record must be possibly enriched with information stored in datastore. \nThe plugin properties are :\n- es.index (String) : Name of the datastore index on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- record.key (String) : Name of the field in the input record containing the id to lookup document in elastic search. This field is mandatory.\n- es.key (String) : Name of the datastore key on which the multiget query will be performed. This field is mandatory.\n- includes (ArrayList) : List of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (ArrayList) : List of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds at least the input record plus potentially one or more fields coming from of one datastore document.","component":"com.hurence.logisland.processor.datastore.EnrichRecords","type":"processor","tags":["datastore","enricher"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.key","isRequired":false,"description":"The name of field in the input record containing the document id to use in ES multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"includes.field","isRequired":false,"description":"The name of the ES fields to include in the record.","defaultValue":"*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"excludes.field","isRequired":false,"description":"The name of the ES fields to exclude.","defaultValue":"N/A","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"type.name","isRequired":false,"description":"The typle of record to look for","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"collection.name","isRequired":false,"description":"The name of the collection to look for","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, -{"name":"EnrichRecordsElasticsearch","description":"Enrich input records with content indexed in elasticsearch using multiget queries.\nEach incoming record must be possibly enriched with information stored in elasticsearch. \nThe plugin properties are :\n- es.index (String) : Name of the elasticsearch index on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- record.key (String) : Name of the field in the input record containing the id to lookup document in elastic search. This field is mandatory.\n- es.key (String) : Name of the elasticsearch key on which the multiget query will be performed. This field is mandatory.\n- includes (ArrayList) : List of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (ArrayList) : List of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds at least the input record plus potentially one or more fields coming from of one elasticsearch document.","component":"com.hurence.logisland.processor.elasticsearch.EnrichRecordsElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.key","isRequired":true,"description":"The name of field in the input record containing the document id to use in ES multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.index","isRequired":true,"description":"The name of the ES index to use in multiget query. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.type","isRequired":false,"description":"The name of the ES type to use in multiget query.","defaultValue":"default","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.includes.field","isRequired":false,"description":"The name of the ES fields to include in the record.","defaultValue":"*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.excludes.field","isRequired":false,"description":"The name of the ES fields to exclude.","defaultValue":"N/A","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"EvaluateJsonPath","description":"Evaluates one or more JsonPath expressions against the content of a FlowFile. The results of those expressions are assigned to Records Fields depending on configuration of the Processor. JsonPaths are entered by adding user-defined properties; the name of the property maps to the Field Name into which the result will be placed. The value of the property must be a valid JsonPath expression. A Return Type of 'auto-detect' will make a determination based off the configured destination. If the JsonPath evaluates to a JSON array or JSON object and the Return Type is set to 'scalar' the Record will be routed to error. A Return Type of JSON can return scalar values if the provided JsonPath evaluates to the specified value. If the expression matches nothing, Fields will be created with empty strings as the value ","component":"com.hurence.logisland.processor.EvaluateJsonPath","type":"processor","tags":["JSON","evaluate","JsonPath"],"dynamicProperties":[{"name":"A Record field","value":"A JsonPath expression","description":"will be set to any JSON objects that match the JsonPath. ","isExpressionLanguageSupported":false}]}, -{"name":"ExcelExtract","description":"Consumes a Microsoft Excel document and converts each worksheet's line to a structured record. The processor is assuming to receive raw excel file as input record.","component":"com.hurence.logisland.processor.excel.ExcelExtract","type":"processor","tags":["excel","processor","poi"],"properties":[{"name":"sheets","isRequired":false,"description":"Comma separated list of Excel document sheet names that should be extracted from the excel document. If this property is left blank then all of the sheets will be extracted from the Excel document. You can specify regular expressions. Any sheets not specified in this value will be ignored.","defaultValue":"","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"skip.columns","isRequired":false,"description":"Comma delimited list of column numbers to skip. Use the columns number and not the letter designation. Use this to skip over columns anywhere in your worksheet that you don't want extracted as part of the record.","defaultValue":"","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"field.names","isRequired":false,"description":"The comma separated list representing the names of columns of extracted cells. Order matters! You should use either field.names either field.row.header but not both together.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"skip.rows","isRequired":false,"description":"The row number of the first row to start processing.Use this to skip over rows of data at the top of your worksheet that are not part of the dataset.Empty rows of data anywhere in the spreadsheet will always be skipped, no matter what this value is set to.","defaultValue":"0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"Default type of record","defaultValue":"excel_record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"field.row.header","isRequired":false,"description":"If set, field names mapping will be extracted from the specified row number. You should use either field.names either field.row.header but not both together.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"FetchHBaseRow","description":"Fetches a row from an HBase table. The Destination property controls whether the cells are added as flow file attributes, or the row is written to the flow file content as JSON. This processor may be used to fetch a fixed row on a interval by specifying the table and row id directly in the processor, or it may be used to dynamically fetch rows by referencing the table and row id from incoming flow files.","component":"com.hurence.logisland.processor.hbase.FetchHBaseRow","type":"processor","tags":["hbase","scan","fetch","get","enrich"],"properties":[{"name":"hbase.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing HBase.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"table.name.field","isRequired":true,"description":"The field containing the name of the HBase Table to fetch from.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"row.identifier.field","isRequired":true,"description":"The field containing the identifier of the row to fetch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"columns.field","isRequired":false,"description":"The field containing an optional comma-separated list of \":\" pairs to fetch. To return all columns for a given family, leave off the qualifier such as \",\".","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"record.serializer","isRequired":false,"description":"the serializer needed to i/o the record in the HBase row","kryo serialization":"serialize events as json blocs","json serialization":"serialize events as json blocs","avro serialization":"serialize events as avro blocs","no serialization":"send events as bytes","defaultValue":"com.hurence.logisland.serializer.KryoSerializer","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.schema","isRequired":false,"description":"the avro schema definition for the Avro serialization","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"table.name.default","isRequired":false,"description":"The table table to use if table name field is not set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"FilterRecords","description":"Keep only records based on a given field value","component":"com.hurence.logisland.processor.FilterRecords","type":"processor","tags":["record","fields","remove","delete"],"properties":[{"name":"field.name","isRequired":true,"description":"the field name","defaultValue":"record_id","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"field.value","isRequired":true,"description":"the field value to keep","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"FlatMap","description":"Converts each field records into a single flatten record\n...","component":"com.hurence.logisland.processor.FlatMap","type":"processor","tags":["record","fields","flatmap","flatten"],"properties":[{"name":"keep.root.record","isRequired":false,"description":"do we add the original record in","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"copy.root.record.fields","isRequired":false,"description":"do we copy the original record fields into the flattened records","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"leaf.record.type","isRequired":false,"description":"the new type for the flattened records if present","defaultValue":"","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"concat.fields","isRequired":false,"description":"comma separated list of fields to apply concatenation ex : $rootField/$leaffield","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"concat.separator","isRequired":false,"description":"returns $rootField/$leaf/field","defaultValue":"/","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"include.position","isRequired":false,"description":"do we add the original record position in","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"GenerateRandomRecord","description":"This is a processor that make random records given an Avro schema","component":"com.hurence.logisland.processor.GenerateRandomRecord","type":"processor","tags":["record","avro","generator"],"properties":[{"name":"avro.output.schema","isRequired":true,"description":"the avro schema definition for the output serialization","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"min.events.count","isRequired":true,"description":"the minimum number of generated events each run","defaultValue":"10","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.events.count","isRequired":true,"description":"the maximum number of generated events each run","defaultValue":"200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"IpToFqdn","description":"Translates an IP address into a FQDN (Fully Qualified Domain Name). An input field from the record has the IP as value. An new field is created and its value is the FQDN matching the IP address. The resolution mechanism is based on the underlying operating system. The resolution request may take some time, specially if the IP address cannot be translated into a FQDN. For these reasons this processor relies on the logisland cache service so that once a resolution occurs or not, the result is put into the cache. That way, the real request for the same IP is not re-triggered during a certain period of time, until the cache entry expires. This timeout is configurable but by default a request for the same IP is not triggered before 24 hours to let the time to the underlying DNS system to be potentially updated.","component":"com.hurence.logisland.processor.enrichment.IpToFqdn","type":"processor","tags":["dns","ip","fqdn","domain","address","fqhn","reverse","resolution","enrich"],"properties":[{"name":"ip.address.field","isRequired":true,"description":"The name of the field containing the ip address to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fqdn.field","isRequired":true,"description":"The field that will contain the full qualified domain name corresponding to the ip address.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"overwrite.fqdn.field","isRequired":false,"description":"If the field should be overwritten when it already exists.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.service","isRequired":true,"description":"The name of the cache service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.max.time","isRequired":false,"description":"The amount of time, in seconds, for which a cached FQDN value is valid in the cache service. After this delay, the next new request to translate the same IP into FQDN will trigger a new reverse DNS request and the result will overwrite the entry in the cache. This allows two things: if the IP was not resolved into a FQDN, this will get a chance to obtain a FQDN if the DNS system has been updated, if the IP is resolved into a FQDN, this will allow to be more accurate if the DNS system has been updated. A value of 0 seconds disables this expiration mechanism. The default value is 84600 seconds, which corresponds to new requests triggered every day if a record with the same IP passes every day in the processor.","defaultValue":"84600","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"resolution.timeout","isRequired":false,"description":"The amount of time, in milliseconds, to wait at most for the resolution to occur. This avoids to block the stream for too much time. Default value is 1000ms. If the delay expires and no resolution could occur before, the FQDN field is not created. A special value of 0 disables the logisland timeout and the resolution request may last for many seconds if the IP cannot be translated into a FQDN by the underlying operating system. In any case, whether the timeout occurs in logisland of in the operating system, the fact that a timeout occurs is kept in the cache system so that a resolution request for the same IP will not occur before the cache entry expires.","defaultValue":"1000","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"debug","isRequired":false,"description":"If true, some additional debug fields are added. If the FQDN field is named X, a debug field named X_os_resolution_time_ms contains the resolution time in ms (using the operating system, not the cache). This field is added whether the resolution occurs or time is out. A debug field named X_os_resolution_timeout contains a boolean value to indicate if the timeout occurred. Finally, a debug field named X_from_cache contains a boolean value to indicate the origin of the FQDN field. The default value for this property is false (debug is disabled.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"IpToGeo","description":"Looks up geolocation information for an IP address. The attribute that contains the IP address to lookup must be provided in the **ip.address.field** property. By default, the geo information are put in a hierarchical structure. That is, if the name of the IP field is 'X', then the the geo attributes added by enrichment are added under a father field named X_geo. \"_geo\" is the default hierarchical suffix that may be changed with the **geo.hierarchical.suffix** property. If one wants to put the geo fields at the same level as the IP field, then the **geo.hierarchical** property should be set to false and then the geo attributes are created at the same level as him with the naming pattern X_geo_. \"_geo_\" is the default flat suffix but this may be changed with the **geo.flat.suffix** property. The IpToGeo processor requires a reference to an Ip to Geo service. This must be defined in the **iptogeo.service** property. The added geo fields are dependant on the underlying Ip to Geo service. The **geo.fields** property must contain the list of geo fields that should be created if data is available for the IP to resolve. This property defaults to \"*\" which means to add every available fields. If one only wants a subset of the fields, one must define a comma separated list of fields as a value for the **geo.fields** property. The list of the available geo fields is in the description of the **geo.fields** property.","component":"com.hurence.logisland.processor.enrichment.IpToGeo","type":"processor","tags":["geo","enrich","ip"],"properties":[{"name":"ip.address.field","isRequired":true,"description":"The name of the field containing the ip address to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"iptogeo.service","isRequired":true,"description":"The reference to the IP to Geo service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.fields","isRequired":false,"description":"Comma separated list of geo information fields to add to the record. Defaults to '*', which means to include all available fields. If a list of fields is specified and the data is not available, the geo field is not created. The geo fields are dependant on the underlying defined Ip to Geo service. The currently only supported type of Ip to Geo service is the Maxmind Ip to Geo service. This means that the currently supported list of geo fields is the following:**continent**: the identified continent for this IP address. **continent_code**: the identified continent code for this IP address. **city**: the identified city for this IP address. **latitude**: the identified latitude for this IP address. **longitude**: the identified longitude for this IP address. **location**: the identified location for this IP address, defined as Geo-point expressed as a string with the format: 'latitude,longitude'. **accuracy_radius**: the approximate accuracy radius, in kilometers, around the latitude and longitude for the location. **time_zone**: the identified time zone for this IP address. **subdivision_N**: the identified subdivision for this IP address. N is a one-up number at the end of the attribute name, starting with 0. **subdivision_isocode_N**: the iso code matching the identified subdivision_N. **country**: the identified country for this IP address. **country_isocode**: the iso code for the identified country for this IP address. **postalcode**: the identified postal code for this IP address. **lookup_micros**: the number of microseconds that the geo lookup took. The Ip to Geo service must have the lookup_micros property enabled in order to have this field available.","defaultValue":"*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.hierarchical","isRequired":false,"description":"Should the additional geo information fields be added under a hierarchical father field or not.","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.hierarchical.suffix","isRequired":false,"description":"Suffix to use for the field holding geo information. If geo.hierarchical is true, then use this suffix appended to the IP field name to define the father field name. This may be used for instance to distinguish between geo fields with various locales using many Ip to Geo service instances.","defaultValue":"_geo","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.flat.suffix","isRequired":false,"description":"Suffix to use for geo information fields when they are flat. If geo.hierarchical is false, then use this suffix appended to the IP field name but before the geo field name. This may be used for instance to distinguish between geo fields with various locales using many Ip to Geo service instances.","defaultValue":"_geo_","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.service","isRequired":true,"description":"The name of the cache service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"debug","isRequired":false,"description":"If true, an additional debug field is added. If the geo info fields prefix is X, a debug field named X_from_cache contains a boolean value to indicate the origin of the geo fields. The default value for this property is false (debug is disabled).","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"MatchIP","description":"IP address Query matching (using `Luwak )`_\n\nYou can use this processor to handle custom events matching IP address (CIDR)\nThe record sent from a matching an IP address record is tagged appropriately.\n\nA query is expressed as a lucene query against a field like for example: \n\n.. code::\n\n\tmessage:'bad exception'\n\terror_count:[10 TO *]\n\tbytes_out:5000\n\tuser_name:tom*\n\nPlease read the `Lucene syntax guide `_ for supported operations\n\n.. warning::\n\n\tdon't forget to set numeric fields property to handle correctly numeric ranges queries","component":"com.hurence.logisland.processor.MatchIP","type":"processor","tags":["analytic","percolator","record","record","query","lucene"],"properties":[{"name":"numeric.fields","isRequired":false,"description":"a comma separated string of numeric field to be matched","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"alert_match","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type.updatePolicy","isRequired":false,"description":"Record type update policy","defaultValue":"overwrite","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmatch","isRequired":false,"description":"the policy applied to match events: 'first' (default value) match events are tagged with the name and value of the first query that matched;'all' match events are tagged with all names and values of the queries that matched.","defaultValue":"first","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmiss","isRequired":false,"description":"the policy applied to miss events: 'discard' (default value) drop events that did not match any query;'forward' include also events that did not match any query.","defaultValue":"discard","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"include.input.records","isRequired":false,"description":"if set to true all the input records are copied to output","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"query","value":"some Lucene query","description":"generate a new record when this query is matched","isExpressionLanguageSupported":true}]}, -{"name":"MatchQuery","description":"Query matching based on `Luwak `_\n\nyou can use this processor to handle custom events defined by lucene queries\na new record is added to output each time a registered query is matched\n\nA query is expressed as a lucene query against a field like for example: \n\n.. code::\n\n\tmessage:'bad exception'\n\terror_count:[10 TO *]\n\tbytes_out:5000\n\tuser_name:tom*\n\nPlease read the `Lucene syntax guide `_ for supported operations\n\n.. warning::\n\n\tdon't forget to set numeric fields property to handle correctly numeric ranges queries","component":"com.hurence.logisland.processor.MatchQuery","type":"processor","tags":["analytic","percolator","record","record","query","lucene"],"properties":[{"name":"numeric.fields","isRequired":false,"description":"a comma separated string of numeric field to be matched","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"alert_match","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type.updatePolicy","isRequired":false,"description":"Record type update policy","defaultValue":"overwrite","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmatch","isRequired":false,"description":"the policy applied to match events: 'first' (default value) match events are tagged with the name and value of the first query that matched;'all' match events are tagged with all names and values of the queries that matched.","defaultValue":"first","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmiss","isRequired":false,"description":"the policy applied to miss events: 'discard' (default value) drop events that did not match any query;'forward' include also events that did not match any query.","defaultValue":"discard","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"include.input.records","isRequired":false,"description":"if set to true all the input records are copied to output","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"query","value":"some Lucene query","description":"generate a new record when this query is matched","isExpressionLanguageSupported":true}]}, -{"name":"ModifyId","description":"modify id of records or generate it following defined rules","component":"com.hurence.logisland.processor.ModifyId","type":"processor","tags":["record","id","idempotent","generate","modify"],"properties":[{"name":"id.generation.strategy","isRequired":true,"description":"the strategy to generate new Id","generate a random uid":"generate a randomUid using java library","generate a hash from fields":"generate a hash from fields","generate a string from java pattern and fields":"generate a string from java pattern and fields","generate a concatenation of type, time and a hash from fields":"generate a concatenation of type, time and a hash from fields (as for generate_hash strategy)","defaultValue":"randomUuid","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.hash","isRequired":true,"description":"the comma separated list of field names (e.g. : 'policyid,date_raw'","defaultValue":"record_raw_value","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"hash.charset","isRequired":true,"description":"the charset to use to hash id string (e.g. 'UTF-8')","defaultValue":"UTF-8","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"hash.algorithm","isRequired":true,"description":"the algorithme to use to hash id string (e.g. 'SHA-256'","SHA-384":null,"SHA-224":null,"SHA-256":null,"MD2":null,"SHA":null,"SHA-512":null,"MD5":null,"defaultValue":"SHA-256","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"java.formatter.string","isRequired":false,"description":"the format to use to build id string (e.g. '%4$2s %3$2s %2$2s %1$2s' (see java Formatter)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"language.tag","isRequired":true,"description":"the language to use to format numbers in string","aa":null,"ab":null,"ae":null,"af":null,"ak":null,"am":null,"an":null,"ar":null,"as":null,"av":null,"ay":null,"az":null,"ba":null,"be":null,"bg":null,"bh":null,"bi":null,"bm":null,"bn":null,"bo":null,"br":null,"bs":null,"ca":null,"ce":null,"ch":null,"co":null,"cr":null,"cs":null,"cu":null,"cv":null,"cy":null,"da":null,"de":null,"dv":null,"dz":null,"ee":null,"el":null,"en":null,"eo":null,"es":null,"et":null,"eu":null,"fa":null,"ff":null,"fi":null,"fj":null,"fo":null,"fr":null,"fy":null,"ga":null,"gd":null,"gl":null,"gn":null,"gu":null,"gv":null,"ha":null,"he":null,"hi":null,"ho":null,"hr":null,"ht":null,"hu":null,"hy":null,"hz":null,"ia":null,"id":null,"ie":null,"ig":null,"ii":null,"ik":null,"in":null,"io":null,"is":null,"it":null,"iu":null,"iw":null,"ja":null,"ji":null,"jv":null,"ka":null,"kg":null,"ki":null,"kj":null,"kk":null,"kl":null,"km":null,"kn":null,"ko":null,"kr":null,"ks":null,"ku":null,"kv":null,"kw":null,"ky":null,"la":null,"lb":null,"lg":null,"li":null,"ln":null,"lo":null,"lt":null,"lu":null,"lv":null,"mg":null,"mh":null,"mi":null,"mk":null,"ml":null,"mn":null,"mo":null,"mr":null,"ms":null,"mt":null,"my":null,"na":null,"nb":null,"nd":null,"ne":null,"ng":null,"nl":null,"nn":null,"no":null,"nr":null,"nv":null,"ny":null,"oc":null,"oj":null,"om":null,"or":null,"os":null,"pa":null,"pi":null,"pl":null,"ps":null,"pt":null,"qu":null,"rm":null,"rn":null,"ro":null,"ru":null,"rw":null,"sa":null,"sc":null,"sd":null,"se":null,"sg":null,"si":null,"sk":null,"sl":null,"sm":null,"sn":null,"so":null,"sq":null,"sr":null,"ss":null,"st":null,"su":null,"sv":null,"sw":null,"ta":null,"te":null,"tg":null,"th":null,"ti":null,"tk":null,"tl":null,"tn":null,"to":null,"tr":null,"ts":null,"tt":null,"tw":null,"ty":null,"ug":null,"uk":null,"ur":null,"uz":null,"ve":null,"vi":null,"vo":null,"wa":null,"wo":null,"xh":null,"yi":null,"yo":null,"za":null,"zh":null,"zu":null,"defaultValue":"en","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"MultiGet","description":"Retrieves a content from datastore using datastore multiget queries.\nEach incoming record contains information regarding the datastore multiget query that will be performed. This information is stored in record fields whose names are configured in the plugin properties (see below) :\n- collection (String) : name of the datastore collection on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- type (String) : name of the datastore type on which the multiget query will be performed. This field is not mandatory.\n- ids (String) : comma separated list of document ids to fetch. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- includes (String) : comma separated list of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (String) : comma separated list of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds data of one datastore retrieved document. This data is stored in these fields :\n- collection (same field name as the incoming record) : name of the datastore collection.\n- type (same field name as the incoming record) : name of the datastore type.\n- id (same field name as the incoming record) : retrieved document id.\n- a list of String fields containing :\n * field name : the retrieved field name\n * field value : the retrieved field value","component":"com.hurence.logisland.processor.datastore.MultiGet","type":"processor","tags":["datastore","get","multiget"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":true,"description":"the name of the incoming records field containing es collection name to use in multiget query. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"type.field","isRequired":true,"description":"the name of the incoming records field containing es type name to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"ids.field","isRequired":true,"description":"the name of the incoming records field containing es document Ids to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"includes.field","isRequired":true,"description":"the name of the incoming records field containing es includes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"excludes.field","isRequired":true,"description":"the name of the incoming records field containing es excludes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"MultiGetElasticsearch","description":"Retrieves a content indexed in elasticsearch using elasticsearch multiget queries.\nEach incoming record contains information regarding the elasticsearch multiget query that will be performed. This information is stored in record fields whose names are configured in the plugin properties (see below) :\n- index (String) : name of the elasticsearch index on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- type (String) : name of the elasticsearch type on which the multiget query will be performed. This field is not mandatory.\n- ids (String) : comma separated list of document ids to fetch. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- includes (String) : comma separated list of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (String) : comma separated list of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds data of one elasticsearch retrieved document. This data is stored in these fields :\n- index (same field name as the incoming record) : name of the elasticsearch index.\n- type (same field name as the incoming record) : name of the elasticsearch type.\n- id (same field name as the incoming record) : retrieved document id.\n- a list of String fields containing :\n * field name : the retrieved field name\n * field value : the retrieved field value","component":"com.hurence.logisland.processor.elasticsearch.MultiGetElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":true,"description":"the name of the incoming records field containing es index name to use in multiget query. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":true,"description":"the name of the incoming records field containing es type name to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.ids.field","isRequired":true,"description":"the name of the incoming records field containing es document Ids to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.includes.field","isRequired":true,"description":"the name of the incoming records field containing es includes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.excludes.field","isRequired":true,"description":"the name of the incoming records field containing es excludes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"NormalizeFields","description":"Changes the name of a field according to a provided name mapping\n...","component":"com.hurence.logisland.processor.NormalizeFields","type":"processor","tags":["record","fields","normalizer"],"properties":[{"name":"conflict.resolution.policy","isRequired":true,"description":"what to do when a field with the same name already exists ?","nothing to do":"leave record as it was","overwrite existing field":"if field already exist","keep only old field and delete the other":"keep only old field and delete the other","keep old field and new one":"creates an alias for the new field","defaultValue":"do_nothing","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative mapping","value":"a comma separated list of possible field name","description":"when a field has a name contained in the list it will be renamed with this property field name","isExpressionLanguageSupported":true}]}, -{"name":"ParseBroEvent","description":"The ParseBroEvent processor is the Logisland entry point to get and process `Bro `_ events. The `Bro-Kafka plugin `_ should be used and configured in order to have Bro events sent to Kafka. See the `Bro/Logisland tutorial `_ for an example of usage for this processor. The ParseBroEvent processor does some minor pre-processing on incoming Bro events from the Bro-Kafka plugin to adapt them to Logisland.\n\nBasically the events coming from the Bro-Kafka plugin are JSON documents with a first level field indicating the type of the event. The ParseBroEvent processor takes the incoming JSON document, sets the event type in a record_type field and sets the original sub-fields of the JSON event as first level fields in the record. Also any dot in a field name is transformed into an underscore. Thus, for instance, the field id.orig_h becomes id_orig_h. The next processors in the stream can then process the Bro events generated by this ParseBroEvent processor.\n\nAs an example here is an incoming event from Bro:\n\n{\n\n \"conn\": {\n\n \"id.resp_p\": 9092,\n\n \"resp_pkts\": 0,\n\n \"resp_ip_bytes\": 0,\n\n \"local_orig\": true,\n\n \"orig_ip_bytes\": 0,\n\n \"orig_pkts\": 0,\n\n \"missed_bytes\": 0,\n\n \"history\": \"Cc\",\n\n \"tunnel_parents\": [],\n\n \"id.orig_p\": 56762,\n\n \"local_resp\": true,\n\n \"uid\": \"Ct3Ms01I3Yc6pmMZx7\",\n\n \"conn_state\": \"OTH\",\n\n \"id.orig_h\": \"172.17.0.2\",\n\n \"proto\": \"tcp\",\n\n \"id.resp_h\": \"172.17.0.3\",\n\n \"ts\": 1487596886.953917\n\n }\n\n }\n\nIt gets processed and transformed into the following Logisland record by the ParseBroEvent processor:\n\n\"@timestamp\": \"2017-02-20T13:36:32Z\"\n\n\"record_id\": \"6361f80a-c5c9-4a16-9045-4bb51736333d\"\n\n\"record_time\": 1487597792782\n\n\"record_type\": \"conn\"\n\n\"id_resp_p\": 9092\n\n\"resp_pkts\": 0\n\n\"resp_ip_bytes\": 0\n\n\"local_orig\": true\n\n\"orig_ip_bytes\": 0\n\n\"orig_pkts\": 0\n\n\"missed_bytes\": 0\n\n\"history\": \"Cc\"\n\n\"tunnel_parents\": []\n\n\"id_orig_p\": 56762\n\n\"local_resp\": true\n\n\"uid\": \"Ct3Ms01I3Yc6pmMZx7\"\n\n\"conn_state\": \"OTH\"\n\n\"id_orig_h\": \"172.17.0.2\"\n\n\"proto\": \"tcp\"\n\n\"id_resp_h\": \"172.17.0.3\"\n\n\"ts\": 1487596886.953917","component":"com.hurence.logisland.processor.bro.ParseBroEvent","type":"processor","tags":["bro","security","IDS","NIDS"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"ParseGitlabLog","description":"The Gitlab logs processor is the Logisland entry point to get and process `Gitlab `_ logs. This allows for instance to monitor activities in your Gitlab server. The expected input of this processor are records from the production_json.log log file of Gitlab which contains JSON records. You can for instance use the `kafkacat `_ command to inject those logs into kafka and thus Logisland.","component":"com.hurence.logisland.processor.commonlogs.gitlab.ParseGitlabLog","type":"processor","tags":["logs","gitlab"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"ParseNetflowEvent","description":"The `Netflow V5 `_ processor is the Logisland entry point to process Netflow (V5) events. NetFlow is a feature introduced on Cisco routers that provides the ability to collect IP network traffic.We can distinguish 2 components:\n\n\t-Flow exporter: aggregates packets into flows and exports flow records (binary format) towards one or more flow collectors\n\n\t-Flow collector: responsible for reception, storage and pre-processing of flow data received from a flow exporter\nThe collected data are then available for analysis purpose (intrusion detection, traffic analysis...)\nNetflow are sent to kafka in order to be processed by logisland.\nIn the tutorial we will simulate Netflow traffic using `nfgen `_. this traffic will be sent to port 2055. The we rely on nifi to listen of that port for incoming netflow (V5) traffic and send them to a kafka topic. The Netflow processor could thus treat these events and generate corresponding logisland records. The following processors in the stream can then process the Netflow records generated by this processor.","component":"com.hurence.logisland.processor.netflow.ParseNetflowEvent","type":"processor","tags":["netflow","security"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"netflowevent","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"enrich.record","isRequired":false,"description":"Enrich data. If enabledthe netflow record is enriched with inferred data","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"ParseNetworkPacket","description":"The ParseNetworkPacket processor is the LogIsland entry point to parse network packets captured either off-the-wire (stream mode) or in pcap format (batch mode). In batch mode, the processor decodes the bytes of the incoming pcap record, where a Global header followed by a sequence of [packet header, packet data] pairs are stored. Then, each incoming pcap event is parsed into n packet records. The fields of packet headers are then extracted and made available in dedicated record fields. See the `Capturing Network packets tutorial `_ for an example of usage of this processor.","component":"com.hurence.logisland.processor.networkpacket.ParseNetworkPacket","type":"processor","tags":["PCap","security","IDS","NIDS"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"flow.mode","isRequired":true,"description":"Flow Mode. Indicate whether packets are provided in batch mode (via pcap files) or in stream mode (without headers). Allowed values are batch and stream.","batch":null,"stream":null,"defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"ParseProperties","description":"Parse a field made of key=value fields separated by spaces\na string like \"a=1 b=2 c=3\" will add a,b & c fields, respectively with values 1,2 & 3 to the current Record","component":"com.hurence.logisland.processor.ParseProperties","type":"processor","tags":["record","properties","parser"],"properties":[{"name":"properties.field","isRequired":true,"description":"the field containing the properties to split and treat","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"ParseUserAgent","description":"The user-agent processor allows to decompose User-Agent value from an HTTP header into several attributes of interest. There is no standard format for User-Agent strings, hence it is not easily possible to use regexp to handle them. This processor rely on the `YAUAA library `_ to do the heavy work.","component":"com.hurence.logisland.processor.useragent.ParseUserAgent","type":"processor","tags":["User-Agent","clickstream","DMP"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.enabled","isRequired":false,"description":"Enable caching. Caching to avoid to redo the same computation for many identical User-Agent strings.","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.size","isRequired":false,"description":"Set the size of the cache.","defaultValue":"1000","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"useragent.field","isRequired":true,"description":"Must contain the name of the field that contains the User-Agent value in the incoming record.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"useragent.keep","isRequired":false,"description":"Defines if the field that contained the User-Agent must be kept or not in the resulting records.","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"confidence.enabled","isRequired":false,"description":"Enable confidence reporting. Each field will report a confidence attribute with a value comprised between 0 and 10000.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"ambiguity.enabled","isRequired":false,"description":"Enable ambiguity reporting. Reports a count of ambiguities.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields","isRequired":false,"description":"Defines the fields to be returned.","defaultValue":"DeviceClass, DeviceName, DeviceBrand, DeviceCpu, DeviceFirmwareVersion, DeviceVersion, OperatingSystemClass, OperatingSystemName, OperatingSystemVersion, OperatingSystemNameVersion, OperatingSystemVersionBuild, LayoutEngineClass, LayoutEngineName, LayoutEngineVersion, LayoutEngineVersionMajor, LayoutEngineNameVersion, LayoutEngineNameVersionMajor, LayoutEngineBuild, AgentClass, AgentName, AgentVersion, AgentVersionMajor, AgentNameVersion, AgentNameVersionMajor, AgentBuild, AgentLanguage, AgentLanguageCode, AgentInformationEmail, AgentInformationUrl, AgentSecurity, AgentUuid, FacebookCarrier, FacebookDeviceClass, FacebookDeviceName, FacebookDeviceVersion, FacebookFBOP, FacebookFBSS, FacebookOperatingSystemName, FacebookOperatingSystemVersion, Anonymized, HackerAttackVector, HackerToolkit, KoboAffiliate, KoboPlatformId, IECompatibilityVersion, IECompatibilityVersionMajor, IECompatibilityNameVersion, IECompatibilityNameVersionMajor, __SyntaxError__, Carrier, GSAInstallationID, WebviewAppName, WebviewAppNameVersionMajor, WebviewAppVersion, WebviewAppVersionMajor","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"PutHBaseCell","description":"Adds the Contents of a Record to HBase as the value of a single cell","component":"com.hurence.logisland.processor.hbase.PutHBaseCell","type":"processor","tags":["hadoop","hbase"],"properties":[{"name":"hbase.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing HBase.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"table.name.field","isRequired":true,"description":"The field containing the name of the HBase Table to put data into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"row.identifier.field","isRequired":false,"description":"Specifies field containing the Row ID to use when inserting data into HBase","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"row.identifier.encoding.strategy","isRequired":false,"description":"Specifies the data type of Row ID used when inserting data into HBase. The default behavior is to convert the row id to a UTF-8 byte array. Choosing Binary will convert a binary formatted string to the correct byte[] representation. The Binary option should be used if you are using Binary row keys in HBase","String":"Stores the value of row id as a UTF-8 String.","Binary":"Stores the value of the rows id as a binary byte array. It expects that the row id is a binary formatted string.","defaultValue":"String","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"column.family.field","isRequired":true,"description":"The field containing the Column Family to use when inserting data into HBase","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"column.qualifier.field","isRequired":true,"description":"The field containing the Column Qualifier to use when inserting data into HBase","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"batch.size","isRequired":true,"description":"The maximum number of Records to process in a single execution. The Records will be grouped by table, and a single Put per table will be performed.","defaultValue":"25","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.schema","isRequired":false,"description":"the avro schema definition for the Avro serialization","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.serializer","isRequired":false,"description":"the serializer needed to i/o the record in the HBase row","kryo serialization":"serialize events as json blocs","json serialization":"serialize events as json blocs","avro serialization":"serialize events as avro blocs","no serialization":"send events as bytes","defaultValue":"com.hurence.logisland.serializer.KryoSerializer","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"table.name.default","isRequired":false,"description":"The table table to use if table name field is not set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"column.family.default","isRequired":false,"description":"The column family to use if column family field is not set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"column.qualifier.default","isRequired":false,"description":"The column qualifier to use if column qualifier field is not set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"RemoveFields","description":"Removes a list of fields defined by a comma separated list of field names","component":"com.hurence.logisland.processor.RemoveFields","type":"processor","tags":["record","fields","remove","delete"],"properties":[{"name":"fields.to.remove","isRequired":true,"description":"the comma separated list of field names (e.g. 'policyid,date_raw'","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"RunPython","description":" !!!! WARNING !!!!\n\nThe RunPython processor is currently an experimental feature : it is delivered as is, with the current set of features and is subject to modifications in API or anything else in further logisland releases without warnings. There is no tutorial yet. If you want to play with this processor, use the python-processing.yml example and send the apache logs of the index apache logs tutorial. The debug stream processor at the end of the stream should output events in stderr file of the executors from the spark console.\n\nThis processor allows to implement and run a processor written in python. This can be done in 2 ways. Either directly defining the process method code in the **script.code.process** configuration property or poiting to an external python module script file in the **script.path** configuration property. Directly defining methods is called the inline mode whereas using a script file is called the file mode. Both ways are mutually exclusive. Whether using the inline of file mode, your python code may depend on some python dependencies. If the set of python dependencies already delivered with the Logisland framework is not sufficient, you can use the **dependencies.path** configuration property to give their location. Currently only the nltk python library is delivered with Logisland.","component":"com.hurence.logisland.processor.scripting.python.RunPython","type":"processor","tags":["scripting","python"],"properties":[{"name":"script.code.imports","isRequired":false,"description":"For inline mode only. This is the python code that should hold the import statements if required.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"script.code.init","isRequired":false,"description":"The python code to be called when the processor is initialized. This is the python equivalent of the init method code for a java processor. This is not mandatory but can only be used if **script.code.process** is defined (inline mode).","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"script.code.process","isRequired":false,"description":"The python code to be called to process the records. This is the pyhton equivalent of the process method code for a java processor. For inline mode, this is the only minimum required configuration property. Using this property, you may also optionally define the **script.code.init** and **script.code.imports** properties.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"script.path","isRequired":false,"description":"The path to the user's python processor script. Use this property for file mode. Your python code must be in a python file with the following constraints: let's say your pyhton script is named MyProcessor.py. Then MyProcessor.py is a module file that must contain a class named MyProcessor which must inherits from the Logisland delivered class named AbstractProcessor. You can then define your code in the process method and in the other traditional methods (init...) as you would do in java in a class inheriting from the AbstractProcessor java class.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"dependencies.path","isRequired":false,"description":"The path to the additional dependencies for the user's python code, whether using inline or file mode. This is optional as your code may not have additional dependencies. If you defined **script.path** (so using file mode) and if **dependencies.path** is not defined, Logisland will scan a potential directory named **dependencies** in the same directory where the script file resides and if it exists, any python code located there will be loaded as dependency as needed.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"logisland.dependencies.path","isRequired":false,"description":"The path to the directory containing the python dependencies shipped with logisland. You should not have to tune this parameter.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"SampleRecords","description":"Query matching based on `Luwak `_\n\nyou can use this processor to handle custom events defined by lucene queries\na new record is added to output each time a registered query is matched\n\nA query is expressed as a lucene query against a field like for example: \n\n.. code::\n\n message:'bad exception'\n error_count:[10 TO *]\n bytes_out:5000\n user_name:tom*\n\nPlease read the `Lucene syntax guide `_ for supported operations\n\n.. warning::\n don't forget to set numeric fields property to handle correctly numeric ranges queries","component":"com.hurence.logisland.processor.SampleRecords","type":"processor","tags":["analytic","sampler","record","iot","timeseries"],"properties":[{"name":"record.value.field","isRequired":false,"description":"the name of the numeric field to sample","defaultValue":"record_value","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.time.field","isRequired":false,"description":"the name of the time field to sample","defaultValue":"record_time","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sampling.algorithm","isRequired":true,"description":"the implementation of the algorithm","none":null,"lttb":null,"average":null,"first_item":null,"min_max":null,"mode_median":null,"defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sampling.parameter","isRequired":true,"description":"the parmater of the algorithm","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"SelectDistinctRecords","description":"Keep only distinct records based on a given field","component":"com.hurence.logisland.processor.SelectDistinctRecords","type":"processor","tags":["record","fields","remove","delete"],"properties":[{"name":"field.name","isRequired":true,"description":"the field to distinct records","defaultValue":"record_id","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"SendMail","description":"The SendMail processor is aimed at sending an email (like for instance an alert email) from an incoming record. There are three ways an incoming record can generate an email according to the special fields it must embed. Here is a list of the record fields that generate a mail and how they work:\n\n- **mail_text**: this is the simplest way for generating a mail. If present, this field means to use its content (value) as the payload of the mail to send. The mail is sent in text format if there is only this special field in the record. Otherwise, used with either mail_html or mail_use_template, the content of mail_text is the aletrnative text to the HTML mail that is generated.\n\n- **mail_html**: this field specifies that the mail should be sent as HTML and the value of the field is mail payload. If mail_text is also present, its value is used as the alternative text for the mail. mail_html cannot be used with mail_use_template: only one of those two fields should be present in the record.\n\n- **mail_use_template**: If present, this field specifies that the mail should be sent as HTML and the HTML content is to be generated from the template in the processor configuration key **html.template**. The template can contain parameters which must also be present in the record as fields. See documentation of html.template for further explanations. mail_use_template cannot be used with mail_html: only one of those two fields should be present in the record.\n\n If **allow_overwrite** configuration key is true, any mail.* (dot format) configuration key may be overwritten with a matching field in the record of the form mail_* (underscore format). For instance if allow_overwrite is true and mail.to is set to config_address@domain.com, a record generating a mail with a mail_to field set to record_address@domain.com will send a mail to record_address@domain.com.\n\n Apart from error records (when he is unable to process the incoming record or to send the mail), this processor is not expected to produce any output records.","component":"com.hurence.logisland.processor.SendMail","type":"processor","tags":["smtp","email","e-mail","mail","mailer","sendmail","message","alert","html"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, debug information are written to stdout.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.server","isRequired":true,"description":"FQDN, hostname or IP address of the SMTP server to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.port","isRequired":false,"description":"TCP port number of the SMTP server to use.","defaultValue":"25","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.security.username","isRequired":false,"description":"SMTP username.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.security.password","isRequired":false,"description":"SMTP password.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.security.ssl","isRequired":false,"description":"Use SSL under SMTP or not (SMTPS). Default is false.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.from.address","isRequired":true,"description":"Valid mail sender email address.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.from.name","isRequired":false,"description":"Mail sender name.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.bounce.address","isRequired":true,"description":"Valid bounce email address (where error mail is sent if the mail is refused by the recipient server).","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.replyto.address","isRequired":false,"description":"Reply to email address.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.subject","isRequired":false,"description":"Mail subject.","defaultValue":"[LOGISLAND] Automatic email","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.to","isRequired":false,"description":"Comma separated list of email recipients. If not set, the record must have a mail_to field and allow_overwrite configuration key should be true.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow_overwrite","isRequired":false,"description":"If true, allows to overwrite processor configuration with special record fields (mail_to, mail_from_address, mail_from_name, mail_bounce_address, mail_replyto_address, mail_subject). If false, special record fields are ignored and only processor configuration keys are used.","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"html.template","isRequired":false,"description":"HTML template to use. It is used when the incoming record contains a mail_use_template field. The template may contain some parameters. The parameter format in the template is of the form ${xxx}. For instance ${param_user} in the template means that a field named param_user must be present in the record and its value will replace the ${param_user} string in the HTML template when the mail will be sent. If some parameters are declared in the template, everyone of them must be present in the record as fields, otherwise the record will generate an error record. If an incoming record contains a mail_use_template field, a template must be present in the configuration and the HTML mail format will be used. If the record also contains a mail_text field, its content will be used as an alternative text message to be used in the mail reader program of the recipient if it does not supports HTML.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"SplitField","description":"This processor is used to create a new set of fields from one field (using split).","component":"com.hurence.logisland.processor.SplitField","type":"processor","tags":["parser","split","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"split.limit","isRequired":false,"description":"Specify the maximum number of split to allow","defaultValue":"10","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"split.counter.enable","isRequired":false,"description":"Enable the counter of items returned by the split","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"split.counter.suffix","isRequired":false,"description":"Enable the counter of items returned by the split","defaultValue":"Counter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative split field","value":"another split that could match","description":"This processor is used to create a new set of fields from one field (using split).","isExpressionLanguageSupported":true}]}, -{"name":"SplitText","description":"This is a processor that is used to split a String into fields according to a given Record mapping","component":"com.hurence.logisland.processor.SplitText","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"value.regex","isRequired":true,"description":"the regex to match for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"value.fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.regex","isRequired":false,"description":"the regex to match for the message key","defaultValue":".*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.fields","isRequired":false,"description":"a comma separated list of fields corresponding to matching groups for the message key","defaultValue":"record_raw_key","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"default type of record","defaultValue":"record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"keep.raw.content","isRequired":false,"description":"do we add the initial raw content ?","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timezone.record.time","isRequired":false,"description":"what is the time zone of the string formatted date for 'record_time' field.","defaultValue":"UTC","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"this regex will be tried if the main one has not matched. It must be in the form alt.value.regex.1 and alt.value.fields.1","isExpressionLanguageSupported":true}]}, -{"name":"SplitTextMultiline","description":"No description provided.","component":"com.hurence.logisland.processor.SplitTextMultiline","type":"processor","properties":[{"name":"regex","isRequired":true,"description":"the regex to match","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"event.type","isRequired":true,"description":"the type of event","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"SplitTextWithProperties","description":"This is a processor that is used to split a String into fields according to a given Record mapping","component":"com.hurence.logisland.processor.SplitTextWithProperties","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"value.regex","isRequired":true,"description":"the regex to match for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"value.fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.regex","isRequired":false,"description":"the regex to match for the message key","defaultValue":".*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.fields","isRequired":false,"description":"a comma separated list of fields corresponding to matching groups for the message key","defaultValue":"record_raw_key","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"default type of record","defaultValue":"record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"keep.raw.content","isRequired":false,"description":"do we add the initial raw content ?","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"properties.field","isRequired":true,"description":"the field containing the properties to split and treat","defaultValue":"properties","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"this regex will be tried if the main one has not matched. It must be in the form alt.value.regex.1 and alt.value.fields.1","isExpressionLanguageSupported":true}]}, -{"name":"setSourceOfTraffic","description":"Compute the source of traffic of a web session. Users arrive at a website or application through a variety of sources, \nincluding advertising/paying campaigns, search engines, social networks, referring sites or direct access. \nWhen analysing user experience on a webshop, it is crucial to collects, processes, and reports the campaign and traffic-source data. \nTo compute the source of traffic of a web session, the user has to provide the utm_* related properties if available\ni-e: **utm_source.field**, **utm_medium.field**, **utm_campaign.field**, **utm_content.field**, **utm_term.field**)\n, the referer (**referer.field** property) and the first visited page of the session (**first.visited.page.field** property).\nBy default the source of traffic informations are placed in a flat structure (specified by the **source_of_traffic.suffix** property\n with a default value of source_of_traffic_). To work properly the setSourceOfTraffic processor needs to have access to an \nElasticsearch index containing a list of the most popular search engines and social networks. The ES index (specified by the **es.index** property) should be structured such that the _id of an ES document MUST be the name of the domain. If the domain is a search engine, the related ES doc MUST have a boolean field (default being search_engine) specified by the property **es.search_engine.field** with a value set to true. If the domain is a social network , the related ES doc MUST have a boolean field (default being social_network) specified by the property **es.social_network.field** with a value set to true. ","component":"com.hurence.logisland.processor.webAnalytics.setSourceOfTraffic","type":"processor","tags":["session","traffic","source","web","analytics"],"properties":[{"name":"referer.field","isRequired":false,"description":"Name of the field containing the referer value in the session","defaultValue":"referer","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"first.visited.page.field","isRequired":false,"description":"Name of the field containing the first visited page in the session","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_source.field","isRequired":false,"description":"Name of the field containing the utm_source value in the session","defaultValue":"utm_source","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_medium.field","isRequired":false,"description":"Name of the field containing the utm_medium value in the session","defaultValue":"utm_medium","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_campaign.field","isRequired":false,"description":"Name of the field containing the utm_campaign value in the session","defaultValue":"utm_campaign","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_content.field","isRequired":false,"description":"Name of the field containing the utm_content value in the session","defaultValue":"utm_content","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_term.field","isRequired":false,"description":"Name of the field containing the utm_term value in the session","defaultValue":"utm_term","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"source_of_traffic.suffix","isRequired":false,"description":"Suffix for the source of the traffic related fields","defaultValue":"source_of_traffic","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"source_of_traffic.hierarchical","isRequired":false,"description":"Should the additional source of trafic information fields be added under a hierarchical father field or not.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.service","isRequired":true,"description":"Name of the cache service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.validity.timeout","isRequired":false,"description":"Timeout validity (in seconds) of an entry in the cache.","defaultValue":"0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"debug","isRequired":false,"description":"If true, an additional debug field is added. If the source info fields prefix is X, a debug field named X_from_cache contains a boolean value to indicate the origin of the source fields. The default value for this property is false (debug is disabled).","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index","isRequired":true,"description":"Name of the ES index containing the list of search engines and social network. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type","isRequired":false,"description":"Name of the ES type to use.","defaultValue":"default","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.search_engine.field","isRequired":false,"description":"Name of the ES field used to specify that the domain is a search engine.","defaultValue":"search_engine","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.social_network.field","isRequired":false,"description":"Name of the ES field used to specify that the domain is a social network.","defaultValue":"social_network","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -] diff --git a/logisland-framework/logisland-agent/src/main/resources/log4j.properties b/logisland-framework/logisland-agent/src/main/resources/log4j.properties deleted file mode 100644 index 8dc58af0b..000000000 --- a/logisland-framework/logisland-agent/src/main/resources/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -log4j.rootLogger=INFO, stdout, file - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=[%d] %p %m (%c:%L)%n - -log4j.logger.kafka=ERROR, stdout -log4j.logger.org.apache.zookeeper=ERROR -log4j.logger.org.apache.kafka=ERROR, stdout -log4j.logger.org.I0Itec.zkclient=ERROR, stdout -log4j.logger.org.eclipse.jetty=ERROR, stdout -log4j.additivity.kafka.server=false -log4j.additivity.kafka.consumer.ZookeeperConsumerConnector=false - -log4j.appender.file=org.apache.log4j.RollingFileAppender -log4j.appender.file.maxBackupIndex=10 -log4j.appender.file.maxFileSize=100MB -log4j.appender.file.File=log/logisland-agent.log -log4j.appender.file.layout=org.apache.log4j.PatternLayout -log4j.appender.file.layout.ConversionPattern=[%d] %p %m (%c)%n diff --git a/logisland-framework/logisland-agent/src/main/swagger/api-swagger.yaml b/logisland-framework/logisland-agent/src/main/swagger/api-swagger.yaml deleted file mode 100644 index 4ae6c290d..000000000 --- a/logisland-framework/logisland-agent/src/main/swagger/api-swagger.yaml +++ /dev/null @@ -1,1018 +0,0 @@ -swagger: '2.0' -info: - description: REST API for logisland agent - version: v1 - title: logisland-agent - contact: - name: Thomas Bailet - email: bailet.thomas@gmail.com -host: localhost:8081 -basePath: / -schemes: - - http - - https -consumes: - - application/json -produces: - - application/json -paths: - - /: - get: - summary: the root resource - description: / entrypoint - responses: - 200: - description: OK - - /processors: - get: - tags: - - config - operationId: getProcessors - summary: get all processors - description: get all processors - responses: - "200": - description: processors - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - /metrics: - get: - tags: - - metrics - operationId: getMetrics - summary: retrieve all job metrics in Prometheus format - description: get Prometheus metrics. - have a look to https://prometheus.io/docs/instrumenting/exposition_formats/ - produces: - - text/plain - responses: - "200": - description: metrics - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - # Job API - /configs: - get: - tags: - - config - operationId: getConfig - summary: global config - description: get all global configuration properties - responses: - "200": - description: global configuration - schema: - type: array - items: - $ref: '#/definitions/Property' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - # Job API - /jobs: - get: - tags: - - job - operationId: getAllJobs - summary: get all jobs - description: retrieve all jobs (retrieve only summary fields) - responses: - "200": - description: job configuration list - schema: - type: array - items: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - post: - tags: - - job - summary: create new job - description: store a new job configuration if valid - operationId: addJob - parameters: - - name: job - in: body - description: Job to add to the store - required: true - schema: - $ref: '#/definitions/Job' - responses: - "404": - description: Job not found - "400": - description: Invalid ID supplied - "200": - description: Job successfuly created - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - /jobs/metrics: - get: - tags: - - job - summary: get job metrics - description: get the metrics of corresponding Job - operationId: getJobMetrics - parameters: - - name: count - in: query - description: max number of ites to retrieve - required: false - type: integer - default: 20 - responses: - "200": - description: job metrics - schema: - type: array - items: - $ref: '#/definitions/Metrics' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - /jobs/alerts: - get: - tags: - - job - summary: get job alerts - description: get the alerts - operationId: getJobAlerts - parameters: - - name: count - in: query - description: max number of ites to retrieve - required: false - type: integer - default: 20 - responses: - "200": - description: job metrics - schema: - type: array - items: - $ref: '#/definitions/Metrics' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - /jobs/errors: - get: - tags: - - job - summary: get last job errors - description: get the errors - operationId: getJobErrors - parameters: - - name: count - in: query - description: max number of ites to retrieve - required: false - type: integer - default: 20 - responses: - "200": - description: job errors - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - - /jobs/{jobId}: - get: - tags: - - job - summary: get job - description: get the corresponding Job definition - operationId: getJob - produces: - - application/json - - text/plain - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job definition - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - delete: - tags: - - job - summary: delete job - description: remove the corresponding Job definition and stop if its currently running - operationId: deleteJob - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "404": - description: Job not found - "400": - description: Invalid ID supplied - "200": - description: job successfully removed - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - put: - tags: - - job - summary: update job - description: update an existing job configuration if valid - operationId: updateJob - parameters: - - name: jobId - in: path - description: Job to add to the store - required: true - type: string - - name: job - in: body - description: Job to add to the store - required: true - schema: - $ref: '#/definitions/Job' - responses: - "200": - description: Job successfuly created - schema: - $ref: '#/definitions/Job' - post: - tags: - - job - summary: create new job - description: store a new job configuration if valid - operationId: addJobWithId - parameters: - - name: body - in: body - description: Job configuration to add to the store - required: true - schema: - $ref: '#/definitions/Job' - - name: jobId - in: path - description: JobId to add to the store - required: true - type: string - responses: - "404": - description: Job not found - "400": - description: Invalid ID supplied - "200": - description: Job successfuly created - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/restart: - post: - tags: - - job - summary: start job - description: start the corresponding Job definition - operationId: reStartJob - parameters: - - name: jobId - in: path - description: id of the job to restart - required: true - type: string - responses: - "200": - description: job successfuly started - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/start: - post: - tags: - - job - summary: start job - description: start the corresponding Job definition - operationId: startJob - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job successfuly started - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/shutdown: - post: - tags: - - job - summary: shutdown job - description: shutdown the running Job - operationId: shutdownJob - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job successfuly started - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/pause: - post: - tags: - - job - summary: pause job - description: pause the corresponding Job - operationId: pauseJob - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job successfuly paused - schema: - $ref: '#/definitions/Job' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/status: - get: - tags: - - job - summary: get job status - description: get the status of corresponding Job - operationId: getJobStatus - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job status - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/version: - get: - tags: - - job - summary: get job version - description: get the version of corresponding Job - operationId: getJobVersion - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job version - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /jobs/{jobId}/engine: - get: - tags: - - job - - engine - summary: get job engine configuration - description: this is usefull when you want to launch a spark app within YARN to retrieve the launching config before submitting the job itself - produces: - - text/plain - operationId: getJobEngine - parameters: - - name: jobId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: job status - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - - # Topics API - /topics: - get: - tags: - - topic - summary: get all topics - operationId: getAllTopics - parameters: [] - responses: - '200': - description: Status 200 - schema: - type: array - items: - $ref: '#/definitions/Topic' - post: - tags: - - topic - summary: create new topic - operationId: addNewTopic - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/Topic' - responses: - '200': - description: Status 200 - /topics/{topicId}: - get: - tags: - - topic - summary: get topic - operationId: getTopic - parameters: - - name: topicId - in: path - required: true - type: string - responses: - "200": - description: Status 200 - schema: - $ref: '#/definitions/Topic' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - put: - tags: - - topic - summary: update topic - operationId: updateTopic - parameters: - - in: body - name: body - required: true - schema: - $ref: '#/definitions/Topic' - - name: topicId - in: path - required: true - type: string - responses: - "200": - description: job successfuly started - schema: - $ref: '#/definitions/Topic' - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - delete: - tags: - - topic - summary: delete topic - description: remove a topic config and remove all content from Kafka - operationId: deleteTopic - parameters: - - name: topicId - in: path - required: true - type: string - responses: - "200": - description: topic successfully deleted - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /topics/{topicId}/keySchema: - get: - tags: - - schema - summary: get topic key schema - operationId: getTopicKeySchema - parameters: - - name: topicId - in: path - required: true - type: string - - name: version - in: query - description: version of the schema ("latest" if not provided) - required: false - type: string - default: latest - responses: - "200": - description: Avro schema - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - put: - tags: - - schema - summary: update topic key schema - operationId: updateTopicKeySchema - parameters: - - name: body - in: body - description: schema to add to the store - required: true - schema: - type: string - - name: topicId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: Avro schema - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /topics/{topicId}/keySchema/checkCompatibility: - post: - tags: - - schema - summary: check topic key schema compatibility - operationId: checkTopicKeySchemaCompatibility - parameters: - - name: body - in: body - description: Avro schema as a json string - required: true - schema: - type: string - - name: topicId - in: path - description: id of the job to return - required: true - type: string - responses: - "200": - description: compatibility level - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /topics/{topicId}/valueSchema: - get: - tags: - - schema - summary: get topic value schema - operationId: getTopicValueSchema - parameters: - - name: topicId - in: path - description: id of the job to return - required: true - type: string - - name: version - in: query - description: version of the schema ("latest" if not provided) - required: false - type: string - default: latest - responses: - "200": - description: job definition - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - put: - tags: - - schema - summary: update topic value schema - operationId: updateTopicValueSchema - parameters: - - name: body - in: body - description: Avro schema as a json string - required: true - schema: - type: string - - name: topicId - in: path - description: id of the job to return - required: true - type: string - - responses: - "200": - description: Avro schema - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - /topics/{topicId}/valueSchema/checkCompatibility: - post: - tags: - - schema - summary: check topic value schema compatibility - operationId: checkTopicValueSchemaCompatibility - parameters: - - name: topicId - in: path - description: id of the job to return - required: true - type: string - - name: body - in: body - description: Avro schema as a json string - required: true - schema: - type: string - responses: - "200": - description: compatibility level - schema: - type: string - default: - description: unexpected error - schema: - $ref: '#/definitions/Error' - -definitions: - - - FieldType: - type: object - required: - - name - - type - properties: - name: - description: a unique identifier for the topic - type: string - encrypted: - description: is the field need to be encrypted - type: boolean - default: false - indexed: - description: is the field need to be indexed to search store - type: boolean - default: true - persistent: - description: is the field need to be persisted to data store - type: boolean - default: true - optional: - description: is the field mandatory - type: boolean - default: true - type: - description: the type of the field - type: string - default: string - enum: - - string - - int - - long - - array - - float - - double - - bytes - - record - - map - - enum - - boolean - - - - - Metrics: - type: object - properties: - spark_app_name: - type: string - spark_partition_id: - type: integer - component_name: - type: string - input_topics: - type: string - output_topics: - type: string - topic_offset_from: - type: integer - format: int64 - topic_offset_until: - type: integer - format: int64 - num_incoming_messages: - type: integer - num_incoming_records: - type: integer - num_outgoing_records: - type: integer - num_errors_records: - type: integer - format: int64 - error_percentage: - type: number - format: float - average_bytes_per_field: - type: integer - average_bytes_per_second: - type: integer - average_num_records_per_second: - type: integer - average_fields_per_record: - type: integer - average_bytes_per_record: - type: integer - total_bytes: - type: integer - total_fields: - type: integer - total_processing_time_in_ms: - type: integer - format: int64 - - - Topic: - type: object - required: - - name - - partitions - - replicationFactor - - valueSchema - - serializer - properties: - id: - description: a unique identifier for the topic - type: integer - format: int64 - version: - description: the version of the topic configuration - type: integer - format: int32 - name: - description: the name of the topic - type: string - partitions: - description: default number of partitions - type: integer - format: int32 - replicationFactor: - description: default replication factor - type: integer - format: int32 - dateModified: - description: latest date of modification - type: string - format: date-time - documentation: - description: the description of the topic - type: string - serializer: - description: the class of the Serializer - type: string - default: "com.hurence.logisland.serializer.KryoSerializer" - businessTimeField: - description: the record_time field - type: string - default: "record_time" - rowkeyField: - description: the record_id field - type: string - default: "record_id" - recordTypeField: - description: the record type field - type: string - default: "record_type" - keySchema: - type: array - items: - $ref: '#/definitions/FieldType' - valueSchema: - type: array - items: - $ref: '#/definitions/FieldType' - - - JobSummary: - type: object - properties: - usedCores: - description: the number of used cores - type: integer - format: int32 - usedMemory: - description: the total memory allocated for this job - type: integer - format: int32 - status: - description: the job status - type: string - default: stopped - enum: - - stopped - - running - - failed - - paused - dateModified: - description: latest date of modification - type: string - format: date-time - documentation: - description: write here what the job is doing - type: string - - Job: - type: object - required: - - engine - - name - - streams - - version - properties: - id: - description: a unique identifier for the job - type: integer - format: int64 - version: - description: the version of the job configuration - type: integer - format: int32 - name: - description: the job name - type: string - summary: - $ref: '#/definitions/JobSummary' - engine: - $ref: '#/definitions/Engine' - streams: - type: array - items: - $ref: '#/definitions/Stream' - - Engine: - type: object - required: - - component - - config - - name - properties: - name: - type: string - component: - type: string - config: - type: array - items: - $ref: '#/definitions/Property' - - Stream: - type: object - required: - - component - - name - properties: - name: - type: string - component: - type: string - documentation: - type: string - config: - type: array - items: - $ref: '#/definitions/Property' - processors: - type: array - items: - $ref: '#/definitions/Processor' - - Processor: - type: object - required: - - component - - config - - name - properties: - name: - type: string - component: - type: string - documentation: - type: string - config: - type: array - items: - $ref: '#/definitions/Property' - - Property: - type: object - required: - - key - - value - properties: - key: - type: string - type: - type: string - default: "string" - value: - type: string - - - Error: - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string diff --git a/logisland-framework/logisland-agent/src/main/swagger/templates/api.mustache b/logisland-framework/logisland-agent/src/main/swagger/templates/api.mustache deleted file mode 100644 index 526906f7d..000000000 --- a/logisland-framework/logisland-agent/src/main/swagger/templates/api.mustache +++ /dev/null @@ -1,66 +0,0 @@ -// hola -package {{package}}; - -import {{modelPackage}}.*; -import {{package}}.{{classname}}Service; -import {{package}}.factories.{{classname}}ServiceFactory; - -import io.swagger.annotations.ApiParam; - -{{#useBeanValidation}} - import javax.validation.constraints.*; -{{/useBeanValidation}} - -{{#imports}}import {{import}}; -{{/imports}} - -import java.util.List; -import {{package}}.NotFoundException; - -import java.io.InputStream; - - -import javax.ws.rs.core.Context; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -import javax.ws.rs.*; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -@Path("/{{baseName}}") -{{#hasConsumes}}@Consumes({ {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}} -{{#hasProduces}}@Produces({ {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}} -@io.swagger.annotations.Api(description = "the {{baseName}} API") -{{>generatedAnnotation}} -{{#operations}} -public class {{classname}} { - - private final {{classname}}Service delegate; - - public {{classname}}(KafkaRegistry kafkaRegistry) { - this.delegate = {{classname}}ServiceFactory.get{{classname}}(kafkaRegistry); - } - - {{#operation}} - @{{httpMethod}} - {{#subresourceOperation}}@Path("{{path}}"){{/subresourceOperation}} - {{#hasConsumes}}@Consumes({ {{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}} }){{/hasConsumes}} - {{#hasProduces}}@Produces({ {{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}} }){{/hasProduces}} - @io.swagger.annotations.ApiOperation(value = "{{{summary}}}", notes = "{{{notes}}}", response = {{{returnType}}}.class{{#returnContainer}}, responseContainer = "{{{returnContainer}}}"{{/returnContainer}}{{#hasAuthMethods}}, authorizations = { - {{#authMethods}}@io.swagger.annotations.Authorization(value = "{{name}}"{{#isOAuth}}, scopes = { - {{#scopes}}@io.swagger.annotations.AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{#hasMore}}, - {{/hasMore}}{{/scopes}} - }{{/isOAuth}}){{#hasMore}}, - {{/hasMore}}{{/authMethods}} - }{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{#hasMore}}, {{/hasMore}}{{/vendorExtensions.x-tags}} }) - @io.swagger.annotations.ApiResponses(value = { {{#responses}} - @io.swagger.annotations.ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{returnType}}}.class{{#returnContainer}}, responseContainer = "{{{returnContainer}}}"{{/returnContainer}}){{#hasMore}},{{/hasMore}}{{/responses}} }) - public Response {{nickname}}( - {{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}, - {{/allParams}}@Context SecurityContext securityContext) - throws NotFoundException { - return delegate.{{nickname}}({{#allParams}}{{#isFile}}inputStream, fileDetail{{/isFile}}{{^isFile}}{{paramName}}{{/isFile}},{{/allParams}}securityContext); - } - {{/operation}} - } -{{/operations}} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/swagger/templates/apiService.mustache b/logisland-framework/logisland-agent/src/main/swagger/templates/apiService.mustache deleted file mode 100644 index 05d6a1ccf..000000000 --- a/logisland-framework/logisland-agent/src/main/swagger/templates/apiService.mustache +++ /dev/null @@ -1,39 +0,0 @@ -package {{package}}; - -import {{package}}.*; -import {{modelPackage}}.*; - - - -{{#imports}}import {{import}}; -{{/imports}} - -import java.util.List; -import {{package}}.NotFoundException; - -import java.io.InputStream; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -{{#useBeanValidation}} - import javax.validation.constraints.*; -{{/useBeanValidation}} -{{>generatedAnnotation}} -{{#operations}} -public abstract class {{classname}}Service { - - protected final KafkaRegistry kafkaRegistry; - - public {{classname}}Service(KafkaRegistry kafkaRegistry) { - this.kafkaRegistry = kafkaRegistry; - } - {{#operation}} - public abstract Response {{nickname}}({{#allParams}}{{>serviceQueryParams}}{{>servicePathParams}}{{>serviceHeaderParams}}{{>serviceBodyParams}}{{>serviceFormParams}},{{/allParams}}SecurityContext securityContext) - throws NotFoundException; - {{/operation}} - } -{{/operations}} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceFactory.mustache b/logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceFactory.mustache deleted file mode 100644 index daf20f441..000000000 --- a/logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceFactory.mustache +++ /dev/null @@ -1,18 +0,0 @@ -package {{package}}.factories; - -import {{package}}.{{classname}}Service; -import {{package}}.impl.{{classname}}ServiceImpl; - -import com.hurence.logisland.kafka.registry.KafkaRegistry; - -{{>generatedAnnotation}} -public class {{classname}}ServiceFactory { - private static {{classname}}Service service = null; - - public static {{classname}}Service get{{classname}}(KafkaRegistry kafkaRegistry) { - if (service == null) { - service = new {{classname}}ServiceImpl(kafkaRegistry); - } - return service; - } -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceImpl.mustache b/logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceImpl.mustache deleted file mode 100644 index c78d6a0c8..000000000 --- a/logisland-framework/logisland-agent/src/main/swagger/templates/apiServiceImpl.mustache +++ /dev/null @@ -1,38 +0,0 @@ -package {{package}}.impl; - -import {{package}}.*; -import {{modelPackage}}.*; - -{{#imports}}import {{import}}; -{{/imports}} - -import java.util.List; -import {{package}}.NotFoundException; - -import java.io.InputStream; -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataContentDisposition; - -import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; -{{#useBeanValidation}} - import javax.validation.constraints.*; -{{/useBeanValidation}} -{{>generatedAnnotation}} -{{#operations}} - public class {{classname}}ServiceImpl extends {{classname}}Service { - - public {{classname}}ServiceImpl(KafkaRegistry kafkaRegistry) { - super(kafkaRegistry); - } - - {{#operation}} - @Override - public Response {{nickname}}({{#allParams}}{{>serviceQueryParams}}{{>servicePathParams}}{{>serviceHeaderParams}}{{>serviceBodyParams}}{{>serviceFormParams}}, {{/allParams}}SecurityContext securityContext) throws NotFoundException { - // do some magic! - return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); - } - {{/operation}} - } -{{/operations}} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/main/webapp/WEB-INF/web.xml b/logisland-framework/logisland-agent/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 46361c3e3..000000000 --- a/logisland-framework/logisland-agent/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - jersey - org.glassfish.jersey.servlet.ServletContainer - - jersey.config.server.provider.packages - - io.swagger.jaxrs.listing, - io.swagger.sample.resource, - com.hurence.logisland.agent.rest.api - - - - jersey.config.server.provider.classnames - org.glassfish.jersey.media.multipart.MultiPartFeature - - - jersey.config.server.wadl.disableWadl - true - - 1 - - - - Jersey2Config - io.swagger.jersey.config.JerseyJaxrsConfig - - api.version - 1.0.0 - - - swagger.api.title - Swagger Server - - - swagger.api.basepath - http://localhost:8080/logisland/api/v1 - - - 2 - - - Bootstrap - com.hurence.logisland.agent.rest.api.Bootstrap - 2 - - - jersey - /logisland/api/v1/* - - - ApiOriginFilter - com.hurence.logisland.agent.rest.api.ApiOriginFilter - - - ApiOriginFilter - /* - - diff --git a/logisland-framework/logisland-agent/src/test/avro/extended_user.avsc b/logisland-framework/logisland-agent/src/test/avro/extended_user.avsc deleted file mode 100644 index f24e44427..000000000 --- a/logisland-framework/logisland-agent/src/test/avro/extended_user.avsc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "namespace": "io.confluent.kafka.example", - "doc": "Extending the User type to test projection", - "type": "record", - "name": "ExtendedUser", - "fields": [ - { - "name": "name", - "type": "string" - }, - { - "name": "age", - "type": "int" - } - ] -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/test/avro/user.avsc b/logisland-framework/logisland-agent/src/test/avro/user.avsc deleted file mode 100644 index 951f8f375..000000000 --- a/logisland-framework/logisland-agent/src/test/avro/user.avsc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "namespace": "io.confluent.kafka.example", - "type": "record", - "name": "User", - "fields": [ - { - "name": "name", - "type": "string" - } - ] -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationTest.java deleted file mode 100644 index ac15d771c..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.utils; - -import org.junit.Assert; -import org.junit.Test; - - -public class YarnApplicationTest { - - - private static String sample = " application_1484246503127_0024\t SaveToHDFS\t SPARK\t hurence\t default\t RUNNING\t UNDEFINED\t 10%\t http://10.91.84.219:4051\n"; - - @Test - public void construct() throws Exception { - YarnApplication app = new YarnApplication(sample); - Assert.assertEquals(app.getId(), "application_1484246503127_0024"); - Assert.assertEquals(app.getName(), "SaveToHDFS"); - Assert.assertEquals(app.getType(), "SPARK"); - Assert.assertEquals(app.getUser(), "hurence"); - Assert.assertEquals(app.getYarnQueue(), "default"); - Assert.assertEquals(app.getState(), "RUNNING"); - Assert.assertEquals(app.getFinalState(), "UNDEFINED"); - Assert.assertEquals(app.getProgress(), "10%"); - Assert.assertEquals(app.getTrackingUrl(), "http://10.91.84.219:4051"); - } - -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationWrapperTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationWrapperTest.java deleted file mode 100644 index b98fdb912..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/agent/utils/YarnApplicationWrapperTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.agent.utils; - -import org.junit.Assert; -import org.junit.Test; - - -public class YarnApplicationWrapperTest { - - private static final String sample = "17/03/15 10:46:03 INFO impl.TimelineClientImpl: Timeline service address: http://sd-79372.dedibox.fr:8188/ws/v1/timeline/\n" + - "17/03/15 10:46:03 INFO client.AHSProxy: Connecting to Application History server at sd-79372.dedibox.fr/10.91.58.228:10200\n" + - "17/03/15 10:46:04 WARN ipc.Client: Failed to connect to server: sd-79372.dedibox.fr/10.91.58.228:8032: retries get failed due to exceeded maximum allowed retries number: 0\n" + - "java.net.ConnectException: Connection refused\n" + - "\tat sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)\n" + - "\tat sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:717)\n" + - "\tat org.apache.hadoop.net.SocketIOWithTimeout.connect(SocketIOWithTimeout.java:206)\n" + - "\tat org.apache.hadoop.net.NetUtils.connect(NetUtils.java:531)\n" + - "\tat org.apache.hadoop.net.NetUtils.connect(NetUtils.java:495)\n" + - "\tat org.apache.hadoop.ipc.Client$Connection.setupConnection(Client.java:650)\n" + - "\tat org.apache.hadoop.ipc.Client$Connection.setupIOstreams(Client.java:745)\n" + - "\tat org.apache.hadoop.ipc.Client$Connection.access$3200(Client.java:397)\n" + - "\tat org.apache.hadoop.ipc.Client.getConnection(Client.java:1618)\n" + - "\tat org.apache.hadoop.ipc.Client.call(Client.java:1449)\n" + - "\tat org.apache.hadoop.ipc.Client.call(Client.java:1396)\n" + - "\tat org.apache.hadoop.ipc.ProtobufRpcEngine$Invoker.invoke(ProtobufRpcEngine.java:233)\n" + - "\tat com.sun.proxy.$Proxy17.getApplications(Unknown Source)\n" + - "\tat org.apache.hadoop.yarn.api.impl.pb.client.ApplicationClientProtocolPBClientImpl.getApplications(ApplicationClientProtocolPBClientImpl.java:251)\n" + - "\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n" + - "\tat sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n" + - "\tat sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n" + - "\tat java.lang.reflect.Method.invoke(Method.java:497)\n" + - "\tat org.apache.hadoop.io.retry.RetryInvocationHandler.invokeMethod(RetryInvocationHandler.java:278)\n" + - "\tat org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:194)\n" + - "\tat org.apache.hadoop.io.retry.RetryInvocationHandler.invoke(RetryInvocationHandler.java:176)\n" + - "\tat com.sun.proxy.$Proxy18.getApplications(Unknown Source)\n" + - "\tat org.apache.hadoop.yarn.client.api.impl.YarnClientImpl.getApplications(YarnClientImpl.java:484)\n" + - "\tat org.apache.hadoop.yarn.client.cli.ApplicationCLI.listApplications(ApplicationCLI.java:401)\n" + - "\tat org.apache.hadoop.yarn.client.cli.ApplicationCLI.run(ApplicationCLI.java:207)\n" + - "\tat org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:76)\n" + - "\tat org.apache.hadoop.util.ToolRunner.run(ToolRunner.java:90)\n" + - "\tat org.apache.hadoop.yarn.client.cli.ApplicationCLI.main(ApplicationCLI.java:83)\n" + - "17/03/15 10:46:04 INFO client.ConfiguredRMFailoverProxyProvider: Failing over to rm2\n" + - "Total number of applications (application-types: [] and states: [SUBMITTED, ACCEPTED, RUNNING]):2\n" + - " Application-Id\t Application-Name\t Application-Type\t User\t Queue\t State\t Final-State\t Progress\t Tracking-URL\n" + - "application_1484246503127_0024\t SaveToHDFS\t SPARK\t hurence\t default\t RUNNING\t UNDEFINED\t 10%\t http://10.91.84.219:4051\n" + - "application_1489079367586_0017\t IndexApacheLogsDemo\t SPARK\t hurence\t default\t RUNNING\t UNDEFINED\t 10%\t http://10.91.84.214:4050"; - - - @Test - public void getApplication() throws Exception { - YarnApplicationWrapper wrapper = new YarnApplicationWrapper(sample); - - YarnApplication app = wrapper.getApplication("SaveToHDFS"); - Assert.assertEquals(app.getId(), "application_1484246503127_0024"); - Assert.assertEquals(app.getName(), "SaveToHDFS"); - Assert.assertEquals(app.getType(), "SPARK"); - Assert.assertEquals(app.getUser(), "hurence"); - Assert.assertEquals(app.getYarnQueue(), "default"); - Assert.assertEquals(app.getState(), "RUNNING"); - Assert.assertEquals(app.getFinalState(), "UNDEFINED"); - Assert.assertEquals(app.getProgress(), "10%"); - Assert.assertEquals(app.getTrackingUrl(), "http://10.91.84.219:4051"); - - YarnApplication app2 = wrapper.getApplication("IndexApacheLogsDemo"); - Assert.assertEquals(app2.getId(), "application_1489079367586_0017"); - Assert.assertEquals(app2.getName(), "IndexApacheLogsDemo"); - Assert.assertEquals(app2.getType(), "SPARK"); - Assert.assertEquals(app2.getUser(), "hurence"); - Assert.assertEquals(app2.getYarnQueue(), "default"); - Assert.assertEquals(app2.getState(), "RUNNING"); - Assert.assertEquals(app2.getFinalState(), "UNDEFINED"); - Assert.assertEquals(app2.getProgress(), "10%"); - Assert.assertEquals(app2.getTrackingUrl(), "http://10.91.84.214:4050"); - } - -} \ No newline at end of file diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/AvroCompatibilityTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/AvroCompatibilityTest.java deleted file mode 100644 index ef9f9843e..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/AvroCompatibilityTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.avro; - - -import org.apache.avro.Schema; -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class AvroCompatibilityTest { - - @Test - public void testBasicCompatibility() { - String schemaString1 = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":\"string\",\"name\":\"f1\"}]}"; - Schema schema1 = AvroUtils.parseSchema(schemaString1).schemaObj; - - String schemaString2 = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":\"string\",\"name\":\"f1\"}," - + " {\"type\":\"string\",\"name\":\"f2\", \"default\": \"foo\"}]}"; - Schema schema2 = AvroUtils.parseSchema(schemaString2).schemaObj; - - String schemaString3 = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":\"string\",\"name\":\"f1\"}," - + " {\"type\":\"string\",\"name\":\"f2\"}]}"; - Schema schema3 = AvroUtils.parseSchema(schemaString3).schemaObj; - - String schemaString4 = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":\"string\",\"name\":\"f1_new\", \"aliases\": [\"f1\"]}]}"; - Schema schema4 = AvroUtils.parseSchema(schemaString4).schemaObj; - - String schemaString6 = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":[\"null\", \"string\"],\"name\":\"f1\"," - + " \"doc\":\"doc of f1\"}]}"; - Schema schema6 = AvroUtils.parseSchema(schemaString6).schemaObj; - - String schemaString7 = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":[\"null\", \"string\", \"int\"],\"name\":\"f1\"," - + " \"doc\":\"doc of f1\"}]}"; - Schema schema7 = AvroUtils.parseSchema(schemaString7).schemaObj; - - AvroCompatibilityChecker backwardChecker = AvroCompatibilityChecker.BACKWARD_CHECKER; - assertTrue("adding a field with default is a backward compatible change", - backwardChecker.isCompatible(schema2, schema1)); - assertFalse("adding a field w/o default is not a backward compatible change", - backwardChecker.isCompatible(schema3, schema1)); - assertFalse("changing field name is not a backward compatible change", - backwardChecker.isCompatible(schema4, schema1)); - assertTrue("evolving a field type to a union is a backward compatible change", - backwardChecker.isCompatible(schema6, schema1)); - assertFalse("removing a type from a union is not a backward compatible change", - backwardChecker.isCompatible(schema1, schema6)); - assertTrue("adding a new type in union is a backward compatible change", - backwardChecker.isCompatible(schema7, schema6)); - assertFalse("removing a type from a union is not a backward compatible change", - backwardChecker.isCompatible(schema6, schema7)); - - AvroCompatibilityChecker forwardChecker = AvroCompatibilityChecker.FORWARD_CHECKER; - assertTrue("adding a field is a forward compatible change", - forwardChecker.isCompatible(schema2, schema1)); - - AvroCompatibilityChecker fullChecker = AvroCompatibilityChecker.FULL_CHECKER; - assertTrue("adding a field with default is a backward and a forward compatible change", - fullChecker.isCompatible(schema2, schema1)); - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/ExtendedUser.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/ExtendedUser.java deleted file mode 100644 index 24ec091af..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/ExtendedUser.java +++ /dev/null @@ -1,211 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Autogenerated by Avro - * - * DO NOT EDIT DIRECTLY - */ -package com.hurence.logisland.avro.example; -@SuppressWarnings("all") -/** Extending the User type to test projection */ -@org.apache.avro.specific.AvroGenerated -public class ExtendedUser extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { - public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"ExtendedUser\",\"namespace\":\"io.confluent.kafka.example\",\"doc\":\"Extending the User type to test projection\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"age\",\"type\":\"int\"}]}"); - public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } - @Deprecated public java.lang.CharSequence name; - @Deprecated public int age; - - /** - * Default constructor. Note that this does not initialize fields - * to their default values from the schema. If that is desired then - * one should use newBuilder(). - */ - public ExtendedUser() {} - - /** - * All-args constructor. - */ - public ExtendedUser(java.lang.CharSequence name, java.lang.Integer age) { - this.name = name; - this.age = age; - } - - public org.apache.avro.Schema getSchema() { return SCHEMA$; } - // Used by DatumWriter. Applications should not call. - public java.lang.Object get(int field$) { - switch (field$) { - case 0: return name; - case 1: return age; - default: throw new org.apache.avro.AvroRuntimeException("Bad index"); - } - } - // Used by DatumReader. Applications should not call. - @SuppressWarnings(value="unchecked") - public void put(int field$, java.lang.Object value$) { - switch (field$) { - case 0: name = (java.lang.CharSequence)value$; break; - case 1: age = (java.lang.Integer)value$; break; - default: throw new org.apache.avro.AvroRuntimeException("Bad index"); - } - } - - /** - * Gets the value of the 'name' field. - */ - public java.lang.CharSequence getName() { - return name; - } - - /** - * Sets the value of the 'name' field. - * @param value the value to set. - */ - public void setName(java.lang.CharSequence value) { - this.name = value; - } - - /** - * Gets the value of the 'age' field. - */ - public java.lang.Integer getAge() { - return age; - } - - /** - * Sets the value of the 'age' field. - * @param value the value to set. - */ - public void setAge(java.lang.Integer value) { - this.age = value; - } - - /** Creates a new ExtendedUser RecordBuilder */ - public static com.hurence.logisland.avro.example.ExtendedUser.Builder newBuilder() { - return new com.hurence.logisland.avro.example.ExtendedUser.Builder(); - } - - /** Creates a new ExtendedUser RecordBuilder by copying an existing Builder */ - public static com.hurence.logisland.avro.example.ExtendedUser.Builder newBuilder(com.hurence.logisland.avro.example.ExtendedUser.Builder other) { - return new com.hurence.logisland.avro.example.ExtendedUser.Builder(other); - } - - /** Creates a new ExtendedUser RecordBuilder by copying an existing ExtendedUser instance */ - public static com.hurence.logisland.avro.example.ExtendedUser.Builder newBuilder(com.hurence.logisland.avro.example.ExtendedUser other) { - return new com.hurence.logisland.avro.example.ExtendedUser.Builder(other); - } - - /** - * RecordBuilder for ExtendedUser instances. - */ - public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase - implements org.apache.avro.data.RecordBuilder { - - private java.lang.CharSequence name; - private int age; - - /** Creates a new Builder */ - private Builder() { - super(com.hurence.logisland.avro.example.ExtendedUser.SCHEMA$); - } - - /** Creates a Builder by copying an existing Builder */ - private Builder(com.hurence.logisland.avro.example.ExtendedUser.Builder other) { - super(other); - if (isValidValue(fields()[0], other.name)) { - this.name = data().deepCopy(fields()[0].schema(), other.name); - fieldSetFlags()[0] = true; - } - if (isValidValue(fields()[1], other.age)) { - this.age = data().deepCopy(fields()[1].schema(), other.age); - fieldSetFlags()[1] = true; - } - } - - /** Creates a Builder by copying an existing ExtendedUser instance */ - private Builder(com.hurence.logisland.avro.example.ExtendedUser other) { - super(com.hurence.logisland.avro.example.ExtendedUser.SCHEMA$); - if (isValidValue(fields()[0], other.name)) { - this.name = data().deepCopy(fields()[0].schema(), other.name); - fieldSetFlags()[0] = true; - } - if (isValidValue(fields()[1], other.age)) { - this.age = data().deepCopy(fields()[1].schema(), other.age); - fieldSetFlags()[1] = true; - } - } - - /** Gets the value of the 'name' field */ - public java.lang.CharSequence getName() { - return name; - } - - /** Sets the value of the 'name' field */ - public com.hurence.logisland.avro.example.ExtendedUser.Builder setName(java.lang.CharSequence value) { - validate(fields()[0], value); - this.name = value; - fieldSetFlags()[0] = true; - return this; - } - - /** Checks whether the 'name' field has been set */ - public boolean hasName() { - return fieldSetFlags()[0]; - } - - /** Clears the value of the 'name' field */ - public com.hurence.logisland.avro.example.ExtendedUser.Builder clearName() { - name = null; - fieldSetFlags()[0] = false; - return this; - } - - /** Gets the value of the 'age' field */ - public java.lang.Integer getAge() { - return age; - } - - /** Sets the value of the 'age' field */ - public com.hurence.logisland.avro.example.ExtendedUser.Builder setAge(int value) { - validate(fields()[1], value); - this.age = value; - fieldSetFlags()[1] = true; - return this; - } - - /** Checks whether the 'age' field has been set */ - public boolean hasAge() { - return fieldSetFlags()[1]; - } - - /** Clears the value of the 'age' field */ - public com.hurence.logisland.avro.example.ExtendedUser.Builder clearAge() { - fieldSetFlags()[1] = false; - return this; - } - - @Override - public ExtendedUser build() { - try { - ExtendedUser record = new ExtendedUser(); - record.name = fieldSetFlags()[0] ? this.name : (java.lang.CharSequence) defaultValue(fields()[0]); - record.age = fieldSetFlags()[1] ? this.age : (java.lang.Integer) defaultValue(fields()[1]); - return record; - } catch (Exception e) { - throw new org.apache.avro.AvroRuntimeException(e); - } - } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/User.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/User.java deleted file mode 100644 index d93081f88..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/avro/example/User.java +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -/** - * Autogenerated by Avro - * - * DO NOT EDIT DIRECTLY - */ -package com.hurence.logisland.avro.example; -@SuppressWarnings("all") -@org.apache.avro.specific.AvroGenerated -public class User extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord { - public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"User\",\"namespace\":\"io.confluent.kafka.example\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"}]}"); - public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; } - @Deprecated public java.lang.CharSequence name; - - /** - * Default constructor. Note that this does not initialize fields - * to their default values from the schema. If that is desired then - * one should use newBuilder(). - */ - public User() {} - - /** - * All-args constructor. - */ - public User(java.lang.CharSequence name) { - this.name = name; - } - - public org.apache.avro.Schema getSchema() { return SCHEMA$; } - // Used by DatumWriter. Applications should not call. - public java.lang.Object get(int field$) { - switch (field$) { - case 0: return name; - default: throw new org.apache.avro.AvroRuntimeException("Bad index"); - } - } - // Used by DatumReader. Applications should not call. - @SuppressWarnings(value="unchecked") - public void put(int field$, java.lang.Object value$) { - switch (field$) { - case 0: name = (java.lang.CharSequence)value$; break; - default: throw new org.apache.avro.AvroRuntimeException("Bad index"); - } - } - - /** - * Gets the value of the 'name' field. - */ - public java.lang.CharSequence getName() { - return name; - } - - /** - * Sets the value of the 'name' field. - * @param value the value to set. - */ - public void setName(java.lang.CharSequence value) { - this.name = value; - } - - /** Creates a new User RecordBuilder */ - public static com.hurence.logisland.avro.example.User.Builder newBuilder() { - return new com.hurence.logisland.avro.example.User.Builder(); - } - - /** Creates a new User RecordBuilder by copying an existing Builder */ - public static com.hurence.logisland.avro.example.User.Builder newBuilder(com.hurence.logisland.avro.example.User.Builder other) { - return new com.hurence.logisland.avro.example.User.Builder(other); - } - - /** Creates a new User RecordBuilder by copying an existing User instance */ - public static com.hurence.logisland.avro.example.User.Builder newBuilder(com.hurence.logisland.avro.example.User other) { - return new com.hurence.logisland.avro.example.User.Builder(other); - } - - /** - * RecordBuilder for User instances. - */ - public static class Builder extends org.apache.avro.specific.SpecificRecordBuilderBase - implements org.apache.avro.data.RecordBuilder { - - private java.lang.CharSequence name; - - /** Creates a new Builder */ - private Builder() { - super(com.hurence.logisland.avro.example.User.SCHEMA$); - } - - /** Creates a Builder by copying an existing Builder */ - private Builder(com.hurence.logisland.avro.example.User.Builder other) { - super(other); - if (isValidValue(fields()[0], other.name)) { - this.name = data().deepCopy(fields()[0].schema(), other.name); - fieldSetFlags()[0] = true; - } - } - - /** Creates a Builder by copying an existing User instance */ - private Builder(com.hurence.logisland.avro.example.User other) { - super(com.hurence.logisland.avro.example.User.SCHEMA$); - if (isValidValue(fields()[0], other.name)) { - this.name = data().deepCopy(fields()[0].schema(), other.name); - fieldSetFlags()[0] = true; - } - } - - /** Gets the value of the 'name' field */ - public java.lang.CharSequence getName() { - return name; - } - - /** Sets the value of the 'name' field */ - public com.hurence.logisland.avro.example.User.Builder setName(java.lang.CharSequence value) { - validate(fields()[0], value); - this.name = value; - fieldSetFlags()[0] = true; - return this; - } - - /** Checks whether the 'name' field has been set */ - public boolean hasName() { - return fieldSetFlags()[0]; - } - - /** Clears the value of the 'name' field */ - public com.hurence.logisland.avro.example.User.Builder clearName() { - name = null; - fieldSetFlags()[0] = false; - return this; - } - - @Override - public User build() { - try { - User record = new User(); - record.name = fieldSetFlags()[0] ? this.name : (java.lang.CharSequence) defaultValue(fields()[0]); - return record; - } catch (Exception e) { - throw new org.apache.avro.AvroRuntimeException(e); - } - } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/ClusterTestHarness.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/ClusterTestHarness.java deleted file mode 100644 index ae62ee37b..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/ClusterTestHarness.java +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry; - - -import com.hurence.logisland.avro.AvroCompatibilityLevel; -import kafka.server.KafkaConfig; -import kafka.server.KafkaServer; -import kafka.utils.CoreUtils; -import kafka.utils.SystemTime$; -import kafka.utils.TestUtils; -import kafka.utils.ZkUtils; -import kafka.zk.EmbeddedZookeeper; -import org.I0Itec.zkclient.ZkClient; -import org.apache.kafka.common.protocol.SecurityProtocol; -import org.apache.kafka.common.security.JaasUtils; -import org.junit.After; -import org.junit.Before; -import scala.Option; -import scala.Option$; -import scala.collection.JavaConversions; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.util.List; -import java.util.Properties; -import java.util.Vector; - -/** - * Test harness to run against a real, local Kafka cluster and REST proxy. This is essentially - * Kafka's ZookeeperTestHarness and KafkaServerTestHarness traits combined and ported to Java with - * the addition of the REST proxy. Defaults to a 1-ZK, 3-broker, 1 REST proxy cluster. - */ -public abstract class ClusterTestHarness { - - public static final int DEFAULT_NUM_BROKERS = 1; - public static final String KAFKASTORE_TOPIC = KafkaRegistryConfig.DEFAULT_KAFKASTORE_TOPIC_JOBS; - protected static final Option SASL_PROPERTIES = Option$.MODULE$.empty(); - - /** - * Choose a number of random available ports - */ - public static int[] choosePorts(int count) { - try { - ServerSocket[] sockets = new ServerSocket[count]; - int[] ports = new int[count]; - for (int i = 0; i < count; i++) { - sockets[i] = new ServerSocket(0, 0, InetAddress.getByName("0.0.0.0")); - ports[i] = sockets[i].getLocalPort(); - } - for (int i = 0; i < count; i++) - sockets[i].close(); - return ports; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Choose an available port - */ - public static int choosePort() { - return choosePorts(1)[0]; - } - - private int numBrokers; - private boolean setupRestApp; - private String compatibilityType; - - // ZK Config - protected EmbeddedZookeeper zookeeper; - protected String zkConnect; - protected ZkClient zkClient; - protected ZkUtils zkUtils; - protected int zkConnectionTimeout = 6000; - protected int zkSessionTimeout = 6000; - - // Kafka Config - protected List configs = null; - protected List servers = null; - protected String brokerList = null; - - protected RestApp restApp = null; - - public ClusterTestHarness() { - this(DEFAULT_NUM_BROKERS); - } - - public ClusterTestHarness(int numBrokers) { - this(numBrokers, false); - } - - public ClusterTestHarness(int numBrokers, boolean setupRestApp) { - this(numBrokers, setupRestApp, AvroCompatibilityLevel.NONE.name); - } - - public ClusterTestHarness(int numBrokers, boolean setupRestApp, String compatibilityType) { - this.numBrokers = numBrokers; - this.setupRestApp = setupRestApp; - this.compatibilityType = compatibilityType; - } - - @Before - public void setUp() throws Exception { - zookeeper = new EmbeddedZookeeper(); - zkConnect = String.format("127.0.0.1:%d", zookeeper.port()); - zkUtils = ZkUtils.apply( - zkConnect, zkSessionTimeout, zkConnectionTimeout, - JaasUtils.isZkSecurityEnabled()); - zkClient = zkUtils.zkClient(); - - configs = new Vector<>(); - servers = new Vector<>(); - for (int i = 0; i < numBrokers; i++) { - KafkaConfig config = getKafkaConfig(i); - configs.add(config); - - KafkaServer server = TestUtils.createServer(config, SystemTime$.MODULE$); - servers.add(server); - } - - brokerList = - TestUtils.getBrokerListStrFromServers(JavaConversions.asScalaBuffer(servers), - getSecurityProtocol()); - - if (setupRestApp) { - restApp = new RestApp(choosePort(), zkConnect, KAFKASTORE_TOPIC, compatibilityType); - restApp.start(); - } - } - - protected KafkaConfig getKafkaConfig(int brokerId) { - final Option noFile = Option.apply(null); - final Option noInterBrokerSecurityProtocol = Option.apply(null); - Properties props = TestUtils.createBrokerConfig( - brokerId, zkConnect, false, false, TestUtils.RandomPort(), noInterBrokerSecurityProtocol, - noFile, SASL_PROPERTIES, true, false, TestUtils.RandomPort(), false, TestUtils.RandomPort(), false, - TestUtils.RandomPort(), Option.empty()); - props.setProperty("auto.create.topics.enable", "true"); - props.setProperty("num.partitions", "1"); - - // We *must* override this to use the ZooKeeper port chosen in this test harness, instead of the - // port chosen by TestUtils.createBrokerConfig(). - props.setProperty("zookeeper.connect", this.zkConnect); - return KafkaConfig.fromProps(props); - } - - protected SecurityProtocol getSecurityProtocol() { - return SecurityProtocol.PLAINTEXT; - } - - @After - public void tearDown() throws Exception { - if (restApp != null) { - restApp.stop(); - } - - if (servers != null) { - for (KafkaServer server : servers) { - server.shutdown(); - } - - // Remove any persistent data - for (KafkaServer server : servers) { - CoreUtils.delete(server.config().logDirs()); - } - } - - if (zkUtils != null) { - zkUtils.close(); - } - - if (zookeeper != null) { - zookeeper.shutdown(); - } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/RestApp.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/RestApp.java deleted file mode 100644 index 650eb83c6..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/RestApp.java +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry; - - - -import com.hurence.logisland.agent.rest.RestService; -import com.hurence.logisland.avro.AvroCompatibilityLevel; -import com.hurence.logisland.kafka.registry.exceptions.RegistryException; -import com.hurence.logisland.kafka.zookeeper.RegistryIdentity; -import org.eclipse.jetty.server.Server; - -import java.util.Properties; - -public class RestApp { - - public final Properties prop; - public RestService restClient; - public KafkaRegistryRestApplication restApp; - public Server restServer; - public String restConnect; - - public RestApp(int port, String zkConnect, String kafkaTopic) { - this(port, zkConnect, kafkaTopic, AvroCompatibilityLevel.NONE.name); - } - - public RestApp(int port, String zkConnect, String kafkaTopic, String compatibilityType) { - this(port, zkConnect, kafkaTopic, compatibilityType, true); - } - - public RestApp(int port, String zkConnect, String kafkaTopic, - String compatibilityType, boolean masterEligibility) { - prop = new Properties(); - prop.setProperty(KafkaRegistryConfig.PORT_CONFIG, ((Integer) port).toString()); - prop.setProperty(KafkaRegistryConfig.KAFKASTORE_CONNECTION_URL_CONFIG, zkConnect); - prop.put(KafkaRegistryConfig.KAFKASTORE_TOPIC_JOBS_CONFIG, kafkaTopic); - prop.put(KafkaRegistryConfig.COMPATIBILITY_CONFIG, compatibilityType); - prop.put(KafkaRegistryConfig.MASTER_ELIGIBILITY, masterEligibility); - } - - public void start() throws Exception { - restApp = new KafkaRegistryRestApplication(prop); - restServer = restApp.createServer(); - restServer.start(); - restConnect = restServer.getURI().toString(); - if (restConnect.endsWith("/")) - restConnect = restConnect.substring(0, restConnect.length()-1); - restClient = new RestService(restConnect); - } - - public void stop() throws Exception { - restClient = null; - if (restServer != null) { - restServer.stop(); - restServer.join(); - } - } - - public boolean isMaster() { - return restApp.schemaRegistry().isMaster(); - } - - public void setMaster(RegistryIdentity schemaRegistryIdentity) - throws RegistryException { - restApp.schemaRegistry().setMaster(schemaRegistryIdentity); - } - - public RegistryIdentity myIdentity() { - return restApp.schemaRegistry().myIdentity(); - } - - public RegistryIdentity masterIdentity() { - return restApp.schemaRegistry().masterIdentity(); - } - - public KafkaRegistry kafkaRegistry() { - return restApp.schemaRegistry(); - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/SSLClusterTestHarness.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/SSLClusterTestHarness.java deleted file mode 100644 index 7b63f5911..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/registry/SSLClusterTestHarness.java +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.registry; - -import kafka.server.KafkaConfig; -import kafka.utils.TestUtils; -import org.apache.kafka.common.network.Mode; -import org.apache.kafka.common.protocol.SecurityProtocol; -import org.apache.kafka.test.TestSslUtils; -import scala.Option; - -import java.io.File; -import java.io.IOException; -import java.util.Map; -import java.util.Properties; - -public class SSLClusterTestHarness extends ClusterTestHarness { - public Map clientSslConfigs; - - public SSLClusterTestHarness() { - super(DEFAULT_NUM_BROKERS); - } - - protected SecurityProtocol getSecurityProtocol() { - return SecurityProtocol.SSL; - } - - protected KafkaConfig getKafkaConfig(int brokerId) { - File trustStoreFile; - try { - trustStoreFile = File.createTempFile("SSLClusterTestHarness-truststore", ".jks"); - } catch (IOException ioe) { - throw new RuntimeException("Unable to create temporary file for the truststore."); - } - final Option trustStoreFileOption = Option.apply(trustStoreFile); - final Option sslInterBrokerSecurityProtocol = Option.apply(SecurityProtocol.SSL); - Properties props = TestUtils.createBrokerConfig( - brokerId, zkConnect, false, false, TestUtils.RandomPort(), sslInterBrokerSecurityProtocol, - trustStoreFileOption, SASL_PROPERTIES, false, false, TestUtils.RandomPort(), true, TestUtils.RandomPort(), - false, TestUtils.RandomPort(), Option.empty()); - - // setup client SSL. Needs to happen before the broker is initialized, because the client's cert - // needs to be added to the broker's trust store. - Map sslConfigs; - try { - this.clientSslConfigs = TestSslUtils.createSslConfig(true, true, Mode.CLIENT, - trustStoreFile, "client", "localhost"); - } catch (Exception e) { - throw new RuntimeException(e); - } - - props.setProperty("auto.create.topics.enable", "true"); - props.setProperty("num.partitions", "1"); - if (requireSSLClientAuth()) { - props.setProperty("ssl.client.auth", "required"); - } - // We *must* override this to use the port we allocated (Kafka currently allocates one port - // that it always uses for ZK - props.setProperty("zookeeper.connect", this.zkConnect); - - return KafkaConfig.fromProps(props); - } - - protected boolean requireSSLClientAuth() { - return true; - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaRegistryTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaRegistryTest.java deleted file mode 100644 index 3d532d62b..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaRegistryTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import org.junit.Test; - -import java.util.LinkedList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -public class KafkaRegistryTest { - @Test - public void testGetPortForIdentityPrecedence() { - List listeners = new LinkedList(); - listeners.add("http://localhost:456"); - - int port = KafkaRegistry.getPortForIdentity(123, listeners); - assertEquals("Expected listeners to take precedence over port.", 456, port); - } - - @Test - public void testGetPortForIdentityNoListeners() { - List listeners = new LinkedList(); - int port = KafkaRegistry.getPortForIdentity(123, listeners); - assertEquals("Expected port to take the configured port value", 123, port); - } - - @Test - public void testGetPortForIdentityMultipleListeners() { - List listeners = new LinkedList(); - listeners.add("http://localhost:123"); - listeners.add("https://localhost:456"); - - int port = KafkaRegistry.getPortForIdentity(-1, listeners); - assertEquals("Expected first listener's port to be returned", 123, port); - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThreadTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThreadTest.java deleted file mode 100644 index 226208173..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreReaderThreadTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.store.exceptions.StoreTimeoutException; -import com.hurence.logisland.kafka.registry.ClusterTestHarness; -import com.hurence.logisland.kafka.utils.TestUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.concurrent.TimeUnit; - -import static org.junit.Assert.fail; - -public class KafkaStoreReaderThreadTest extends ClusterTestHarness { - - public KafkaStoreReaderThreadTest() { - super(1, true); - } - - private static final Logger log = LoggerFactory.getLogger(KafkaStoreReaderThreadTest.class); - - @Before - public void setup() { - log.debug("Zk conn url = " + zkConnect); - } - - @After - public void teardown() { - log.debug("Shutting down"); - } - - - @Test - @Ignore - public void testWaitUntilOffset() throws Exception { -// String schema = TestUtils.getRandomCanonicalAvroString(1).get(0); -// int id1 = restApp.restClient.registerSchema(schema, "subject1"); -// -// KafkaRegistry sr = (KafkaRegistry) restApp.kafkaRegistry(); -// KafkaStoreReaderThread readerThread = sr.getJobsKafkaStore().getKafkaStoreReaderThread(); -// try { -// readerThread.waitUntilOffset(50L, 500L, TimeUnit.MILLISECONDS); -// fail("Should have timed out waiting to reach non-existent offset."); -// } catch (StoreTimeoutException e) { -// // This is expected -// } -// -// try { -// readerThread.waitUntilOffset(0L, 5000L, TimeUnit.MILLISECONDS); -// } catch (StoreTimeoutException e) { -// fail("5 seconds should be more than enough time to reach offset 0 in the log."); -// } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLAuthTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLAuthTest.java deleted file mode 100644 index ae448b115..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLAuthTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import com.hurence.logisland.kafka.registry.SSLClusterTestHarness; -import com.hurence.logisland.kafka.store.exceptions.StoreException; -import com.hurence.logisland.kafka.store.exceptions.StoreInitializationException; -import org.apache.kafka.common.errors.TimeoutException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -public class KafkaStoreSSLAuthTest extends SSLClusterTestHarness { - private static final Logger log = LoggerFactory.getLogger(KafkaStoreSSLAuthTest.class); - - @Before - public void setup() { - log.debug("Zk conn url = " + zkConnect); - } - - @After - public void teardown() { - log.debug("Shutting down"); - } - - @Test - public void testInitialization() { - KafkaStore kafkaStore = StoreUtils.createAndInitSSLKafkaStoreInstance(zkConnect, - zkClient, clientSslConfigs, requireSSLClientAuth()); - kafkaStore.close(); - } - - @Test(expected = TimeoutException.class) - public void testInitializationWithoutClientAuth() { - KafkaStore kafkaStore = StoreUtils.createAndInitSSLKafkaStoreInstance(zkConnect, - zkClient, clientSslConfigs, false); - kafkaStore.close(); - - // TODO: make the timeout shorter so the test fails quicker. - } - - @Test - public void testDoubleInitialization() { - KafkaStore kafkaStore = StoreUtils.createAndInitSSLKafkaStoreInstance(zkConnect, - zkClient, clientSslConfigs, requireSSLClientAuth()); - try { - kafkaStore.init(); - fail("Kafka store repeated initialization should fail"); - } catch (StoreInitializationException e) { - // this is expected - } - kafkaStore.close(); - } - - @Test - public void testSimplePut() throws InterruptedException { - KafkaStore kafkaStore = StoreUtils.createAndInitSSLKafkaStoreInstance(zkConnect, - zkClient, clientSslConfigs, requireSSLClientAuth()); - String key = "Kafka"; - String value = "Rocks"; - try { - try { - kafkaStore.put(key, value); - } catch (StoreException e) { - throw new RuntimeException("Kafka store put(Kafka, Rocks) operation failed", e); - } - String retrievedValue = null; - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertEquals("Retrieved value should match entered value", value, retrievedValue); - } finally { - kafkaStore.close(); - } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLNoAuthTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLNoAuthTest.java deleted file mode 100644 index 7d012fb10..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreSSLNoAuthTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - -import org.junit.Test; - -public class KafkaStoreSSLNoAuthTest extends KafkaStoreSSLAuthTest { - protected boolean requireSSLClientAuth() { - return false; - } - - // ignore this test because it doesn't apply when SSL client auth is off. - @Test - @Override - public void testInitializationWithoutClientAuth() {} -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreTest.java deleted file mode 100644 index 968667f71..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/KafkaStoreTest.java +++ /dev/null @@ -1,308 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - - -import com.hurence.logisland.kafka.store.exceptions.StoreException; -import com.hurence.logisland.kafka.store.exceptions.StoreInitializationException; -import com.hurence.logisland.kafka.registry.ClusterTestHarness; -import kafka.cluster.Broker; -import org.apache.kafka.common.config.ConfigException; -import org.apache.kafka.common.protocol.SecurityProtocol; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.*; - -public class KafkaStoreTest extends ClusterTestHarness { - - private static final Logger log = LoggerFactory.getLogger(KafkaStoreTest.class); - - @Before - public void setup() { - log.debug("Zk conn url = " + zkConnect); - } - - @After - public void teardown() { - log.debug("Shutting down"); - } - - @Test - public void testInitialization() { - KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, - zkClient); - kafkaStore.close(); - } - - @Test - public void testDoubleInitialization() { - KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, - zkClient); - try { - kafkaStore.init(); - fail("Kafka store repeated initialization should fail"); - } catch (StoreInitializationException e) { - // this is expected - } - kafkaStore.close(); - } - - @Test - public void testSimplePut() throws InterruptedException { - KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, - zkClient); - String key = "Kafka"; - String value = "Rocks"; - try { - try { - kafkaStore.put(key, value); - } catch (StoreException e) { - throw new RuntimeException("Kafka store put(Kafka, Rocks) operation failed", e); - } - String retrievedValue = null; - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertEquals("Retrieved value should match entered value", value, retrievedValue); - } finally { - kafkaStore.close(); - } - } - - // TODO: This requires fix for https://issues.apache.org/jira/browse/KAFKA-1788 -// @Test -// public void testPutRetries() throws InterruptedException { -// KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, -// zkClient); -// String key = "Kafka"; -// String value = "Rocks"; -// try { -// kafkaStore.put(key, value); -// } catch (StoreException e) { -// fail("Kafka store put(Kafka, Rocks) operation failed"); -// } -// String retrievedValue = null; -// try { -// retrievedValue = kafkaStore.get(key); -// } catch (StoreException e) { -// fail("Kafka store get(Kafka) operation failed"); -// } -// assertEquals("Retrieved value should match entered value", value, retrievedValue); -// // stop the Kafka servers -// for (KafkaServer server : servers) { -// server.shutdown(); -// } -// try { -// kafkaStore.put(key, value); -// fail("Kafka store put(Kafka, Rocks) operation should fail"); -// } catch (StoreException e) { -// // expected since the Kafka producer will run out of retries -// } -// kafkaStore.close(); -// } - - @Test - public void testSimpleGetAfterFailure() throws InterruptedException { - Store inMemoryStore = new InMemoryStore(); - KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, - zkClient, - inMemoryStore); - String key = "Kafka"; - String value = "Rocks"; - String retrievedValue = null; - try { - try { - kafkaStore.put(key, value); - } catch (StoreException e) { - throw new RuntimeException("Kafka store put(Kafka, Rocks) operation failed", e); - } - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertEquals("Retrieved value should match entered value", value, retrievedValue); - } finally { - kafkaStore.close(); - } - - // recreate kafka store - kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, zkClient, inMemoryStore); - try { - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertEquals("Retrieved value should match entered value", value, retrievedValue); - } finally { - kafkaStore.close(); - } - } - - @Test - public void testSimpleDelete() throws InterruptedException { - KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, - zkClient); - String key = "Kafka"; - String value = "Rocks"; - try { - try { - kafkaStore.put(key, value); - } catch (StoreException e) { - throw new RuntimeException("Kafka store put(Kafka, Rocks) operation failed", e); - } - String retrievedValue = null; - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertEquals("Retrieved value should match entered value", value, retrievedValue); - try { - kafkaStore.delete(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store delete(Kafka) operation failed", e); - } - // verify that value is deleted - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertNull("Value should have been deleted", retrievedValue); - } finally { - kafkaStore.close(); - } - } - - @Test - public void testDeleteAfterRestart() throws InterruptedException { - Store inMemoryStore = new InMemoryStore(); - KafkaStore kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, - zkClient, - inMemoryStore); - String key = "Kafka"; - String value = "Rocks"; - try { - try { - kafkaStore.put(key, value); - } catch (StoreException e) { - throw new RuntimeException("Kafka store put(Kafka, Rocks) operation failed", e); - } - String retrievedValue = null; - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertEquals("Retrieved value should match entered value", value, retrievedValue); - // delete the key - try { - kafkaStore.delete(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store delete(Kafka) operation failed", e); - } - // verify that key is deleted - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertNull("Value should have been deleted", retrievedValue); - kafkaStore.close(); - // recreate kafka store - kafkaStore = StoreUtils.createAndInitKafkaStoreInstance(zkConnect, zkClient, inMemoryStore); - // verify that key still doesn't exist in the store - retrievedValue = value; - try { - retrievedValue = kafkaStore.get(key); - } catch (StoreException e) { - throw new RuntimeException("Kafka store get(Kafka) operation failed", e); - } - assertNull("Value should have been deleted", retrievedValue); - } finally { - kafkaStore.close(); - } - } - - @Test - public void testFilterBrokerEndpointsSinglePlaintext() { - String endpoint = "PLAINTEXT://hostname:1234"; - List endpointsList = new ArrayList(); - endpointsList.add("PLAINTEXT://hostname:1234"); - assertEquals("Expected one PLAINTEXT endpoint for localhost", endpoint, - KafkaStore.filterBrokerEndpoints(endpointsList)); - } - - @Test(expected = ConfigException.class) - public void testGetBrokerEndpointsEmpty() { - KafkaStore.filterBrokerEndpoints(new ArrayList()); - } - - @Test - public void testGetBrokerEndpointsMixed() throws IOException { - List endpointsList = new ArrayList(4); - endpointsList.add("PLAINTEXT://localhost:1234"); - endpointsList.add("PLAINTEXT://localhost1:1234"); - endpointsList.add("SASL_PLAINTEXT://localhost1:1235"); - endpointsList.add("SSL://localhost1:1236"); - - String endpointsString = KafkaStore.filterBrokerEndpoints(endpointsList); - String[] endpoints = endpointsString.split(","); - assertEquals("Expected a different number of endpoints.", endpointsList.size() - 1, endpoints.length); - for (String endpoint : endpoints) { - if (endpoint.contains("localhost1:1236")) { - assertTrue("Endpoint must be a SSL endpoint.", endpoint.contains("SSL://")); - } else { - assertTrue("Endpoint must be a PLAINTEXT endpoint.", endpoint.contains("PLAINTEXT://")); - } - } - } - - @Test - public void testBrokersToEndpoints() { - List brokersList = new ArrayList(4); - brokersList.add(new Broker(0, "localhost", 1, SecurityProtocol.PLAINTEXT)); - brokersList.add(new Broker(1, "localhost1", 12, SecurityProtocol.PLAINTEXT)); - brokersList.add(new Broker(2, "localhost2", 123, SecurityProtocol.SASL_PLAINTEXT)); - brokersList.add(new Broker(3, "localhost3", 1234, SecurityProtocol.SSL)); - List endpointsList = KafkaStore.brokersToEndpoints((brokersList)); - - List expected = new ArrayList(4); - expected.add("PLAINTEXT://localhost:1"); - expected.add("PLAINTEXT://localhost1:12"); - expected.add("SASL_PLAINTEXT://localhost2:123"); - expected.add("SSL://localhost3:1234"); - - assertEquals("Expected the same size list.", expected.size(), endpointsList.size()); - - for (int i = 0; i < endpointsList.size(); i++) { - assertEquals("Expected a different endpoint", expected.get(i), endpointsList.get(i)); - } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/RegistryKeysTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/RegistryKeysTest.java deleted file mode 100644 index 31586f3c4..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/RegistryKeysTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - -import com.hurence.logisland.kafka.registry.KafkaRegistry; -import com.hurence.logisland.kafka.serialization.RegistrySerializer; -import com.hurence.logisland.kafka.serialization.Serializer; -import com.hurence.logisland.kafka.store.exceptions.SerializationException; -import com.hurence.logisland.kafka.store.exceptions.StoreException; -import org.junit.Test; - -import java.util.Iterator; - -import static org.junit.Assert.*; - -public class RegistryKeysTest { - - @Test - public void testTopicKeySerde() { - String subject = "foo"; - int version = 1; - TopicKey key = new TopicKey(subject, version); - Serializer serializer = new RegistrySerializer(); - byte[] serializedKey = null; - try { - serializedKey = serializer.serializeKey(key); - } catch (SerializationException e) { - fail(); - } - assertNotNull(serializedKey); - try { - RegistryKey deserializedKey = serializer.deserializeKey(serializedKey); - assertEquals("Deserialized key should be equal to original key", key, deserializedKey); - } catch (SerializationException e) { - e.printStackTrace(); - fail(); - } - } - - @Test - public void testTopicKeyComparator() { - String subject = "foo"; - RegistryKey key1 = new TopicKey(subject, 0); - RegistryKey key2 = new TopicKey(subject, Integer.MAX_VALUE); - assertTrue("key 1 should be less than key2", key1.compareTo(key2) < 0); - RegistryKey key1Dup = new TopicKey(subject, 0); - assertEquals("key 1 should be equal to key1Dup", key1, key1Dup); - String subject4 = "bar"; - RegistryKey key4 = new TopicKey(subject4, Integer.MIN_VALUE); - assertTrue("key1 should be greater than key4", key1.compareTo(key4) > 0); - String subject5 = "fo"; - RegistryKey key5 = new TopicKey(subject5, Integer.MIN_VALUE); - // compare key1 and key5 - assertTrue("key5 should be less than key1", key1.compareTo(key5) > 0); - RegistryKey[] expectedOrder = {key4, key5, key1, key2}; - testStoreKeyOrder(expectedOrder); - } - - @Test - public void testJobKeySerde() { - String subject = "foo"; - JobKey key1 = new JobKey(null,0); - JobKey key2 = new JobKey(subject,0); - Serializer serializer = new RegistrySerializer(); - byte[] serializedKey1 = null; - byte[] serializedKey2 = null; - try { - serializedKey1 = serializer.serializeKey(key1); - serializedKey2 = serializer.serializeKey(key2); - } catch (SerializationException e) { - fail(); - } - try { - RegistryKey deserializedKey1 = serializer.deserializeKey(serializedKey1); - RegistryKey deserializedKey2 = serializer.deserializeKey(serializedKey2); - assertEquals("Deserialized key should be equal to original key", key1, deserializedKey1); - assertEquals("Deserialized key should be equal to original key", key2, deserializedKey2); - } catch (SerializationException e) { - fail(); - } - } - - @Test - public void testJobKeyComparator() { - JobKey key1 = new JobKey(null,0); - JobKey key2 = new JobKey(null,0); - assertEquals("Top level config keys should be equal", key1, key2); - String subject = "foo"; - JobKey key3 = new JobKey(subject,0); - assertTrue("Top level config should be less than subject level config", - key1.compareTo(key3) < 0); - String subject4 = "bar"; - JobKey key4 = new JobKey(subject4,0); - assertTrue("key3 should be greater than key4", key3.compareTo(key4) > 0); - RegistryKey[] expectedOrder = {key1, key4, key3}; - testStoreKeyOrder(expectedOrder); - } - - @Test - public void testKeyComparator() { - String subject = "foo"; - JobKey topLevelJobKey = new JobKey(null,0); - JobKey subjectLevelJobKey = new JobKey(subject,0); - TopicKey schemaKey = new TopicKey(subject, 1); - TopicKey schemaKeyWithHigherVersion = new TopicKey(subject, 2); - RegistryKey[] - expectedOrder = - {topLevelJobKey, subjectLevelJobKey, schemaKey, schemaKeyWithHigherVersion}; - testStoreKeyOrder(expectedOrder); - } - - private void testStoreKeyOrder(RegistryKey[] orderedKeys) { - int numKeys = orderedKeys.length; - InMemoryStore store = new InMemoryStore(); - while (--numKeys >= 0) { - try { - store.put(orderedKeys[numKeys], orderedKeys[numKeys].toString()); - } catch (StoreException e) { - fail("Error writing key " + orderedKeys[numKeys].toString() + " to the in memory store"); - } - } - // test key order - try { - Iterator keys = store.getAllKeys(); - RegistryKey[] retrievedKeyOrder = new RegistryKey[orderedKeys.length]; - int keyIndex = 0; - while (keys.hasNext()) { - retrievedKeyOrder[keyIndex++] = keys.next(); - } - assertArrayEquals(orderedKeys, retrievedKeyOrder); - } catch (StoreException e) { - fail(); - } - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StoreUtils.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StoreUtils.java deleted file mode 100644 index 414133cf0..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StoreUtils.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - - -import com.hurence.logisland.kafka.registry.ClusterTestHarness; -import com.hurence.logisland.kafka.registry.KafkaRegistryConfig; -import com.hurence.logisland.kafka.store.exceptions.StoreInitializationException; -import io.confluent.rest.RestConfigException; -import org.I0Itec.zkclient.ZkClient; -import org.apache.kafka.clients.CommonClientConfigs; -import org.apache.kafka.common.config.SslConfigs; -import org.apache.kafka.common.config.types.Password; - -import java.util.Map; -import java.util.Properties; - -import static org.junit.Assert.fail; - -/** - * For all store related utility methods. - */ -public class StoreUtils { - - /** - * Get a new instance of KafkaStore and initialize it. - */ - public static KafkaStore createAndInitKafkaStoreInstance( - String zkConnect, ZkClient zkClient) { - Store inMemoryStore = new InMemoryStore(); - return createAndInitKafkaStoreInstance(zkConnect, zkClient, inMemoryStore); - } - /** - * Get a new instance of KafkaStore and initialize it. - */ - public static KafkaStore createAndInitKafkaStoreInstance( - String zkConnect, ZkClient zkClient, Store inMemoryStore) { - return createAndInitKafkaStoreInstance(zkConnect, zkClient, inMemoryStore, - new Properties()); - } - - /** - * Get a new instance of an SSL KafkaStore and initialize it. - */ - public static KafkaStore createAndInitSSLKafkaStoreInstance( - String zkConnect, ZkClient zkClient, Map sslConfigs, - boolean requireSSLClientAuth) { - Properties props = new Properties(); - props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, - KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_SSL); - - props.put(KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_CONFIG, - KafkaRegistryConfig.KAFKASTORE_SECURITY_PROTOCOL_SSL); - props.put(KafkaRegistryConfig.KAFKASTORE_SSL_TRUSTSTORE_LOCATION_CONFIG, - sslConfigs.get(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG)); - props.put(KafkaRegistryConfig.KAFKASTORE_SSL_TRUSTSTORE_PASSWORD_CONFIG, - ((Password)sslConfigs.get(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG)).value()); - if (requireSSLClientAuth) { - props.put(KafkaRegistryConfig.KAFKASTORE_SSL_KEYSTORE_LOCATION_CONFIG, - sslConfigs.get(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG)); - props.put(KafkaRegistryConfig.KAFKASTORE_SSL_KEYSTORE_PASSWORD_CONFIG, - ((Password) sslConfigs.get(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG)).value()); - props.put(KafkaRegistryConfig.KAFKASTORE_SSL_KEY_PASSWORD_CONFIG, - ((Password) sslConfigs.get(SslConfigs.SSL_KEY_PASSWORD_CONFIG)).value()); - } - - Store inMemoryStore = new InMemoryStore(); - return createAndInitKafkaStoreInstance(zkConnect, zkClient, inMemoryStore, props); - } - - /** - * Get a new instance of KafkaStore and initialize it. - */ - public static KafkaStore createAndInitKafkaStoreInstance( - String zkConnect, ZkClient zkClient, Store inMemoryStore, - Properties props) { - props.put(KafkaRegistryConfig.KAFKASTORE_CONNECTION_URL_CONFIG, zkConnect); - props.put(KafkaRegistryConfig.KAFKASTORE_TOPIC_JOBS_CONFIG, ClusterTestHarness.KAFKASTORE_TOPIC); - - KafkaRegistryConfig config = null; - try { - config = new KafkaRegistryConfig(props); - } catch (RestConfigException e) { - fail("Can't initialize configs"); - } - - KafkaStore kafkaStore = - new KafkaStore( - KafkaRegistryConfig.KAFKASTORE_TOPIC_JOBS_CONFIG, - config, - new StringMessageHandler(), - StringSerializer.INSTANCE, - inMemoryStore, - new NoopKey().toString()); - try { - kafkaStore.init(); - } catch (StoreInitializationException e) { - fail("Kafka store failed to initialize"); - } - return kafkaStore; - } - -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringMessageHandler.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringMessageHandler.java deleted file mode 100644 index 2f025453c..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringMessageHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - -public class StringMessageHandler implements StoreUpdateHandler { - - /** - * Invoked on every new K,V pair written to the store - * - * @param key Key associated with the data - * @param value Data written to the store - */ - @Override - public void handleUpdate(String key, String value) { - - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringSerializer.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringSerializer.java deleted file mode 100644 index 9334b1680..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/store/StringSerializer.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.store; - - - -import com.hurence.logisland.kafka.serialization.Serializer; -import com.hurence.logisland.kafka.store.exceptions.SerializationException; - -import java.util.Map; - -public class StringSerializer implements Serializer { - - public static StringSerializer INSTANCE = new StringSerializer(); - - // only a singleton is needed - private StringSerializer() { - } - - /** - * @param key Typed key - * @return bytes of the serialized key - */ - @Override - public byte[] serializeKey(String key) throws SerializationException { - return key != null ? key.getBytes() : null; - } - - /** - * @param value Typed value - * @return bytes of the serialized value - */ - @Override - public byte[] serializeValue(String value) throws SerializationException { - return value != null ? value.getBytes() : null; - } - - @Override - public String deserializeKey(byte[] key) { - return new String(key); - } - - /** - * @param key Typed key corresponding to this value - * @param value Bytes of the serialized value - * @return Typed deserialized value - */ - @Override - public String deserializeValue(String key, byte[] value) throws SerializationException { - return new String(value); - } - - @Override - public void close() { - // do nothing - } - - @Override - public void configure(Map stringMap) { - // do nothing - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/utils/TestUtils.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/utils/TestUtils.java deleted file mode 100644 index c685741e2..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/utils/TestUtils.java +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.kafka.utils; - - - -import com.hurence.logisland.avro.AvroUtils; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.concurrent.Callable; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -/** - * For general utility methods used in unit tests. - */ -public class TestUtils { - - private static final String IoTmpDir = System.getProperty("java.io.tmpdir"); - private static final Random random = new Random(); - - /** - * Create a temporary directory - */ - public static File tempDir(String namePrefix) { - final File f = new File(IoTmpDir, namePrefix + "-" + random.nextInt(1000000)); - f.mkdirs(); - f.deleteOnExit(); - - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - rm(f); - } - }); - return f; - } - - /** - * Recursively delete the given file/directory and any subfiles (if any exist) - * - * @param file The root file at which to begin deleting - */ - public static void rm(File file) { - if (file == null) { - return; - } else if (file.isDirectory()) { - File[] files = file.listFiles(); - if (files != null) { - for (File f : files) { - rm(f); - } - } - } else { - file.delete(); - } - } - - /** - * Wait until a callable returns true or the timeout is reached. - */ - public static void waitUntilTrue(Callable callable, long timeoutMs, String errorMsg) { - try { - long startTime = System.currentTimeMillis(); - Boolean state = false; - do { - state = callable.call(); - if (System.currentTimeMillis() > startTime + timeoutMs) { - fail(errorMsg); - } - Thread.sleep(50); - } while (!state); - } catch (Exception e) { - fail("Unexpected exception: " + e); - } - } - - - - /** - * Register a new schema and verify that it can be found on the expected version. - - public static void registerAndVerifySchema(RestService restService, String schemaString, - int expectedId, String subject) - throws IOException, RestClientException { - assertEquals("Registering a new schema should succeed", - expectedId, - restService.registerSchema(schemaString, subject)); - - // the newly registered schema should be immediately readable on the master - assertEquals("Registered schema should be found", - schemaString, - restService.getId(expectedId).getSchemaString()); - - }*/ - - public static List getRandomCanonicalAvroString(int num) { - List avroStrings = new ArrayList(); - - for (int i = 0; i < num; i++) { - String schemaString = "{\"type\":\"record\"," - + "\"name\":\"myrecord\"," - + "\"fields\":" - + "[{\"type\":\"string\",\"name\":" - + "\"f" + random.nextInt(Integer.MAX_VALUE) + "\"}]}"; - avroStrings.add(AvroUtils.parseSchema(schemaString).canonicalString); - } - return avroStrings; - } -} diff --git a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/zookeeper/MasterElectorTest.java b/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/zookeeper/MasterElectorTest.java deleted file mode 100644 index 8ea61f3d0..000000000 --- a/logisland-framework/logisland-agent/src/test/java/com/hurence/logisland/kafka/zookeeper/MasterElectorTest.java +++ /dev/null @@ -1,698 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// -//package com.hurence.logisland.kafka.zookeeper; -// -//import com.hurence.logisland.agent.rest.RestService; -//import com.hurence.logisland.agent.rest.client.exceptions.RestClientException; -//import com.hurence.logisland.kafka.registry.KafkaRegistry; -//import com.hurence.logisland.zookeeper.serializers.ZkStringSerializer; -//import com.hurence.logisland.kafka.registry.ClusterTestHarness; -//import com.hurence.logisland.kafka.registry.RestApp; -//import com.hurence.logisland.kafka.utils.TestUtils; -// -//import io.confluent.common.utils.zookeeper.ZkUtils; -//import org.I0Itec.zkclient.ZkClient; -//import org.junit.Test; -// -//import java.io.IOException; -//import java.util.*; -//import java.util.concurrent.Callable; -// -//import static com.hurence.logisland.avro.AvroCompatibilityLevel.FORWARD; -//import static com.hurence.logisland.avro.AvroCompatibilityLevel.NONE; -//import static org.junit.Assert.*; -// -//public class MasterElectorTest extends ClusterTestHarness { -// private static final int ID_BATCH_SIZE = -// KafkaRegistry.ZOOKEEPER_SCHEMA_ID_COUNTER_BATCH_SIZE; -// private static final String ZK_ID_COUNTER_PATH = -// "/schema_registry" + KafkaRegistry.ZOOKEEPER_SCHEMA_ID_COUNTER; -// -// @Test -// public void testAutoFailover() throws Exception { -// final String subject = "testTopic"; -// final String configSubject = "configTopic"; -// List avroSchemas = TestUtils.getRandomCanonicalAvroString(4); -// -// // create schema registry instance 1 -// final RestApp restApp1 = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC); -// restApp1.start(); -// -// // create schema registry instance 2 -// final RestApp restApp2 = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC); -// restApp2.start(); -// assertTrue("Schema registry instance 1 should be the master", restApp1.isMaster()); -// assertFalse("Schema registry instance 2 shouldn't be the master", restApp2.isMaster()); -// assertEquals("Instance 2's master should be instance 1", -// restApp1.myIdentity(), restApp2.masterIdentity()); -// -// // test registering a schema to the master and finding it on the expected version -// final String firstSchema = avroSchemas.get(0); -// final int firstSchemaExpectedId = 1; -// TestUtils.registerAndVerifySchema(restApp1.restClient, firstSchema, firstSchemaExpectedId, -// subject); -// // the newly registered schema should be eventually readable on the non-master -// verifyIdAndSchema(restApp2.restClient, firstSchemaExpectedId, firstSchema, -// "Registered schema should be found on the non-master"); -// -// // test registering a schema to the non-master and finding it on the expected version -// final String secondSchema = avroSchemas.get(1); -// final int secondSchemaExpectedId = 2; -// final int secondSchemaExpectedVersion = 2; -// assertEquals("Registering a new schema to the non-master should succeed", -// secondSchemaExpectedId, -// restApp2.restClient.registerSchema(secondSchema, subject)); -// -// // the newly registered schema should be immediately readable on the master using the id -// assertEquals("Registered schema should be found on the master", -// secondSchema, -// restApp1.restClient.getId(secondSchemaExpectedId).getSchemaString()); -// -// // the newly registered schema should be immediately readable on the master using the version -// assertEquals("Registered schema should be found on the master", -// secondSchema, -// restApp1.restClient.getVersion(subject, -// secondSchemaExpectedVersion).getSchema()); -// -// // the newly registered schema should be eventually readable on the non-master -// verifyIdAndSchema(restApp2.restClient, secondSchemaExpectedId, secondSchema, -// "Registered schema should be found on the non-master"); -// -// // test registering an existing schema to the master -// assertEquals("Registering an existing schema to the master should return its id", -// secondSchemaExpectedId, -// restApp1.restClient.registerSchema(secondSchema, subject)); -// -// // test registering an existing schema to the non-master -// assertEquals("Registering an existing schema to the non-master should return its id", -// secondSchemaExpectedId, -// restApp2.restClient.registerSchema(secondSchema, subject)); -// -// // update config to master -// restApp1.restClient -// .updateCompatibility(FORWARD.name, configSubject); -// assertEquals("New compatibility level should be FORWARD on the master", -// FORWARD.name, -// restApp1.restClient.getConfig(configSubject).getCompatibilityLevel()); -// -// // the new config should be eventually readable on the non-master -// waitUntilCompatibilityLevelSet(restApp2.restClient, configSubject, -// FORWARD.name, -// "New compatibility level should be FORWARD on the non-master"); -// -// // update config to non-master -// restApp2.restClient -// .updateCompatibility(NONE.name, configSubject); -// assertEquals("New compatibility level should be NONE on the master", -// NONE.name, -// restApp1.restClient.getConfig(configSubject).getCompatibilityLevel()); -// -// // the new config should be eventually readable on the non-master -// waitUntilCompatibilityLevelSet(restApp2.restClient, configSubject, -// NONE.name, -// "New compatibility level should be NONE on the non-master"); -// -// // fake an incorrect master and registration should fail -// restApp1.setMaster(null); -// int statusCodeFromRestApp1 = 0; -// final String failedSchema = "{\"type\":\"string\"}";; -// try { -// restApp1.restClient.registerSchema(failedSchema, subject); -// fail("Registration should fail on the master"); -// } catch (RestClientException e) { -// // this is expected. -// statusCodeFromRestApp1 = e.getStatus(); -// } -// -// int statusCodeFromRestApp2 = 0; -// try { -// restApp2.restClient.registerSchema(failedSchema, subject); -// fail("Registration should fail on the non-master"); -// } catch (RestClientException e) { -// // this is expected. -// statusCodeFromRestApp2 = e.getStatus(); -// } -// -// assertEquals("Status code from a non-master rest app for register schema should be 500", -// 500, statusCodeFromRestApp1); -// assertEquals("Error code from the master and the non-master should be the same", -// statusCodeFromRestApp1, statusCodeFromRestApp2); -// -// // update config should fail if master is not available -// int updateConfigStatusCodeFromRestApp1 = 0; -// try { -// restApp1.restClient.updateCompatibility(FORWARD.name, -// configSubject); -// fail("Update config should fail on the master"); -// } catch (RestClientException e) { -// // this is expected. -// updateConfigStatusCodeFromRestApp1 = e.getStatus(); -// } -// -// int updateConfigStatusCodeFromRestApp2 = 0; -// try { -// restApp2.restClient.updateCompatibility(FORWARD.name, -// configSubject); -// fail("Update config should fail on the non-master"); -// } catch (RestClientException e) { -// // this is expected. -// updateConfigStatusCodeFromRestApp2 = e.getStatus(); -// } -// -// assertEquals("Status code from a non-master rest app for update config should be 500", -// 500, updateConfigStatusCodeFromRestApp1); -// assertEquals("Error code from the master and the non-master should be the same", -// updateConfigStatusCodeFromRestApp1, updateConfigStatusCodeFromRestApp2); -// -// // test registering an existing schema to the non-master when the master is not available -// assertEquals("Registering an existing schema to the non-master should return its id", -// secondSchemaExpectedId, -// restApp2.restClient.registerSchema(secondSchema, subject)); -// -// // set the correct master identity back -// restApp1.setMaster(restApp1.myIdentity()); -// -// // registering a schema to the master -// final String thirdSchema = avroSchemas.get(2); -// final int thirdSchemaExpectedVersion = 3; -// final int thirdSchemaExpectedId = ID_BATCH_SIZE + 1; -// assertEquals("Registering a new schema to the master should succeed", -// thirdSchemaExpectedId, -// restApp1.restClient.registerSchema(thirdSchema, subject)); -// -// // stop schema registry instance 1; instance 2 should become the new master -// restApp1.stop(); -// Callable condition = new Callable() { -// @Override -// public Boolean call() throws Exception { -// return restApp2.isMaster(); -// } -// }; -// TestUtils.waitUntilTrue(condition, 5000, -// "Schema registry instance 2 should become the master"); -// -// // the latest version should be immediately available on the new master using the id -// assertEquals("Latest version should be found on the new master", -// thirdSchema, -// restApp2.restClient.getId(thirdSchemaExpectedId).getSchemaString()); -// -// // the latest version should be immediately available on the new master using the version -// assertEquals("Latest version should be found on the new master", -// thirdSchema, -// restApp2.restClient.getVersion(subject, -// thirdSchemaExpectedVersion).getSchema()); -// -// // register a schema to the new master -// final String fourthSchema = avroSchemas.get(3); -// final int fourthSchemaExpectedId = 2 * ID_BATCH_SIZE + 1; -// TestUtils.registerAndVerifySchema(restApp2.restClient, fourthSchema, -// fourthSchemaExpectedId, -// subject); -// -// restApp2.stop(); -// } -// -// -// @Test -// /** -// * Trigger reelection with both a master cluster and slave cluster present. -// * Ensure that nodes in slave cluster are never elected master. -// */ -// public void testSlaveIsNeverMaster() throws Exception { -// int numSlaves = 2; -// int numMasters = 30; -// -// Set slaveApps = new HashSet(); -// RestApp aSlave = null; -// for (int i = 0; i < numSlaves; i++) { -// RestApp slave = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC, -// NONE.name, false); -// slaveApps.add(slave); -// slave.start(); -// aSlave = slave; -// } -// // Sanity check -// assertNotNull(aSlave); -// -// // Check that nothing in the slave cluster points to a master -// for (RestApp slave: slaveApps) { -// assertFalse("No slave should be master.", slave.isMaster()); -// assertNull("No master should be present in a slave cluster.", slave.masterIdentity()); -// } -// -// // It should not be possible to set a slave node as master -// try { -// aSlave.setMaster(aSlave.myIdentity()); -// } catch (IllegalStateException e) { -// // This is expected -// } -// assertFalse("Should not be able to set a slave to be master.", aSlave.isMaster()); -// assertNull("There should be no master present.", aSlave.masterIdentity()); -// -// // Make a master-eligible 'cluster' -// final Set masterApps = new HashSet(); -// for (int i = 0; i < numMasters; i++) { -// RestApp master = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC, -// NONE.name, true); -// masterApps.add(master); -// master.start(); -// waitUntilMasterElectionCompletes(masterApps); -// } -// -// // Kill the current master and wait for reelection until no masters are left -// while (masterApps.size() > 0) { -// RestApp reportedMaster = checkOneMaster(masterApps); -// masterApps.remove(reportedMaster); -// -// checkMasterIdentity(slaveApps, reportedMaster.myIdentity()); -// checkMasterIdentity(masterApps, reportedMaster.myIdentity()); -// checkNoneIsMaster(slaveApps); -// -// reportedMaster.stop(); -// waitUntilMasterElectionCompletes(masterApps); -// } -// -// // All masters are now dead -// checkNoneIsMaster(slaveApps); -// checkNoneIsMaster(masterApps); -// -// for (RestApp slave: slaveApps) { -// slave.stop(); -// } -// } -// -// @Test -// /** -// * Test registration of schemas and fetching by id when a 'master cluster' and 'slave cluster' is -// * present. (Slave cluster == all nodes have masterEligibility false) -// * -// * If only slaves are alive, registration should fail. If both slave and master cluster are -// * alive, registration should succeed. -// * -// * Fetching by id should succeed in all configurations. -// */ -// public void testRegistrationOnMasterSlaveClusters() throws Exception { -// int numSlaves = 4; -// int numMasters = 4; -// int numSchemas = 5; -// String subject = "testSubject"; -// List schemas = TestUtils.getRandomCanonicalAvroString(numSchemas); -// List ids = new ArrayList(); -// -// Set slaveApps = new HashSet(); -// RestApp aSlave = null; -// for (int i = 0; i < numSlaves; i++) { -// RestApp slave = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC, -// NONE.name, false); -// slaveApps.add(slave); -// slave.start(); -// aSlave = slave; -// } -// // Sanity check -// assertNotNull(aSlave); -// -// // Try to register schemas to a slave - should fail -// boolean successfullyRegistered = false; -// try { -// aSlave.restClient.registerSchema(schemas.get(0), subject); -// successfullyRegistered = true; -// } catch (RestClientException e) { -// // registration should fail -// } -// assertFalse("Should not be possible to register with no masters present.", -// successfullyRegistered); -// -// // Make a master-eligible 'cluster' -// final Set masterApps = new HashSet(); -// RestApp aMaster = null; -// for (int i = 0; i < numMasters; i++) { -// RestApp master = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC, -// NONE.name, true); -// masterApps.add(master); -// master.start(); -// aMaster = master; -// } -// assertNotNull(aMaster); -// -// // Try to register to a master cluster node - should succeed -// try { -// for (String schema : schemas) { -// ids.add(aMaster.restClient.registerSchema(schema, subject)); -// } -// } catch (RestClientException e) { -// fail("It should be possible to register schemas when a master cluster is present."); -// } -// -// // Try to register to a slave cluster node - should succeed -// String anotherSchema = TestUtils.getRandomCanonicalAvroString(1).get(0); -// try { -// ids.add(aSlave.restClient.registerSchema(anotherSchema, subject)); -// } catch (RestClientException e) { -// fail("Should be possible register a schema through slave cluster."); -// } -// -// // Verify all ids can be fetched -// try { -// for (int id: ids) { -// waitUntilIdExists(aSlave.restClient, id, -// String.format("Should be possible to fetch id %d from this slave.", id)); -// waitUntilIdExists(aMaster.restClient, id, -// String.format("Should be possible to fetch id %d from this master.", id)); -// -// SchemaString slaveResponse = aSlave.restClient.getId(id); -// SchemaString masterResponse = aMaster.restClient.getId(id); -// assertEquals( -// "Master and slave responded with different schemas when queried with the same id.", -// slaveResponse.getSchemaString(), masterResponse.getSchemaString()); -// } -// } catch (RestClientException e) { -// fail("Expected ids were not found in the schema registry."); -// } -// -// // Stop everything in the master cluster -// while (masterApps.size() > 0) { -// RestApp master = findMaster(masterApps); -// masterApps.remove(master); -// master.stop(); -// waitUntilMasterElectionCompletes(masterApps); -// } -// -// // Try to register a new schema - should fail -// anotherSchema = TestUtils.getRandomCanonicalAvroString(1).get(0); -// successfullyRegistered = false; -// try { -// aSlave.restClient.registerSchema(anotherSchema, subject); -// successfullyRegistered = true; -// } catch (RestClientException e) { -// // should fail -// } -// assertFalse("Should not be possible to register with no masters present.", -// successfullyRegistered); -// -// // Try fetching preregistered ids from slaves - should succeed -// try { -// -// for (int id: ids) { -// SchemaString schemaString = aSlave.restClient.getId(id); -// } -// List versions = aSlave.restClient.getAllVersions(subject); -// assertEquals("Number of ids should match number of versions.", ids.size(), versions.size()); -// } catch (RestClientException e) { -// fail("Should be possible to fetch registered schemas even with no masters present."); -// } -// -// for (RestApp slave: slaveApps) { -// slave.stop(); -// } -// } -// -// @Test -// /** -// * If the zk id counter used to help hand out unique ids is lower than the lowest id in the -// * kafka store, KafkaSchemaRegistry should still do the right thing and continue to hand out -// * increasing ids when new schemas are registered. -// */ -// public void testIncreasingIdZkResetLow() throws Exception { -// // create schema registry instance 1 -// final RestApp restApp1 = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC); -// restApp1.start(); -// List schemas = TestUtils.getRandomCanonicalAvroString(ID_BATCH_SIZE); -// String subject = "testSubject"; -// -// Set ids = new HashSet(); -// int maxId = -1; -// for (int i = 0; i < ID_BATCH_SIZE / 2; i++) { -// int newId = restApp1.restClient.registerSchema(schemas.get(i), subject); -// ids.add(newId); -// -// // Sanity check - ids should be increasing -// assertTrue(newId > maxId); -// maxId = newId; -// } -// -// // Overwrite zk id counter to 0 -// final ZkClient zkClient = new ZkClient(zkConnect, 10000, 10000, new ZkStringSerializer()); -// int zkIdCounter = getZkIdCounter(zkClient); -// assertEquals(ID_BATCH_SIZE, zkIdCounter); // sanity check -// ZkUtils.updatePersistentPath(zkClient, ZK_ID_COUNTER_PATH, "0"); -// -// // Make sure ids are still increasing -// String anotherSchema = TestUtils.getRandomCanonicalAvroString(1).get(0); -// int newId = restApp1.restClient.registerSchema(anotherSchema, subject); -// assertTrue("Next assigned id should be greater than all previous.", newId > maxId); -// maxId = newId; -// -// // Add another schema registry and trigger reelection -// final RestApp restApp2 = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC); -// restApp2.start(); -// restApp1.stop(); -// Callable electionComplete = new Callable() { -// @Override -// public Boolean call() throws Exception { -// return restApp2.isMaster(); -// } -// }; -// TestUtils.waitUntilTrue(electionComplete, 5000, -// "Schema registry instance 2 should become the master"); -// // Reelection should have triggered zk id to update to the next batch -// assertEquals("Zk counter is not the expected value.", -// 2 * ID_BATCH_SIZE, getZkIdCounter(zkClient)); -// -// // Overwrite zk id counter again, then register another batch to trigger id batch update -// // (meanwhile verifying that ids continue to increase) -// ZkUtils.updatePersistentPath(zkClient, ZK_ID_COUNTER_PATH, "0"); -// schemas = TestUtils.getRandomCanonicalAvroString(ID_BATCH_SIZE); -// for (int i = 0; i < ID_BATCH_SIZE; i++) { -// newId = restApp2.restClient.registerSchema(schemas.get(i), subject); -// ids.add(newId); -// -// // Sanity check - ids should be increasing -// assertTrue("new id " + newId + " should be greater than previous max " + maxId, -// newId > maxId); -// maxId = newId; -// } -// -// // We just wrote another batch worth of schemas, so zk counter should jump -// assertEquals("Zk counter is not the expected value.", -// 3 * ID_BATCH_SIZE, getZkIdCounter(zkClient)); -// } -// -// @Test -// /** -// * If there is no schema data in the kafka, but there is id data in zookeeper when a SchemaRegistry -// * instance is booted up, newly assigned ids should be greater than whatever is in zookeeper. -// * -// * Strange preexisting values in zk id counter path should be dealt with gracefully. -// * I.e. regardless of initial value, after zk id counter is updated -// * it should be a multiple of if ID_BATCH_SIZE. -// */ -// public void testIdBehaviorWithZkWithoutKafka() throws Exception { -// // Overwrite the value in zk -// final ZkClient zkClient = new ZkClient(zkConnect, 10000, 10000, new ZkStringSerializer()); -// int weirdInitialCounterValue = ID_BATCH_SIZE - 1; -// ZkUtils.createPersistentPath(zkClient, ZK_ID_COUNTER_PATH, "" + weirdInitialCounterValue); -// -// // Check that zookeeper id counter is updated sensibly during SchemaRegistry bootstrap process -// final RestApp restApp = new RestApp(choosePort(), -// zkConnect, KAFKASTORE_TOPIC); -// restApp.start(); -// assertEquals("", 2 * ID_BATCH_SIZE, getZkIdCounter(zkClient)); -// } -// -// @Test -// /** -// * Verify correct id allocation when a SchemaRegistry instance is initialized, and there is -// * preexisting data in the kafkastore, but no zookeeper id counter node. -// */ -// public void testIdBehaviorWithoutZkWithKafka() throws Exception { -// -// // Pre-populate kafkastore with a few schemas -// int numSchemas = 2; -// List schemas = TestUtils.getRandomCanonicalAvroString(numSchemas); -// String subject = "testSubject"; -// Set ids = new HashSet(); -// RestApp restApp = new RestApp(choosePort(), zkConnect, KAFKASTORE_TOPIC); -// restApp.start(); -// for (String schema: schemas) { -// int id = restApp.restClient.registerSchema(schema, subject); -// ids.add(id); -// waitUntilIdExists(restApp.restClient, id, "Expected id to be available."); -// } -// restApp.stop(); -// -// // Sanity check id counter then remove it -// int zkIdCounter = getZkIdCounter(zkClient); -// assertEquals("Incorrect ZK id counter.", ID_BATCH_SIZE, zkIdCounter); -// zkClient.delete(ZK_ID_COUNTER_PATH); -// -// // start up another app instance and verify zk id node -// restApp = new RestApp(choosePort(), zkConnect, KAFKASTORE_TOPIC); -// restApp.start(); -// zkIdCounter = getZkIdCounter(zkClient); -// assertEquals("ZK id counter was incorrectly initialized.", 2 * ID_BATCH_SIZE, zkIdCounter); -// restApp.stop(); -// } -// -// @Test -// /** Verify expected value of zk schema id counter when schema registry starts up. */ -// public void testZkCounterOnStartup() throws Exception { -// RestApp restApp = new RestApp(choosePort(), zkConnect, KAFKASTORE_TOPIC); -// restApp.start(); -// -// int zkIdCounter = getZkIdCounter(zkClient); -// assertEquals("Initial value of ZooKeeper id counter is incorrect.", ID_BATCH_SIZE, zkIdCounter); -// -// restApp.stop(); -// } -// -// /** Return the first node which reports itself as master, or null if none does. */ -// private static RestApp findMaster(Collection cluster) { -// for (RestApp restApp: cluster) { -// if (restApp.isMaster()) { -// return restApp; -// } -// } -// -// return null; -// } -// -// /** Verify that no node in cluster reports itself as master. */ -// private static void checkNoneIsMaster(Collection cluster) { -// assertNull("Expected none of the nodes in this cluster to report itself as master.", findMaster(cluster)); -// } -// -// /** Verify that all nodes agree on the expected master identity. */ -// private static void checkMasterIdentity(Collection cluster, -// RegistryIdentity expectedMasterIdentity) { -// for (RestApp restApp: cluster) { -// assertEquals("Each master identity should be " + expectedMasterIdentity, -// expectedMasterIdentity, restApp.masterIdentity()); -// } -// } -// -// private static int getZkIdCounter(ZkClient zkClient) { -// return Integer.valueOf(ZkUtils.readData( -// zkClient, ZK_ID_COUNTER_PATH).getData()); -// } -// -// /** -// * Return set of identities of all nodes reported as master. Expect this to be a set of -// * size 1 unless there is some pathological behavior. -// */ -// private static Set getMasterIdentities(Collection cluster) { -// Set masterIdentities = new HashSet<>(); -// for (RestApp app: cluster) { -// if (app != null && app.masterIdentity() != null) { -// masterIdentities.add(app.masterIdentity()); -// } -// } -// -// return masterIdentities; -// } -// -// /** -// * Check that exactly one RestApp in the cluster reports itself as master. -// */ -// private static RestApp checkOneMaster(Collection cluster) { -// int masterCount = 0; -// RestApp master = null; -// for (RestApp restApp: cluster) { -// if (restApp.isMaster()) { -// masterCount++; -// master = restApp; -// } -// } -// -// assertEquals("Expected one master but found " + masterCount, 1, masterCount); -// return master; -// } -// -// private void waitUntilMasterElectionCompletes(final Collection cluster) { -// if (cluster == null || cluster.size() == 0) { -// return; -// } -// -// Callable newMasterElected = new Callable() { -// @Override -// public Boolean call() throws Exception { -// boolean hasMaster = findMaster(cluster) != null; -// // Check that new master identity has propagated to all nodes -// boolean oneReportedMaster = getMasterIdentities(cluster).size() == 1; -// -// return hasMaster && oneReportedMaster; -// } -// }; -// TestUtils.waitUntilTrue( -// newMasterElected, 5000, "A node should have been elected master by now."); -// } -// -// private void waitUntilIdExists(final RestService restService, final int expectedId, String errorMsg) { -// Callable canGetSchemaById = new Callable() { -// @Override -// public Boolean call() throws Exception { -// try { -// restService.getId(expectedId); -// return true; -// } catch (RestClientException e) { -// return false; -// } -// } -// }; -// TestUtils.waitUntilTrue(canGetSchemaById, 5000, errorMsg); -// } -// -// private void waitUntilCompatibilityLevelSet(final RestService restService, final String subject, -// final String expectedCompatibilityLevel, -// String errorMsg) { -// Callable canGetSchemaById = new Callable() { -// @Override -// public Boolean call() throws Exception { -// try { -// String actualCompatibilityLevel = restService.getConfig(subject).getCompatibilityLevel(); -// return expectedCompatibilityLevel.compareTo(actualCompatibilityLevel) == 0; -// } catch (RestClientException e) { -// return false; -// } -// } -// }; -// TestUtils.waitUntilTrue(canGetSchemaById, 5000, errorMsg); -// } -// -// private void verifyIdAndSchema(final RestService restService, final int expectedId, -// final String expectedSchemaString, String errMsg) { -// waitUntilIdExists(restService, expectedId, errMsg); -// String schemaString = null; -// try { -// schemaString = restService.getId(expectedId) -// .getSchemaString(); -// } catch (IOException e) { -// fail(errMsg); -// } catch (RestClientException e) { -// fail(errMsg); -// } -// -// assertEquals(errMsg, expectedSchemaString, schemaString); -// } -//} diff --git a/logisland-framework/logisland-bootstrap/pom.xml b/logisland-framework/logisland-bootstrap/pom.xml index 458592dc8..0ef40256f 100644 --- a/logisland-framework/logisland-bootstrap/pom.xml +++ b/logisland-framework/logisland-bootstrap/pom.xml @@ -88,10 +88,6 @@ com.hurence.logisland logisland-spark_2_1-engine_${scala.binary.version} - - com.hurence.logisland - logisland-agent - diff --git a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/SparkJobLauncher.java b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/SparkJobLauncher.java deleted file mode 100644 index 234e7f777..000000000 --- a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/SparkJobLauncher.java +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.runner; - -import com.hurence.logisland.component.RestComponentFactory; -import com.hurence.logisland.engine.EngineContext; -import org.apache.commons.cli.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Optional; - - -public class SparkJobLauncher { - - public static final String AGENT = "agent"; - public static final String JOB = "job"; - private static Logger logger = LoggerFactory.getLogger(SparkJobLauncher.class); - - - /** - * main entry point - * - * @param args - */ - public static void main(String[] args) { - - logger.info("starting StreamProcessingRunner"); - - ////////////////////////////////////////// - // Commande lien management - Parser parser = new GnuParser(); - Options options = new Options(); - - - String helpMsg = "Print this message."; - Option help = new Option("help", helpMsg); - options.addOption(help); - - OptionBuilder.withArgName(AGENT); - OptionBuilder.withLongOpt("agent-quorum"); - OptionBuilder.isRequired(); - OptionBuilder.hasArg(); - OptionBuilder.withDescription("logisland agent quorum like host1:8081,host2:8081"); - Option agent = OptionBuilder.create(AGENT); - options.addOption(agent); - - OptionBuilder.withArgName(JOB); - OptionBuilder.withLongOpt("job-name"); - OptionBuilder.isRequired(); - OptionBuilder.hasArg(); - OptionBuilder.withDescription("logisland agent quorum like host1:8081,host2:8081"); - Option job = OptionBuilder.create(JOB); - options.addOption(job); - - String logisland = - "██╗ ██████╗ ██████╗ ██╗███████╗██╗ █████╗ ███╗ ██╗██████╗ \n" + - "██║ ██╔═══██╗██╔════╝ ██║██╔════╝██║ ██╔══██╗████╗ ██║██╔══██╗\n" + - "██║ ██║ ██║██║ ███╗ ██║███████╗██║ ███████║██╔██╗ ██║██║ ██║\n" + - "██║ ██║ ██║██║ ██║ ██║╚════██║██║ ██╔══██║██║╚██╗██║██║ ██║\n" + - "███████╗╚██████╔╝╚██████╔╝ ██║███████║███████╗██║ ██║██║ ╚████║██████╔╝\n" + - "╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ v0.12.2\n\n\n"; - - System.out.println(logisland); - Optional engineInstance = Optional.empty(); - try { - // parse the command line arguments - CommandLine line = parser.parse(options, args); - String agentQuorum = line.getOptionValue(AGENT); - String jobName = line.getOptionValue(JOB); - - - - // instanciate engine and all the processor from the config - engineInstance = new RestComponentFactory(agentQuorum).getEngineContext(jobName); - assert engineInstance.isPresent(); - assert engineInstance.get().isValid(); - - logger.info("starting Logisland session version {}", engineInstance.get()); - } catch (Exception e) { - logger.error("unable to launch runner : {}", e.toString()); - } - - try { - // start the engine - EngineContext engineContext = engineInstance.get(); - engineInstance.get().getEngine().start(engineContext); - } catch (Exception e) { - logger.error("something went bad while running the job : {}", e); - System.exit(-1); - } - - - } -} diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-consumer b/logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-consumer deleted file mode 100755 index e7e8bb4e3..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-consumer +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# -# Copyright 2014 Confluent Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -base_dir=$(dirname $0)/.. - -# Production jars -export CLASSPATH=$CLASSPATH:$base_dir/share/java/kafka-serde-tools/* - -# Development jars. `mvn package` should collect all the required dependency jars here -for dir in $base_dir/package-kafka-serde-tools/target/kafka-serde-tools-package-*-development; do - export CLASSPATH=$CLASSPATH:$dir/share/java/kafka-serde-tools/* -done - -DEFAULT_AVRO_FORMATTER="--formatter io.confluent.kafka.formatter.AvroMessageFormatter" - -DEFAULT_SCHEMA_REGISTRY_URL="--property schema.registry.url=http://localhost:8081" - -for OPTION in "$@" -do - case $OPTION in - --formatter) - DEFAULT_AVRO_FORMATTER="" - ;; - --*) - ;; - *) - PROPERTY=$OPTION - case $PROPERTY in - schema.registry.url*) - DEFAULT_SCHEMA_REGISTRY_URL="" - ;; - esac - ;; - esac -done -exec $(dirname $0)/logisland-agent-run-class kafka.tools.ConsoleConsumer $DEFAULT_AVRO_FORMATTER $DEFAULT_SCHEMA_REGISTRY_URL "$@" diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-producer b/logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-producer deleted file mode 100755 index 710569b51..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/kafka-avro-console-producer +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -# -# Copyright 2014 Confluent Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -base_dir=$(dirname $0)/.. - -# Production jars -export CLASSPATH=$CLASSPATH:$base_dir/share/java/kafka-serde-tools/* - -# Development jars. `mvn package` should collect all the required dependency jars here -for dir in $base_dir/package-kafka-serde-tools/target/kafka-serde-tools-package-*-development; do - export CLASSPATH=$CLASSPATH:$dir/share/java/kafka-serde-tools/* -done - - -DEFAULT_LINE_READER="--line-reader io.confluent.kafka.formatter.AvroMessageReader" - -DEFAULT_SCHEMA_REGISTRY_URL="--property schema.registry.url=http://localhost:8081" - -for OPTION in "$@" -do - case $OPTION in - --line-reader) - DEFAULT_LINE_READER="" - ;; - --*) - ;; - *) - PROPERTY=$OPTION - case $PROPERTY in - schema.registry.url*) - DEFAULT_SCHEMA_REGISTRY_URL="" - ;; - esac - ;; - esac -done -exec $(dirname $0)/logisland-agent-run-class kafka.tools.ConsoleProducer $DEFAULT_LINE_READER $DEFAULT_SCHEMA_REGISTRY_URL "$@" diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-run-class b/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-run-class deleted file mode 100755 index 689535ab8..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-run-class +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/sh -# -# Copyright 2014 Confluent Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -base_dir=$(dirname $0)/.. - -for dir in $base_dir/lib; do - CLASSPATH=$CLASSPATH:$dir/* -done - -# logj4 settings -if [ "x$LOGISLAND_AGENT_LOG4J_OPTS" = "x" ]; then - # Test for files from dev -> packages so this will work as expected in dev if you have packages - # installed - if [ -e "$base_dir/conf/log4j.properties" ]; then # Dev environment - LOGISLAND_AGENT_LOG4J_OPTS="-Dlog4j.configuration=file:$base_dir/conf/log4j.properties" - elif [ -e "$base_dir/etc/schema-registry/log4j.properties" ]; then # Simple zip file layout - LOGISLAND_AGENT_LOG4J_OPTS="-Dlog4j.configuration=file:$base_dir/etc/schema-registry/log4j.properties" - elif [ -e "/etc/schema-registry/log4j.properties" ]; then # Normal install layout - LOGISLAND_AGENT_LOG4J_OPTS="-Dlog4j.configuration=file:/etc/schema-registry/log4j.properties" - fi -fi - - -echo $LOGISLAND_AGENT_LOG4J_OPTS - -# JMX settings -if [ -z "$LOGISLAND_AGENT_JMX_OPTS" ]; then - LOGISLAND_AGENT_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false " -fi - -# JMX port to use -if [ $JMX_PORT ]; then - LOGISLAND_AGENT_JMX_OPTS="$LOGISLAND_AGENT_JMX_OPTS -Dcom.sun.management.jmxremote.port=$JMX_PORT " -fi - -# Generic jvm settings you want to add -if [ -z "$LOGISLAND_AGENT_OPTS" ]; then - LOGISLAND_AGENT_OPTS="" -fi - -# Which java to use -if [ -z "$JAVA_HOME" ]; then - JAVA="java" -else - JAVA="$JAVA_HOME/bin/java" -fi - -# Memory options -if [ -z "$LOGISLAND_AGENT_HEAP_OPTS" ]; then - LOGISLAND_AGENT_HEAP_OPTS="-Xmx512M" -fi - -# JVM performance options -if [ -z "$LOGISLAND_AGENT_JVM_PERFORMANCE_OPTS" ]; then - LOGISLAND_AGENT_JVM_PERFORMANCE_OPTS="-server -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:+DisableExplicitGC -Djava.awt.headless=true" -fi - -MAIN=$1 -shift - -while [ $# -gt 0 ]; do - COMMAND=$1 - case $COMMAND in - -help) - HELP="true" - shift - ;; - -daemon) - DAEMON_MODE="true" - shift - ;; - *) - break - ;; - esac -done - -if [ "x$$HELP" = "xtrue" ]; then - echo "USAGE: $0 [-daemon] [opts] [-help]" - exit 0 -fi - -# Launch mode -if [ "x$DAEMON_MODE" = "xtrue" ]; then - nohup $JAVA $LOGISLAND_AGENT_HEAP_OPTS $LOGISLAND_AGENT_JVM_PERFORMANCE_OPTS $LOGISLAND_AGENT_JMX_OPTS $LOGISLAND_AGENT_LOG4J_OPTS -cp $CLASSPATH $LOGISLAND_AGENT_OPTS "$MAIN" "$@" 2>&1 < /dev/null & -else - exec $JAVA $LOGISLAND_AGENT_HEAP_OPTS $LOGISLAND_AGENT_JVM_PERFORMANCE_OPTS $LOGISLAND_AGENT_JMX_OPTS $LOGISLAND_AGENT_LOG4J_OPTS -cp $CLASSPATH $LOGISLAND_AGENT_OPTS "$MAIN" "$@" -fi diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-start b/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-start deleted file mode 100755 index 093f39918..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-start +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -# -# Copyright 2014 Confluent Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -exec $(dirname $0)/logisland-agent-run-class com.hurence.logisland.agent.LogislandAgentMain "$@" diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop b/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop deleted file mode 100755 index 18454bafb..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# -# Copyright 2014 Confluent Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# When stopping, search for both the current SchemaRegistryMain class and the deprecated Main class. -exec $(dirname $0)/logisland-agent-stop-service com.hurence.logisland.agent.LogislandAgentMain diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop-service b/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop-service deleted file mode 100755 index 5d22cf4ca..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-agent-stop-service +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance with -# the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -TARGET=`ps ax | egrep -i "$1" | grep java | grep -v grep | awk '{print $1}'` -if [ "x$TARGET" = "x" ]; then - >&2 echo "No running instance found." - exit 1 -fi - -kill $TARGET -for i in `seq 20`; do - sleep 0.25 - ps ax | egrep -i "$1" | grep "$TARGET" > /dev/null - if [ $? -eq 0 ]; then - exit 0 - fi -done - ->&2 echo "Tried to kill $TARGET but never saw it die" -exit 1 diff --git a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-launch-spark-job b/logisland-framework/logisland-resources/src/main/resources/bin/logisland-launch-spark-job deleted file mode 100755 index 91aa49393..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/bin/logisland-launch-spark-job +++ /dev/null @@ -1,268 +0,0 @@ -#!/bin/sh - - - - -#. $(dirname $0)/launcher.sh -lib_dir="$(readlink -f "$(dirname $0)/../lib")" -CONF_DIR="$(readlink -f "$(dirname $0)/../conf")" - -if [ -z "${lib_dir}" ] -then - echo "readlink command doesn't seem to work properly, assuming you're under mac os (brew install coreutils)" - lib_dir="$(greadlink -f "$(dirname $0)/../lib")" - CONF_DIR="$(greadlink -f "$(dirname $0)/../conf")" -fi - - -app_classpath="" -for entry in "$lib_dir"/* -do - if [ -z "$app_classpath" ] - then - app_classpath="$lib$entry" - else - app_classpath="$lib$entry,$app_classpath" - fi -done - - - -app_mainclass="com.hurence.logisland.runner.SparkJobLauncher" - - -MODE="default" -VERBOSE_OPTIONS="" -YARN_CLUSTER_OPTIONS="" - -usage() { - echo "Usage:" - echo - echo " `basename $0` --agent --job [--spark-home ]" - echo - echo "Options:" - echo - echo " --agent : provides the logisland agents quorum address list" - echo " --job : provides the job's name" - echo " --spark-home : sets the SPARK_HOME (defaults to \$SPARK_HOME environment variable)" - echo " --help : displays help" -} - -if [ $# -eq 0 ] -then - usage - exit 1 -fi - -while [ $# -gt 0 ] -do - KEY="$1" - - case $KEY in - --agent) - AGENT_QUORUM="$2" - shift - ;; - --job) - JOB_NAME="$2" - shift - ;; - --verbose) - VERBOSE_OPTIONS="--verbose" - ;; - --spark-home) - SPARK_HOME="$2" - shift - ;; - --help) - usage - exit 0 - ;; - *) - echo "Unsupported option : $KEY" - usage - exit 1 - ;; - esac - shift -done - -if [ -z "${SPARK_HOME}" ] -then - echo "Please provide the --spark-home option or set the SPARK_HOME environment variable" - usage - exit 1 -fi - -if [ ! -f ${SPARK_HOME}/bin/spark-submit ] -then - echo "Invalid SPARK_HOME provided" - exit 1 -fi - -if [ -z "${AGENT_QUORUM}" ] -then - echo "The agent quorum is missing" - usage - exit 1 -fi - -# check if curl is installed -# check if logisland agent is OK -CONF_FILE="${CONF_DIR}/${JOB_NAME}_agent.properties" -`curl -X GET "${AGENT_QUORUM}/jobs/${JOB_NAME}/engine" > ${CONF_FILE}` - - -MODE=`awk '{ if( $1 == "spark.master:" ){ print $2 } }' ${CONF_FILE}` -case ${MODE} in - "yarn") - EXTRA_MODE=`awk '{ if( $1 == "spark.yarn.deploy-mode:" ){ print $2 } }' ${CONF_FILE}` - if [ -z "${EXTRA_MODE}" ] - then - echo "The property \"spark.yarn.deploy-mode\" is missing in config file \"${CONF_FILE}\"" - exit 1 - fi - - if [ ! ${EXTRA_MODE} = "cluster" -a ! ${EXTRA_MODE} = "client" ] - then - echo "The property \"spark.yarn.deploy-mode\" value \"${EXTRA_MODE}\" is not supported" - exit 1 - else - MODE=${MODE}-${EXTRA_MODE} - fi - ;; -esac - -if [ ! -z "${VERBOSE_OPTIONS}" ] -then - echo "Starting with mode \"${MODE}\"" -fi - -case $MODE in - default) - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/logisland-elasticsearch-plugin-[^,]*.jar,#,#'` - ;; - yarn-cluster) - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/logisland-spark*-engine-[^,]*.jar,#,#'` - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/guava-[^,]*.jar,#,#'` - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/elasticsearch-[^,]*.jar,#,#'` - YARN_CLUSTER_OPTIONS="--master yarn --deploy-mode cluster --files ${CONF_FILE}#logisland-configuration.yml,file:${CONF_DIR}/log4j.properties --conf \"spark.driver.extraJavaOptions=-Dlog4j.configuration=log4j.properties\" --conf \"spark.executor.extraJavaOptions=-Dlog4j.configuration=log4j.properties\" --conf spark.ui.showConsoleProgress=false" - - if [ ! -z "$YARN_APP_NAME" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --name ${YARN_APP_NAME}" - else - YARN_APP_NAME=`awk '{ if( $1 == "spark.app.name:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${YARN_APP_NAME}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --name ${YARN_APP_NAME}" - fi - fi - - SPARK_YARN_QUEUE=`awk '{ if( $1 == "spark.yarn.queue:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${SPARK_YARN_QUEUE}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --queue ${SPARK_YARN_QUEUE}" - fi - - DRIVER_CORES=`awk '{ if( $1 == "spark.driver.cores:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${DRIVER_CORES}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --driver-cores ${DRIVER_CORES}" - fi - - DRIVER_MEMORY=`awk '{ if( $1 == "spark.driver.memory:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${DRIVER_MEMORY}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --driver-memory ${DRIVER_MEMORY}" - fi - - EXECUTORS_CORES=`awk '{ if( $1 == "spark.executor.cores:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${EXECUTORS_CORES}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --executor-cores ${EXECUTORS_CORES}" - fi - - EXECUTORS_MEMORY=`awk '{ if( $1 == "spark.executor.memory:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${EXECUTORS_MEMORY}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --executor-memory ${EXECUTORS_MEMORY}" - fi - - EXECUTORS_INSTANCES=`awk '{ if( $1 == "spark.executor.instances:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${EXECUTORS_INSTANCES}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --num-executors ${EXECUTORS_INSTANCES}" - fi - - SPARK_YARN_MAX_APP_ATTEMPTS=`awk '{ if( $1 == "spark.yarn.maxAppAttempts:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${SPARK_YARN_MAX_APP_ATTEMPTS}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --conf spark.yarn.maxAppAttempts=${SPARK_YARN_MAX_APP_ATTEMPTS}" - fi - - SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL=`awk '{ if( $1 == "spark.yarn.am.attemptFailuresValidityInterval:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --conf spark.yarn.am.attemptFailuresValidityInterval=${SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL}" - fi - - SPARK_YARN_MAX_EXECUTOR_FAILURES=`awk '{ if( $1 == "spark.yarn.max.executor.failures:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${SPARK_YARN_MAX_EXECUTOR_FAILURES}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --conf spark.yarn.max.executor.failures=${SPARK_YARN_MAX_EXECUTOR_FAILURES}" - fi - - SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL=`awk '{ if( $1 == "spark.yarn.executor.failuresValidityInterval:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --conf spark.yarn.executor.failuresValidityInterval=${SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL}" - fi - - SPARK_TASK_MAX_FAILURES=`awk '{ if( $1 == "spark.task.maxFailures:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${SPARK_TASK_MAX_FAILURES}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --conf spark.task.maxFailures=${SPARK_TASK_MAX_FAILURES}" - fi - - CONF_FILE="logisland-configuration.yml" - ;; - yarn-client) - - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/logisland-spark*-engine-[^,]*.jar,#,#'` - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/guava-[^,]*.jar,#,#'` - app_classpath=`echo ${app_classpath} | sed 's#,/[^,]*/elasticsearch-[^,]*.jar,#,#'` - YARN_CLUSTER_OPTIONS="--master yarn --deploy-mode client" - - DRIVER_CORES=`awk '{ if( $1 == "spark.driver.cores:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${DRIVER_CORES}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --driver-cores ${DRIVER_CORES}" - fi - - DRIVER_MEMORY=`awk '{ if( $1 == "spark.driver.memory:" ){ print $2 } }' ${CONF_FILE}` - if [ ! -z "${DRIVER_MEMORY}" ] - then - YARN_CLUSTER_OPTIONS="${YARN_CLUSTER_OPTIONS} --driver-memory ${DRIVER_MEMORY}" - fi - ;; -esac - -java_cmd="${SPARK_HOME}/bin/spark-submit ${VERBOSE_OPTIONS} ${YARN_CLUSTER_OPTIONS} ${YARN_APP_NAME_OPTIONS} \ - --class ${app_mainclass} \ - --jars ${app_classpath} \ - ${lib_dir}/logisland-spark*-engine*.jar - --agent ${AGENT_QUORUM} - --job ${JOB_NAME}" - - -echo "Removing temp configuration file : ${CONF_FILE}" -rm -f ${CONF_FILE} - - -if [ ! -z "${VERBOSE_OPTIONS}" ] -then - echo $java_cmd -fi - -exec $java_cmd diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/agent.rst b/logisland-framework/logisland-resources/src/main/resources/docs/agent.rst deleted file mode 100644 index f0c9159ac..000000000 --- a/logisland-framework/logisland-resources/src/main/resources/docs/agent.rst +++ /dev/null @@ -1,115 +0,0 @@ - -The agent -========= - - -![agent smith](http://img09.deviantart.net/b93e/i/2007/141/a/2/matrix_agent_smith__stencil_by_vegetablelambtartary.jpg) - -The Logisland Agent provides a serving layer for your metadata. - -- provides a RESTful interface for storing and retrieving Avro schemas. -- stores a versioned history of all schemas -- provides multiple compatibility settings -- allows evolution of schemas according to the configured compatibility setting. -- provides serializers that plug into Kafka clients that handle schema storage and retrieval for Kafka messages that are sent in the Avro format. - - - -Deployment ----------- -We recommend to use the provided docker compose script. -Be sure to set `127.0.0.1 sandbox` in your `/etc/hosts` file - -.. code-block:: sh - - # start a Docker container containing Kafka - cd $LOGISLAND_HOME - docker-compose -f conf/docker-compose.yml up -d - -Starting the Logisland Agent is simple once its dependencies are running. - -.. code-block:: sh - - # The default settings in logisland.properties work automatically with - # the default settings for local ZooKeeper and Kafka nodes. - bin/logisland-agent-start conf/logisland.properties - - -On production environment you'll need to export SPARK_HOME and HADOOP_CONF_DIR variables : - -.. code-block:: sh - - export SPARK_HOME=/opt/spark-2.1.0-bin-hadoop2.7 - export HADOOP_CONF_DIR=/usr/hdp/current/hadoop-client/conf/ - -Schema registry -_______________ - -The following assumes you have Kafka and an instance of the Logisland Agent running using the default settings. - -A quick list of REST call to schema registry - -.. code-block:: sh - - # Register a new version of a schema under the subject "Kafka-key" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/subjects/Kafka-key/versions - {"id":1} - - # Register a new version of a schema under the subject "Kafka-value" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/subjects/Kafka-value/versions - {"id":1} - - # List all subjects - curl -X GET http://localhost:8081/subjects - ["Kafka-value","Kafka-key"] - - # List all schema versions registered under the subject "Kafka-value" - curl -X GET http://localhost:8081/subjects/Kafka-value/versions - [1] - - # Fetch a schema by globally unique id 1 - curl -X GET http://localhost:8081/schemas/ids/1 - {"schema":"\"string\""} - - # Fetch version 1 of the schema registered under subject "Kafka-value" - curl -X GET http://localhost:8081/subjects/Kafka-value/versions/1 - {"subject":"Kafka-value","version":1,"id":1,"schema":"\"string\""} - - # Fetch the most recently registered schema under subject "Kafka-value" - curl -X GET http://localhost:8081/subjects/Kafka-value/versions/latest - {"subject":"Kafka-value","version":1,"id":1,"schema":"\"string\""} - - # Check whether a schema has been registered under subject "Kafka-key" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/subjects/Kafka-key - {"subject":"Kafka-key","version":1,"id":1,"schema":"\"string\""} - - # Test compatibility of a schema with the latest schema under subject "Kafka-value" - curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"schema": "{\"type\": \"string\"}"}' \ - http://localhost:8081/compatibility/subjects/Kafka-value/versions/latest - {"is_compatible":true} - - # Get top level config - curl -X GET http://localhost:8081/config - {"compatibilityLevel":"BACKWARD"} - - # Update compatibility requirements globally - curl -X PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"compatibility": "NONE"}' \ - http://localhost:8081/config - {"compatibility":"NONE"} - - # Update compatibility requirements under the subject "Kafka-value" - curl -X PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \ - --data '{"compatibility": "BACKWARD"}' \ - http://localhost:8081/config/Kafka-value - {"compatibility":"BACKWARD"} - - - diff --git a/logisland-framework/pom.xml b/logisland-framework/pom.xml index ea5e9441c..bb4cc9891 100644 --- a/logisland-framework/pom.xml +++ b/logisland-framework/pom.xml @@ -35,12 +35,4 @@ logisland-scripting - - - hdp2.5 - - logisland-agent - - - diff --git a/pom.xml b/pom.xml index 7e49bf3dd..c8f330251 100644 --- a/pom.xml +++ b/pom.xml @@ -710,11 +710,6 @@ logisland-hbase-plugin ${project.version} - - com.hurence.logisland - logisland-agent - ${project.version} - com.hurence.logisland logisland-elasticsearch-client-service-api @@ -1343,7 +1338,7 @@ org.apache.maven.plugins maven-javadoc-plugin - -Xdoclint:none + -Xdoclint:none From 7c1d142ee16776483c521d6e4145dccf254da778 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 23 May 2018 15:54:31 +0200 Subject: [PATCH 02/63] Refactor engines to have a base class --- .../spark/BaseStreamProcessingEngine.scala | 477 ++++++++++++++++++ .../spark/KafkaStreamProcessingEngine.scala | 419 +-------------- .../RemoteApiStreamProcessingEngine.scala | 55 ++ 3 files changed, 544 insertions(+), 407 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala new file mode 100644 index 000000000..4f4df00bc --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala @@ -0,0 +1,477 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark + +import java.util +import java.util.Collections +import java.util.regex.Pattern + +import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} +import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} +import com.hurence.logisland.stream.spark.SparkRecordStream +import com.hurence.logisland.util.spark.SparkUtils +import com.hurence.logisland.validator.StandardValidators +import org.apache.spark.groupon.metrics.UserMetricsSystem +import org.apache.spark.streaming.{Milliseconds, StreamingContext} +import org.apache.spark.{SparkConf, SparkContext} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConversions._ + + +object BaseStreamProcessingEngine { + + val SPARK_MASTER = new PropertyDescriptor.Builder() + .name("spark.master") + .description("The url to Spark Master") + .required(true) + // The regex allows "local[K]" with K as an integer, "local[*]", "yarn", "yarn-client", "yarn-cluster" and "spark://HOST[:PORT]" + // there is NO support for "mesos://HOST:PORT" + .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^(yarn(-(client|cluster))?|local\\[[0-9\\*]+\\]|spark:\\/\\/([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+|[a-z][a-z0-9\\.\\-]+)(:[0-9]+)?)$"))) + .defaultValue("local[2]") + .build + + val SPARK_APP_NAME = new PropertyDescriptor.Builder() + .name("spark.app.name") + .description("Tha application name") + .required(true) + .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^[a-zA-z0-9-_\\.]+$"))) + .defaultValue("logisland") + .build + + val SPARK_STREAMING_BATCH_DURATION = new PropertyDescriptor.Builder() + .name("spark.streaming.batchDuration") + .description("") + .required(true) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("2000") + .build + + val SPARK_YARN_DEPLOYMODE = new PropertyDescriptor.Builder() + .name("spark.yarn.deploy-mode") + .description("The yarn deploy mode") + .required(false) + // .allowableValues("client", "cluster") + .build + + val SPARK_YARN_QUEUE = new PropertyDescriptor.Builder() + .name("spark.yarn.queue") + .description("The name of the YARN queue") + .required(false) + // .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("default") + .build + + val memorySizePattern = Pattern.compile("^[0-9]+[mMgG]$"); + val SPARK_DRIVER_MEMORY = new PropertyDescriptor.Builder() + .name("spark.driver.memory") + .description("The memory size for Spark driver") + .required(false) + .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) + .defaultValue("512m") + .build + + val SPARK_EXECUTOR_MEMORY = new PropertyDescriptor.Builder() + .name("spark.executor.memory") + .description("The memory size for Spark executors") + .required(false) + .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) + .defaultValue("1g") + .build + + val SPARK_DRIVER_CORES = new PropertyDescriptor.Builder() + .name("spark.driver.cores") + .description("The number of cores for Spark driver") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("4") + .build + + val SPARK_EXECUTOR_CORES = new PropertyDescriptor.Builder() + .name("spark.executor.cores") + .description("The number of cores for Spark driver") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("1") + .build + + val SPARK_EXECUTOR_INSTANCES = new PropertyDescriptor.Builder() + .name("spark.executor.instances") + .description("The number of instances for Spark app") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .build + + val SPARK_SERIALIZER = new PropertyDescriptor.Builder() + .name("spark.serializer") + .description("Class to use for serializing objects that will be sent over the network " + + "or need to be cached in serialized form") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("org.apache.spark.serializer.KryoSerializer") + .build + + val SPARK_STREAMING_BLOCK_INTERVAL = new PropertyDescriptor.Builder() + .name("spark.streaming.blockInterval") + .description("Interval at which data received by Spark Streaming receivers is chunked into blocks " + + "of data before storing them in Spark. Minimum recommended - 50 ms") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("350") + .build + + + val SPARK_STREAMING_BACKPRESSURE_ENABLED = new PropertyDescriptor.Builder() + .name("spark.streaming.backpressure.enabled") + .description("This enables the Spark Streaming to control the receiving rate based on " + + "the current batch scheduling delays and processing times so that the system " + + "receives only as fast as the system can process.") + .required(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("false") + .build + + val SPARK_STREAMING_UNPERSIST = new PropertyDescriptor.Builder() + .name("spark.streaming.unpersist") + .description("Force RDDs generated and persisted by Spark Streaming to be automatically unpersisted " + + "from Spark's memory. The raw input data received by Spark Streaming is also automatically cleared." + + " Setting this to false will allow the raw data and persisted RDDs to be accessible outside " + + "the streaming application as they will not be cleared automatically. " + + "But it comes at the cost of higher memory usage in Spark.") + .required(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("false") + .build + + val SPARK_UI_PORT = new PropertyDescriptor.Builder() + .name("spark.ui.port") + .description("") + .required(false) + .addValidator(StandardValidators.PORT_VALIDATOR) + .defaultValue("4050") + .build + + val SPARK_STREAMING_TIMEOUT = new PropertyDescriptor.Builder() + .name("spark.streaming.timeout") + .description("") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("-1") + .build + + + val SPARK_STREAMING_UI_RETAINED_BATCHES = new PropertyDescriptor.Builder() + .name("spark.streaming.ui.retainedBatches") + .description("How many batches the Spark Streaming UI and status APIs remember before garbage collecting.") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("200") + .build + + val SPARK_STREAMING_RECEIVER_WAL_ENABLE = new PropertyDescriptor.Builder() + .name("spark.streaming.receiver.writeAheadLog.enable") + .description("Enable write ahead logs for receivers. " + + "All the input data received through receivers will be saved to write ahead logs " + + "that will allow it to be recovered after driver failures.") + .required(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("false") + .build + + + val SPARK_YARN_MAX_APP_ATTEMPTS = new PropertyDescriptor.Builder() + .name("spark.yarn.maxAppAttempts") + .description("Because Spark driver and Application Master share a single JVM," + + " any error in Spark driver stops our long-running job. " + + "Fortunately it is possible to configure maximum number of attempts " + + "that will be made to re-run the application. " + + "It is reasonable to set higher value than default 2 " + + "(derived from YARN cluster property yarn.resourcemanager.am.max-attempts). " + + "4 works quite well, higher value may cause unnecessary restarts" + + " even if the reason of the failure is permanent.") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("4") + .build + + + val SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() + .name("spark.yarn.am.attemptFailuresValidityInterval") + .description("If the application runs for days or weeks without restart " + + "or redeployment on highly utilized cluster, " + + "4 attempts could be exhausted in few hours. " + + "To avoid this situation, the attempt counter should be reset on every hour of so.") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("1h") + .build + + val SPARK_YARN_MAX_EXECUTOR_FAILURES = new PropertyDescriptor.Builder() + .name("spark.yarn.max.executor.failures") + .description("a maximum number of executor failures before the application fails. " + + "By default it is max(2 * num executors, 3), " + + "well suited for batch jobs but not for long-running jobs." + + " The property comes with corresponding validity interval which also should be set." + + "8 * num_executors") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("20") + .build + + + val SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() + .name("spark.yarn.executor.failuresValidityInterval") + .description("If the application runs for days or weeks without restart " + + "or redeployment on highly utilized cluster, " + + "x attempts could be exhausted in few hours. " + + "To avoid this situation, the attempt counter should be reset on every hour of so.") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("1h") + .build + + val SPARK_TASK_MAX_FAILURES = new PropertyDescriptor.Builder() + .name("spark.task.maxFailures") + .description("For long-running jobs you could also consider to boost maximum" + + " number of task failures before giving up the job. " + + "By default tasks will be retried 4 times and then job fails.") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("8") + .build + + val SPARK_MEMORY_STORAGE_FRACTION = new PropertyDescriptor.Builder() + .name("spark.memory.storageFraction") + .description("expresses the size of R as a fraction of M (default 0.5). " + + "R is the storage space within M where cached blocks immune to being evicted by execution.") + .required(false) + .addValidator(StandardValidators.FLOAT_VALIDATOR) + .defaultValue("0.5") + .build + + val SPARK_MEMORY_FRACTION = new PropertyDescriptor.Builder() + .name("spark.memory.fraction") + .description("expresses the size of M as a fraction of the (JVM heap space - 300MB) (default 0.75). " + + "The rest of the space (25%) is reserved for user data structures, internal metadata in Spark, " + + "and safeguarding against OOM errors in the case of sparse and unusually large records.") + .required(false) + .addValidator(StandardValidators.FLOAT_VALIDATOR) + .defaultValue("0.6") + .build + + val FAIR = new AllowableValue("FAIR", "FAIR", "fair sharing") + val FIFO = new AllowableValue("FIFO", "FIFO", "queueing jobs one after another") + + val SPARK_SCHEDULER_MODE = new PropertyDescriptor.Builder() + .name("spark.scheduler.mode") + .description("The scheduling mode between jobs submitted to the same SparkContext. " + + "Can be set to FAIR to use fair sharing instead of queueing jobs one after another. " + + "Useful for multi-user services.") + .required(false) + .allowableValues(FAIR, FIFO) + .defaultValue(FAIR.getValue) + .build +} + +abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { + + private lazy val conf = new SparkConf() + + + private val logger = LoggerFactory.getLogger(classOf[BaseStreamProcessingEngine]) + + + override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { + val descriptors: util.List[PropertyDescriptor] = new util.ArrayList[PropertyDescriptor] + descriptors.add(BaseStreamProcessingEngine.SPARK_APP_NAME) + descriptors.add(BaseStreamProcessingEngine.SPARK_MASTER) + descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_DEPLOYMODE) + descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_QUEUE) + descriptors.add(BaseStreamProcessingEngine.SPARK_DRIVER_MEMORY) + descriptors.add(BaseStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) + descriptors.add(BaseStreamProcessingEngine.SPARK_DRIVER_CORES) + descriptors.add(BaseStreamProcessingEngine.SPARK_EXECUTOR_CORES) + descriptors.add(BaseStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) + descriptors.add(BaseStreamProcessingEngine.SPARK_SERIALIZER) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) + descriptors.add(BaseStreamProcessingEngine.SPARK_UI_PORT) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) + descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) + descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) + descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) + descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) + descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) + descriptors.add(BaseStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) + descriptors.add(BaseStreamProcessingEngine.SPARK_MEMORY_FRACTION) + descriptors.add(BaseStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) + descriptors.add(BaseStreamProcessingEngine.SPARK_SCHEDULER_MODE) + + Collections.unmodifiableList(descriptors) + } + + + /** + * start the engine + * + * @param engineContext + */ + final override def start(engineContext: EngineContext) = { + logger.info("starting Spark Engine") + val timeout = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_STREAMING_TIMEOUT).asInteger().intValue() + val batchDuration = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION).asInteger().intValue() + + val streamingContext = createStreamingContext(engineContext) + + /** + * shutdown context gracefully + */ + sys.ShutdownHookThread { + logger.info("Gracefully stopping Spark Streaming Application") + streamingContext.stop(stopSparkContext = true, stopGracefully = true) + logger.info("Application stopped") + } + + streamingContext.start() + + if (timeout != -1) streamingContext.awaitTerminationOrTimeout(timeout) + else streamingContext.awaitTermination() + + logger.info("stream processing done") + } + + /** + * Hook to customize spark configuration before creating a spark context. + * + * @param sparkConf the preinitialized configuration. + * @param engineContext the engine context. + */ + protected abstract def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit + + + final def createStreamingContext(engineContext: EngineContext): StreamingContext = { + val sparkMaster = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_MASTER).asString + val appName = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_APP_NAME).asString + val batchDuration = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION).asInteger().intValue() + + conf.setAppName(appName) + conf.setMaster(sparkMaster) + + + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_UI_PORT) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_SERIALIZER) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_DRIVER_MEMORY) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_DRIVER_CORES) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_EXECUTOR_CORES) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) + + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_MEMORY_FRACTION) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_SCHEDULER_MODE) + + conf.set("spark.kryo.registrator", "com.hurence.logisland.util.spark.ProtoBufRegistrator") + + + if (sparkMaster startsWith "yarn") { + // Note that SPARK_YARN_DEPLOYMODE is not used by spark itself but only by spark-submit CLI + // That's why we do not need to propagate it here + setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_QUEUE) + } + + customizeSparkConfiguration(conf, engineContext) + + SparkUtils.customizeLogLevels + val sc = getCurrentSparkContext() + UserMetricsSystem.initialize(sc, "LogislandMetrics") + logger.info(s"spark context initialized with master:$sparkMaster, " + + s"appName:$appName, " + + s"batchDuration:$batchDuration ") + logger.info(s"conf : ${conf.toDebugString}") + + val ssc = getCurrentSparkStreamingContext() + setupStreamingContexts(engineContext, ssc) + + ssc + } + + protected final def getCurrentSparkContext(): SparkContext = { + SparkContext.getOrCreate(conf) + } + + protected final def getCurrentSparkStreamingContext(): StreamingContext = { + val batchDuration = conf.get(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getName).toInt + StreamingContext.getActiveOrCreate(() => new StreamingContext(getCurrentSparkContext(), Milliseconds(batchDuration))) + } + + /** + * Override to setup streaming context before starting them. + * + * @param engineContext the engine context. + * @param scc the spark streaming context. + */ + protected abstract def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit + + protected def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { + + // Need to check if the properties are set because those properties are not "requires" + if (engineContext.getPropertyValue(propertyDescriptor).isSet) { + conf.set(propertyDescriptor.getName, engineContext.getPropertyValue(propertyDescriptor).asString) + } + } + + + final override def shutdown(engineContext: EngineContext) = { + logger.info(s"shutting down Spark engine") + engineContext.getStreamContexts.foreach(streamingContext => { + try { + + val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] + val sc = kafkaStream.getStreamContext(); + sc.stop(stopSparkContext = true, stopGracefully = true) + kafkaStream.stop() + } catch { + case ex: Exception => + logger.error("something bad happened, please check Kafka or cluster health : {}", ex.getMessage) + } + + }) + } + + override def onPropertyModified(descriptor: PropertyDescriptor, oldValue: String, newValue: String) = { + logger.info(s"property ${ + descriptor.getName + } value changed from $oldValue to $newValue") + } + +} + + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index 4fbdb3c83..85dd85d0f 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -19,122 +19,17 @@ package com.hurence.logisland.engine.spark import java.util import java.util.Collections -import java.util.regex.Pattern -import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} -import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} +import com.hurence.logisland.component.PropertyDescriptor +import com.hurence.logisland.engine.EngineContext import com.hurence.logisland.stream.spark.SparkRecordStream -import com.hurence.logisland.util.spark.SparkUtils import com.hurence.logisland.validator.StandardValidators -import org.apache.spark.groupon.metrics.UserMetricsSystem -import org.apache.spark.streaming.{Milliseconds, StreamingContext} -import org.apache.spark.{SparkConf, SparkContext} +import org.apache.spark.SparkConf +import org.apache.spark.streaming.StreamingContext import org.slf4j.LoggerFactory -import scala.collection.JavaConversions._ - - object KafkaStreamProcessingEngine { - val SPARK_MASTER = new PropertyDescriptor.Builder() - .name("spark.master") - .description("The url to Spark Master") - .required(true) - // The regex allows "local[K]" with K as an integer, "local[*]", "yarn", "yarn-client", "yarn-cluster" and "spark://HOST[:PORT]" - // there is NO support for "mesos://HOST:PORT" - .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^(yarn(-(client|cluster))?|local\\[[0-9\\*]+\\]|spark:\\/\\/([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+|[a-z][a-z0-9\\.\\-]+)(:[0-9]+)?)$"))) - .defaultValue("local[2]") - .build - - val SPARK_APP_NAME = new PropertyDescriptor.Builder() - .name("spark.app.name") - .description("Tha application name") - .required(true) - .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^[a-zA-z0-9-_\\.]+$"))) - .defaultValue("logisland") - .build - - val SPARK_STREAMING_BATCH_DURATION = new PropertyDescriptor.Builder() - .name("spark.streaming.batchDuration") - .description("") - .required(true) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("2000") - .build - - val SPARK_YARN_DEPLOYMODE = new PropertyDescriptor.Builder() - .name("spark.yarn.deploy-mode") - .description("The yarn deploy mode") - .required(false) - // .allowableValues("client", "cluster") - .build - - val SPARK_YARN_QUEUE = new PropertyDescriptor.Builder() - .name("spark.yarn.queue") - .description("The name of the YARN queue") - .required(false) - // .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("default") - .build - - val memorySizePattern = Pattern.compile("^[0-9]+[mMgG]$"); - val SPARK_DRIVER_MEMORY = new PropertyDescriptor.Builder() - .name("spark.driver.memory") - .description("The memory size for Spark driver") - .required(false) - .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) - .defaultValue("512m") - .build - - val SPARK_EXECUTOR_MEMORY = new PropertyDescriptor.Builder() - .name("spark.executor.memory") - .description("The memory size for Spark executors") - .required(false) - .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) - .defaultValue("1g") - .build - - val SPARK_DRIVER_CORES = new PropertyDescriptor.Builder() - .name("spark.driver.cores") - .description("The number of cores for Spark driver") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("4") - .build - - val SPARK_EXECUTOR_CORES = new PropertyDescriptor.Builder() - .name("spark.executor.cores") - .description("The number of cores for Spark driver") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("1") - .build - - val SPARK_EXECUTOR_INSTANCES = new PropertyDescriptor.Builder() - .name("spark.executor.instances") - .description("The number of instances for Spark app") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .build - - val SPARK_SERIALIZER = new PropertyDescriptor.Builder() - .name("spark.serializer") - .description("Class to use for serializing objects that will be sent over the network " + - "or need to be cached in serialized form") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("org.apache.spark.serializer.KryoSerializer") - .build - - val SPARK_STREAMING_BLOCK_INTERVAL = new PropertyDescriptor.Builder() - .name("spark.streaming.blockInterval") - .description("Interval at which data received by Spark Streaming receivers is chunked into blocks " + - "of data before storing them in Spark. Minimum recommended - 50 ms") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("350") - .build - val SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION = new PropertyDescriptor.Builder() .name("spark.streaming.kafka.maxRatePerPartition") .description("Maximum rate (number of records per second) at which data will be read from each Kafka partition") @@ -143,44 +38,6 @@ object KafkaStreamProcessingEngine { .defaultValue("5000") .build - val SPARK_STREAMING_BACKPRESSURE_ENABLED = new PropertyDescriptor.Builder() - .name("spark.streaming.backpressure.enabled") - .description("This enables the Spark Streaming to control the receiving rate based on " + - "the current batch scheduling delays and processing times so that the system " + - "receives only as fast as the system can process.") - .required(false) - .addValidator(StandardValidators.BOOLEAN_VALIDATOR) - .defaultValue("false") - .build - - val SPARK_STREAMING_UNPERSIST = new PropertyDescriptor.Builder() - .name("spark.streaming.unpersist") - .description("Force RDDs generated and persisted by Spark Streaming to be automatically unpersisted " + - "from Spark's memory. The raw input data received by Spark Streaming is also automatically cleared." + - " Setting this to false will allow the raw data and persisted RDDs to be accessible outside " + - "the streaming application as they will not be cleared automatically. " + - "But it comes at the cost of higher memory usage in Spark.") - .required(false) - .addValidator(StandardValidators.BOOLEAN_VALIDATOR) - .defaultValue("false") - .build - - val SPARK_UI_PORT = new PropertyDescriptor.Builder() - .name("spark.ui.port") - .description("") - .required(false) - .addValidator(StandardValidators.PORT_VALIDATOR) - .defaultValue("4050") - .build - - val SPARK_STREAMING_TIMEOUT = new PropertyDescriptor.Builder() - .name("spark.streaming.timeout") - .description("") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("-1") - .build - val SPARK_STREAMING_KAFKA_MAXRETRIES = new PropertyDescriptor.Builder() .name("spark.streaming.kafka.maxRetries") .description("Maximum rate (number of records per second) at which data will be read from each Kafka partition") @@ -188,258 +45,32 @@ object KafkaStreamProcessingEngine { .addValidator(StandardValidators.INTEGER_VALIDATOR) .defaultValue("3") .build - - val SPARK_STREAMING_UI_RETAINED_BATCHES = new PropertyDescriptor.Builder() - .name("spark.streaming.ui.retainedBatches") - .description("How many batches the Spark Streaming UI and status APIs remember before garbage collecting.") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("200") - .build - - val SPARK_STREAMING_RECEIVER_WAL_ENABLE = new PropertyDescriptor.Builder() - .name("spark.streaming.receiver.writeAheadLog.enable") - .description("Enable write ahead logs for receivers. " + - "All the input data received through receivers will be saved to write ahead logs " + - "that will allow it to be recovered after driver failures.") - .required(false) - .addValidator(StandardValidators.BOOLEAN_VALIDATOR) - .defaultValue("false") - .build - - - val SPARK_YARN_MAX_APP_ATTEMPTS = new PropertyDescriptor.Builder() - .name("spark.yarn.maxAppAttempts") - .description("Because Spark driver and Application Master share a single JVM," + - " any error in Spark driver stops our long-running job. " + - "Fortunately it is possible to configure maximum number of attempts " + - "that will be made to re-run the application. " + - "It is reasonable to set higher value than default 2 " + - "(derived from YARN cluster property yarn.resourcemanager.am.max-attempts). " + - "4 works quite well, higher value may cause unnecessary restarts" + - " even if the reason of the failure is permanent.") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("4") - .build - - - val SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() - .name("spark.yarn.am.attemptFailuresValidityInterval") - .description("If the application runs for days or weeks without restart " + - "or redeployment on highly utilized cluster, " + - "4 attempts could be exhausted in few hours. " + - "To avoid this situation, the attempt counter should be reset on every hour of so.") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("1h") - .build - - val SPARK_YARN_MAX_EXECUTOR_FAILURES = new PropertyDescriptor.Builder() - .name("spark.yarn.max.executor.failures") - .description("a maximum number of executor failures before the application fails. " + - "By default it is max(2 * num executors, 3), " + - "well suited for batch jobs but not for long-running jobs." + - " The property comes with corresponding validity interval which also should be set." + - "8 * num_executors") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("20") - .build - - - val SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() - .name("spark.yarn.executor.failuresValidityInterval") - .description("If the application runs for days or weeks without restart " + - "or redeployment on highly utilized cluster, " + - "x attempts could be exhausted in few hours. " + - "To avoid this situation, the attempt counter should be reset on every hour of so.") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("1h") - .build - - val SPARK_TASK_MAX_FAILURES = new PropertyDescriptor.Builder() - .name("spark.task.maxFailures") - .description("For long-running jobs you could also consider to boost maximum" + - " number of task failures before giving up the job. " + - "By default tasks will be retried 4 times and then job fails.") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("8") - .build - - val SPARK_MEMORY_STORAGE_FRACTION = new PropertyDescriptor.Builder() - .name("spark.memory.storageFraction") - .description("expresses the size of R as a fraction of M (default 0.5). " + - "R is the storage space within M where cached blocks immune to being evicted by execution.") - .required(false) - .addValidator(StandardValidators.FLOAT_VALIDATOR) - .defaultValue("0.5") - .build - - val SPARK_MEMORY_FRACTION = new PropertyDescriptor.Builder() - .name("spark.memory.fraction") - .description("expresses the size of M as a fraction of the (JVM heap space - 300MB) (default 0.75). " + - "The rest of the space (25%) is reserved for user data structures, internal metadata in Spark, " + - "and safeguarding against OOM errors in the case of sparse and unusually large records.") - .required(false) - .addValidator(StandardValidators.FLOAT_VALIDATOR) - .defaultValue("0.6") - .build - - val FAIR = new AllowableValue("FAIR", "FAIR", "fair sharing") - val FIFO = new AllowableValue("FIFO", "FIFO", "queueing jobs one after another") - - val SPARK_SCHEDULER_MODE = new PropertyDescriptor.Builder() - .name("spark.scheduler.mode") - .description("The scheduling mode between jobs submitted to the same SparkContext. " + - "Can be set to FAIR to use fair sharing instead of queueing jobs one after another. " + - "Useful for multi-user services.") - .required(false) - .allowableValues(FAIR, FIFO) - .defaultValue(FAIR.getValue) - .build } -class KafkaStreamProcessingEngine extends AbstractProcessingEngine { +class KafkaStreamProcessingEngine extends BaseStreamProcessingEngine { private val logger = LoggerFactory.getLogger(classOf[KafkaStreamProcessingEngine]) override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { val descriptors: util.List[PropertyDescriptor] = new util.ArrayList[PropertyDescriptor] - descriptors.add(KafkaStreamProcessingEngine.SPARK_APP_NAME) - descriptors.add(KafkaStreamProcessingEngine.SPARK_MASTER) - descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_DEPLOYMODE) - descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_QUEUE) - descriptors.add(KafkaStreamProcessingEngine.SPARK_DRIVER_MEMORY) - descriptors.add(KafkaStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) - descriptors.add(KafkaStreamProcessingEngine.SPARK_DRIVER_CORES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_EXECUTOR_CORES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_SERIALIZER) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) + descriptors.addAll(super.getSupportedPropertyDescriptors) descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) - descriptors.add(KafkaStreamProcessingEngine.SPARK_UI_PORT) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) - descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) - descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) - descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) - descriptors.add(KafkaStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) - descriptors.add(KafkaStreamProcessingEngine.SPARK_MEMORY_FRACTION) - descriptors.add(KafkaStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) - descriptors.add(KafkaStreamProcessingEngine.SPARK_SCHEDULER_MODE) - Collections.unmodifiableList(descriptors) } - /** - * start the engine - * - * @param engineContext - */ - override def start(engineContext: EngineContext) = { - logger.info("starting Spark Engine") - val timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT).asInteger().intValue() - val streamingContext = createStreamingContext(engineContext) - - /** - * shutdown context gracefully - */ - sys.ShutdownHookThread { - logger.info("Gracefully stopping Spark Streaming Application") - streamingContext.stop(stopSparkContext = true, stopGracefully = true) - logger.info("Application stopped") - } - - streamingContext.start() + override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { + setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) + setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) - if (timeout != -1) streamingContext.awaitTerminationOrTimeout(timeout) - else streamingContext.awaitTermination() - - logger.info("stream processing done") } - - def createStreamingContext(engineContext: EngineContext): StreamingContext = { - - val sparkMaster = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_MASTER).asString - val appName = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_APP_NAME).asString - val batchDuration = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION).asInteger().intValue() - - /** - * job configuration - */ - val conf = new SparkConf() - - conf.setAppName(appName) - conf.setMaster(sparkMaster) - - def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { - - // Need to check if the properties are set because those properties are not "requires" - if (engineContext.getPropertyValue(propertyDescriptor).isSet) { - conf.set(propertyDescriptor.getName, engineContext.getPropertyValue(propertyDescriptor).asString) - } - } - - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_UI_PORT) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_SERIALIZER) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_DRIVER_MEMORY) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_DRIVER_CORES) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_EXECUTOR_CORES) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) - - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_MEMORY_FRACTION) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_SCHEDULER_MODE) - - conf.set("spark.kryo.registrator", "com.hurence.logisland.util.spark.ProtoBufRegistrator") - - if (sparkMaster startsWith "yarn") { - // Note that SPARK_YARN_DEPLOYMODE is not used by spark itself but only by spark-submit CLI - // That's why we do not need to propagate it here - setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_QUEUE) - } - - SparkUtils.customizeLogLevels - @transient val sc = new SparkContext(conf) - @transient val ssc = new StreamingContext(sc, Milliseconds(batchDuration)) - UserMetricsSystem.initialize(sc, "LogislandMetrics") - - logger.info(s"spark context initialized with master:$sparkMaster, " + - s"appName:$appName, " + - s"batchDuration:$batchDuration ") - logger.info(s"conf : ${conf.toDebugString}") - - - /** - * loop over processContext - */ - engineContext.getStreamContexts.foreach(streamingContext => { + override protected def setupStreamingContexts(engineContext: EngineContext, ssc: StreamingContext): Unit = { + val appName = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_APP_NAME).asString + engineContext.getStreamContexts.forEach(streamingContext => { try { val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] kafkaStream.setup(appName, ssc, streamingContext, engineContext) @@ -450,33 +81,7 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { } }) - ssc } - - - override def shutdown(engineContext: EngineContext) = { - logger.info(s"shuting down Spark engine") - engineContext.getStreamContexts.foreach(streamingContext => { - try { - - val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] - val sc = kafkaStream.getStreamContext(); - sc.stop(stopSparkContext = true, stopGracefully = true) - kafkaStream.stop() - } catch { - case ex: Exception => - logger.error("something bad happened, please check Kafka or cluster health : {}", ex.getMessage) - } - - }) - } - - override def onPropertyModified(descriptor: PropertyDescriptor, oldValue: String, newValue: String) = { - logger.info(s"property ${ - descriptor.getName - } value changed from $oldValue to $newValue") - } - } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala new file mode 100644 index 000000000..0fa91805d --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -0,0 +1,55 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark + +import java.util +import java.util.Collections + +import com.hurence.logisland.component.PropertyDescriptor +import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} +import com.hurence.logisland.stream.spark.SparkRecordStream +import com.hurence.logisland.util.spark.SparkUtils +import org.apache.spark.groupon.metrics.UserMetricsSystem +import org.apache.spark.streaming.{Milliseconds, StreamingContext} +import org.apache.spark.{SparkConf, SparkContext} +import org.slf4j.LoggerFactory + +import scala.collection.JavaConversions._ + + +class RemoteApiStreamProcessingEngine extends BaseStreamProcessingEngine { + + private val logger = LoggerFactory.getLogger(classOf[RemoteApiStreamProcessingEngine]) + + override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { + + } + + /** + * Override to setup streaming context before starting them. + * + * @param engineContext the engine context. + * @param scc the spark streaming context. + */ + override protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit = { + + } + +} + + From 295e4bea85c1f6d4fcfb801a80eed389dc0dbf8d Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Thu, 24 May 2018 17:31:14 +0200 Subject: [PATCH 03/63] Add rest client and jackson model --- .../logisland-spark_2_1-engine/pom.xml | 38 +++- .../engine/spark/remote/RemoteApiClient.java | 121 ++++++++++++ .../engine/spark/remote/model/Component.java | 186 ++++++++++++++++++ .../engine/spark/remote/model/Error.java | 123 ++++++++++++ .../engine/spark/remote/model/Pipeline.java | 173 ++++++++++++++++ .../engine/spark/remote/model/Processor.java | 68 +++++++ .../engine/spark/remote/model/Property.java | 150 ++++++++++++++ .../engine/spark/remote/model/Service.java | 68 +++++++ .../engine/spark/remote/model/Stream.java | 108 ++++++++++ .../src/main/resources/api.yaml | 139 +++++++++++++ .../spark/BaseStreamProcessingEngine.scala | 7 +- .../spark/KafkaStreamProcessingEngine.scala | 13 +- .../RemoteApiStreamProcessingEngine.scala | 55 ------ 13 files changed, 1179 insertions(+), 70 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml delete mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index 75f33d54e..3ba2c6ff6 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -27,6 +27,23 @@ http://www.w3.org/2001/XMLSchema-instance "> logisland-spark_2_1-engine_${scala.binary.version} jar + + + + 1.9.2 + 2.9.9 + 1.19.4 + 1.5.16 + 1.0.5 + 1.0.0 + 2.9.2 + + 4.12 + 3.1.5 + 3.0.4 + 0.3.5 + + com.hurence.logisland @@ -136,20 +153,28 @@ http://www.w3.org/2001/XMLSchema-instance "> test + + - org.glassfish.jersey.media - jersey-media-json-jackson - test + io.swagger + swagger-core + ${swagger-core-version} - + - - + + com.squareup.okhttp3 + okhttp-urlconnection + 3.10.0 + + + + net.alchim31.maven scala-maven-plugin @@ -159,6 +184,7 @@ http://www.w3.org/2001/XMLSchema-instance "> scala-compile-first process-resources + add-source compile diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java new file mode 100644 index 000000000..b44bcec85 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -0,0 +1,121 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.ISO8601DateFormat; +import com.hurence.logisland.engine.spark.remote.model.Pipeline; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import java.time.Duration; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Rest client wrapper for pipelines remote APIs. + * + * @author amarziali + */ +public class RemoteApiClient { + + private static final Logger logger = LoggerFactory.getLogger(RemoteApiClient.class); + + private static final String PIPELINES_RESOURCE_URI = "pipelines"; + private static final CollectionType pipelineType = TypeFactory.defaultInstance().constructCollectionType(List.class, Pipeline.class); + + private final OkHttpClient client; + private final HttpUrl baseUrl; + private final ObjectMapper mapper; + + + /** + * Constructs a new instance. + * If username and password are provided, the client will be configured to supply a basic authentication. + * + * @param baseUrl the base url + * @param socketTimeout the read/write socket timeout + * @param connectTimeout the connection socket timeout + * @param username the username if a basic authentication is needed. + * @param password the password if a basic authentication is needed. + */ + public RemoteApiClient(String baseUrl, Duration socketTimeout, Duration connectTimeout, + String username, String password) { + this.baseUrl = HttpUrl.parse(baseUrl); + this.mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.setDateFormat(new ISO8601DateFormat()); + + OkHttpClient.Builder builder = new OkHttpClient() + .newBuilder() + .readTimeout(socketTimeout.toMillis(), TimeUnit.MILLISECONDS) + .writeTimeout(socketTimeout.toMillis(), TimeUnit.MILLISECONDS) + .connectTimeout(connectTimeout.toMillis(), TimeUnit.MILLISECONDS) + .followRedirects(true) + .followSslRedirects(true); + //add basic auth if needed. + if (username != null && password != null) { + builder.addInterceptor(chain -> { + Request originalRequest = chain.request(); + Request requestWithBasicAuth = originalRequest + .newBuilder() + .header(HttpHeaders.AUTHORIZATION, Credentials.basic(username, password)) + .build(); + return chain.proceed(requestWithBasicAuth); + }); + } + this.client = builder.build(); + } + + /** + * Fetches pipelines from a remote server. + * + * @return a list of {@link Pipeline} (never null). Empty in case of error or no results. + */ + public List fetchPipelines() { + Request request = new Request.Builder() + .url(baseUrl.newBuilder().addPathSegment(PIPELINES_RESOURCE_URI).build()) + .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .get() + .build(); + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + logger.error("Error refreshing pipelines from remote server. Got code {}", response.code()); + + } + return mapper.readValue(response.body().byteStream(), pipelineType); + } catch (Exception e) { + logger.error("Unable to refresh pipelines from remote server", e); + } + + return Collections.emptyList(); + + + } + + +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java new file mode 100755 index 000000000..38b7e69b6 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java @@ -0,0 +1,186 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Component + */ +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Component { + @JsonProperty("name") + private String name = null; + + @JsonProperty("component") + private String component = null; + + @JsonProperty("documentation") + private String documentation = null; + + @JsonProperty("config") + @Valid + private List config = new ArrayList(); + + public Component name(String name) { + this.name = name; + return this; + } + + /** + * Get name + * + * @return name + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Component component(String component) { + this.component = component; + return this; + } + + /** + * Get component + * + * @return component + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public Component documentation(String documentation) { + this.documentation = documentation; + return this; + } + + /** + * Get documentation + * + * @return documentation + **/ + @ApiModelProperty(value = "") + + + public String getDocumentation() { + return documentation; + } + + public void setDocumentation(String documentation) { + this.documentation = documentation; + } + + public Component config(List config) { + this.config = config; + return this; + } + + public Component addConfigItem(Property configItem) { + this.config.add(configItem); + return this; + } + + /** + * Get config + * + * @return config + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + @Valid + + public List getConfig() { + return config; + } + + public void setConfig(List config) { + this.config = config; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Component component = (Component) o; + return Objects.equals(this.name, component.name) && + Objects.equals(this.component, component.component) && + Objects.equals(this.documentation, component.documentation) && + Objects.equals(this.config, component.config); + } + + @Override + public int hashCode() { + return Objects.hash(name, component, documentation, config); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Component {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" component: ").append(toIndentedString(component)).append("\n"); + sb.append(" documentation: ").append(toIndentedString(documentation)).append("\n"); + sb.append(" config: ").append(toIndentedString(config)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java new file mode 100755 index 000000000..67406e475 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java @@ -0,0 +1,123 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotNull; +import java.util.Objects; + +/** + * Error + */ +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Error { + @JsonProperty("code") + private Integer code = null; + + @JsonProperty("message") + private String message = null; + + public Error code(Integer code) { + this.code = code; + return this; + } + + /** + * Get code + * + * @return code + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public Error message(String message) { + this.message = message; + return this; + } + + /** + * Get message + * + * @return message + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Error error = (Error) o; + return Objects.equals(this.code, error.code) && + Objects.equals(this.message, error.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, message); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Error {\n"); + + sb.append(" code: ").append(toIndentedString(code)).append("\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java new file mode 100755 index 000000000..ce0e3ceac --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java @@ -0,0 +1,173 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A streaming pipeline. + */ +@ApiModel(description = "A streaming pipeline.") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Pipeline { + @JsonProperty("name") + private String name = null; + + @JsonProperty("services") + @Valid + private List services = null; + + @JsonProperty("streams") + @Valid + private List streams = null; + + public Pipeline name(String name) { + this.name = name; + return this; + } + + /** + * the name of the pipeline + * + * @return name + **/ + @ApiModelProperty(required = true, value = "the name of the pipeline") + @NotNull + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Pipeline services(List services) { + this.services = services; + return this; + } + + public Pipeline addServicesItem(Service servicesItem) { + if (this.services == null) { + this.services = new ArrayList(); + } + this.services.add(servicesItem); + return this; + } + + /** + * The service controllers. + * + * @return services + **/ + @ApiModelProperty(value = "The service controllers.") + + @Valid + + public List getServices() { + return services; + } + + public void setServices(List services) { + this.services = services; + } + + public Pipeline streams(List streams) { + this.streams = streams; + return this; + } + + public Pipeline addStreamsItem(Stream streamsItem) { + if (this.streams == null) { + this.streams = new ArrayList(); + } + this.streams.add(streamsItem); + return this; + } + + /** + * The engine properties. + * + * @return streams + **/ + @ApiModelProperty(value = "The engine properties.") + + @Valid + + public List getStreams() { + return streams; + } + + public void setStreams(List streams) { + this.streams = streams; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Pipeline pipeline = (Pipeline) o; + return Objects.equals(this.name, pipeline.name) && + Objects.equals(this.services, pipeline.services) && + Objects.equals(this.streams, pipeline.streams); + } + + @Override + public int hashCode() { + return Objects.hash(name, services, streams); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Pipeline {\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" services: ").append(toIndentedString(services)).append("\n"); + sb.append(" streams: ").append(toIndentedString(streams)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java new file mode 100755 index 000000000..73798ad92 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java @@ -0,0 +1,68 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import io.swagger.annotations.ApiModel; + +import java.util.Objects; + +/** + * A logisland 'controller service'. + */ +@ApiModel(description = "A logisland 'controller service'.") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Processor extends Component { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Processor {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java new file mode 100755 index 000000000..3cd8053d6 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java @@ -0,0 +1,150 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.constraints.NotNull; +import java.util.Objects; + +/** + * Property + */ + +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Property { + @JsonProperty("key") + private String key = null; + + @JsonProperty("type") + private String type = "string"; + + @JsonProperty("value") + private String value = null; + + public Property key(String key) { + this.key = key; + return this; + } + + /** + * Get key + * + * @return key + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public Property type(String type) { + this.type = type; + return this; + } + + /** + * Get type + * + * @return type + **/ + @ApiModelProperty(value = "") + + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Property value(String value) { + this.value = value; + return this; + } + + /** + * Get value + * + * @return value + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Property property = (Property) o; + return Objects.equals(this.key, property.key) && + Objects.equals(this.type, property.type) && + Objects.equals(this.value, property.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, type, value); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Property {\n"); + + sb.append(" key: ").append(toIndentedString(key)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append(" value: ").append(toIndentedString(value)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java new file mode 100755 index 000000000..3795906e4 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java @@ -0,0 +1,68 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import io.swagger.annotations.ApiModel; + +import java.util.Objects; + +/** + * A logisland 'controller service'. + */ +@ApiModel(description = "A logisland 'controller service'.") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Service extends Component { + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Service {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java new file mode 100755 index 000000000..05896fae6 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java @@ -0,0 +1,108 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Stream + */ +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") + +public class Stream extends Component { + @JsonProperty("processors") + @Valid + private List processors = null; + + public Stream processors(List processors) { + this.processors = processors; + return this; + } + + public Stream addProcessorsItem(Processor processorsItem) { + if (this.processors == null) { + this.processors = new ArrayList(); + } + this.processors.add(processorsItem); + return this; + } + + /** + * Get processors + * + * @return processors + **/ + @ApiModelProperty(value = "") + + @Valid + + public List getProcessors() { + return processors; + } + + public void setProcessors(List processors) { + this.processors = processors; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Stream stream = (Stream) o; + return Objects.equals(this.processors, stream.processors) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(processors, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Stream {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" processors: ").append(toIndentedString(processors)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml new file mode 100644 index 000000000..f3d9e1a76 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml @@ -0,0 +1,139 @@ +swagger: '2.0' +info: + description: >- + REST API for logisland. + + + The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to trigger configuration changes. + + version: v1 + title: Logisland standard API + contact: + name: Hurence + email: support@hurence.com +host: localhost:8081 +schemes: + - http + - https +consumes: + - application/json +produces: + - application/json +paths: + + /pipelines: + get: + tags: + - pipelines + operationId: pollActivePipelines + summary: Retrieves the streaming pipelines to run. + description: Logisland will poll this API in order to start, reconfigure or stop the pipelines according to the received response. + responses: + "200": + description: >- + should return every pipeline that should be running. + On server side, logisland will do the delta and apply the following: + + - Add a new pipeline if any provided is not already running. + + - Reconfigure a pipeline if the same is already running and its configuration differs + + - Remove a pipeline if running but no more present in returned ones. + schema: + type: array + items: + $ref: '#/definitions/Pipeline' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + +definitions: + + Property: + type: object + required: + - key + - value + properties: + key: + type: string + type: + type: string + default: "string" + value: + type: string + + Component: + type: object + required: + - component + - config + - name + properties: + name: + type: string + component: + type: string + documentation: + type: string + config: + type: array + items: + $ref: '#/definitions/Property' + + + + Service: + type: object + description: A logisland 'controller service'. + allOf: + - $ref: '#/definitions/Component' + + + Processor: + type: object + description: A logisland 'controller service'. + allOf: + - $ref: '#/definitions/Component' + + Stream: + type: object + allOf: + - $ref: '#/definitions/Component' + - properties: + processors: + type: array + items: + $ref: '#/definitions/Processor' + + Pipeline: + type: object + description: A streaming pipeline. + properties: + name: + type: string + description: the name of the pipeline + services: + type: array + description: The service controllers. + items: + $ref: '#/definitions/Service' + streams: + type: array + description: The engine properties. + items: + $ref: '#/definitions/Stream' + required: + - name + + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala index 4f4df00bc..884abbe56 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala @@ -23,6 +23,7 @@ import java.util.regex.Pattern import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} +import com.hurence.logisland.stream.StreamContext import com.hurence.logisland.stream.spark.SparkRecordStream import com.hurence.logisland.util.spark.SparkUtils import com.hurence.logisland.validator.StandardValidators @@ -364,7 +365,7 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { * @param sparkConf the preinitialized configuration. * @param engineContext the engine context. */ - protected abstract def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit + protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit final def createStreamingContext(engineContext: EngineContext): StreamingContext = { @@ -438,7 +439,7 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { * @param engineContext the engine context. * @param scc the spark streaming context. */ - protected abstract def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit + protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit protected def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { @@ -451,7 +452,7 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { final override def shutdown(engineContext: EngineContext) = { logger.info(s"shutting down Spark engine") - engineContext.getStreamContexts.foreach(streamingContext => { + engineContext.getStreamContexts foreach (streamingContext => { try { val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index 85dd85d0f..b26977911 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -28,6 +28,8 @@ import org.apache.spark.SparkConf import org.apache.spark.streaming.StreamingContext import org.slf4j.LoggerFactory +import scala.collection.JavaConverters._ + object KafkaStreamProcessingEngine { val SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION = new PropertyDescriptor.Builder() @@ -62,24 +64,23 @@ class KafkaStreamProcessingEngine extends BaseStreamProcessingEngine { } - override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { + override def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) } - override protected def setupStreamingContexts(engineContext: EngineContext, ssc: StreamingContext): Unit = { + override def setupStreamingContexts(engineContext: EngineContext, ssc: StreamingContext): Unit = { val appName = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_APP_NAME).asString - engineContext.getStreamContexts.forEach(streamingContext => { + engineContext.getStreamContexts.asScala.foreach(streamContext => { try { - val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] - kafkaStream.setup(appName, ssc, streamingContext, engineContext) + val kafkaStream = streamContext.getStream.asInstanceOf[SparkRecordStream] + kafkaStream.setup(appName, ssc, streamContext, engineContext) kafkaStream.start() } catch { case ex: Exception => logger.error("something bad happened, please check Kafka or cluster health : {}", ex.getMessage) } - }) } } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala deleted file mode 100644 index 0fa91805d..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ /dev/null @@ -1,55 +0,0 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.hurence.logisland.engine.spark - -import java.util -import java.util.Collections - -import com.hurence.logisland.component.PropertyDescriptor -import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} -import com.hurence.logisland.stream.spark.SparkRecordStream -import com.hurence.logisland.util.spark.SparkUtils -import org.apache.spark.groupon.metrics.UserMetricsSystem -import org.apache.spark.streaming.{Milliseconds, StreamingContext} -import org.apache.spark.{SparkConf, SparkContext} -import org.slf4j.LoggerFactory - -import scala.collection.JavaConversions._ - - -class RemoteApiStreamProcessingEngine extends BaseStreamProcessingEngine { - - private val logger = LoggerFactory.getLogger(classOf[RemoteApiStreamProcessingEngine]) - - override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { - - } - - /** - * Override to setup streaming context before starting them. - * - * @param engineContext the engine context. - * @param scc the spark streaming context. - */ - override protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit = { - - } - -} - - From e9bb04de1ee60144ccfdd79faf678b681a6bc5d4 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 25 May 2018 09:41:31 +0200 Subject: [PATCH 04/63] Added unit tests --- .../logisland-spark_2_1-engine/pom.xml | 8 +++ .../spark/remote/RemoteApiClientTest.java | 69 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index 3ba2c6ff6..5ebfeb9a2 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -169,6 +169,14 @@ http://www.w3.org/2001/XMLSchema-instance "> 3.10.0 + + + com.squareup.okhttp3 + mockwebserver + 3.10.0 + test + + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java new file mode 100644 index 000000000..d2a0a7ce1 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java @@ -0,0 +1,69 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote; + +import okhttp3.Credentials; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.BDDMockito; + +import javax.ws.rs.core.HttpHeaders; +import java.rmi.Remote; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +public class RemoteApiClientTest { + + private RemoteApiClient createInstance(MockWebServer server, String user, String password) { + return new RemoteApiClient(server.url("/").toString(), + Duration.ofSeconds(2), Duration.ofSeconds(2), user, password); + } + + @Test + public void testAllUnsecured() throws Exception { + try (MockWebServer mockWebServer = new MockWebServer()) { + mockWebServer.enqueue(new MockResponse().setResponseCode(404)); + mockWebServer.enqueue(new MockResponse().setBodyDelay(3, TimeUnit.SECONDS)); + mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\",\"services\":[{}],\"streams\":[{}]}]")); + RemoteApiClient client = createInstance(mockWebServer, null, null); + Assert.assertTrue(client.fetchPipelines().isEmpty()); + Assert.assertTrue(client.fetchPipelines().isEmpty()); + Assert.assertEquals(1, client.fetchPipelines().size()); + } + + + + + } + + @Test + public void testAuthentication() throws Exception { + try (MockWebServer mockWebServer = new MockWebServer()) { + RemoteApiClient client = createInstance(mockWebServer, "test", "test"); + mockWebServer.enqueue(new MockResponse().setBody("[]")); + client.fetchPipelines(); + RecordedRequest request = mockWebServer.takeRequest(); + String auth = request.getHeader(HttpHeaders.AUTHORIZATION); + Assert.assertEquals(Credentials.basic("test", "test"), auth); + } + } +} From d671500a934ce930582e88aeeaa92c5df468416e Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 28 May 2018 16:14:05 +0200 Subject: [PATCH 05/63] Cleanup some dependences. Use latest fasterxml jackson (not the codehaus artefacts) --- .../logisland/engine/EngineContext.java | 24 +++- .../engine/StandardEngineContext.java | 10 ++ .../logisland-spark_2_1-engine/pom.xml | 21 +-- .../engine/spark/remote/RemoteApiClient.java | 4 +- .../remote/RemoteApiComponentFactory.java | 124 ++++++++++++++++++ .../spark/remote/RemoteComponentRegistry.java | 68 ++++++++++ .../engine/spark/remote/model/Pipeline.java | 46 +++---- .../spark/remote/RemoteApiClientTest.java | 2 +- logisland-framework/logisland-utils/pom.xml | 4 - .../logisland/util/string/JsonUtil.java | 6 +- .../logisland-cyber-security-plugin/pom.xml | 12 ++ .../logisland-elasticsearch-plugin/pom.xml | 6 +- .../logisland-excel-plugin/pom.xml | 8 ++ .../logisland-hbase-plugin/pom.xml | 6 +- .../pom.xml | 11 ++ .../outlier/streaming/OutlierConfig.java | 9 +- .../logisland-querymatcher-plugin/pom.xml | 4 + .../hurence/logisland/processor/MatchIP.java | 2 +- .../logisland-sampling-plugin/pom.xml | 5 + .../logisland/sampling/AverageSampler.java | 2 +- .../processor/scripting/python/RunPython.java | 4 +- pom.xml | 69 +--------- 22 files changed, 320 insertions(+), 127 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java index 6326ddf07..a27be5961 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -36,6 +36,13 @@ public interface EngineContext extends ComponentContext { */ void addStreamContext(StreamContext streamContext); + /** + * Removes a stream to the collection of Streams + * + * @param streamContext the Stream to add + */ + void removeStreamContext(StreamContext streamContext); + /** * @return the engine @@ -51,7 +58,14 @@ public interface EngineContext extends ComponentContext { /** * add a ControllerServiceConfiguration * - * @param config to add + * @param config to add */ void addControllerServiceConfiguration(ControllerServiceConfiguration config); + + /** + * Removes a {@link ControllerServiceConfiguration} + * + * @param config to remove + */ + void removeControllerServiceConfiguration(ControllerServiceConfiguration config); } diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java index 9bb5e0eaf..f515c7ab1 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java @@ -48,6 +48,11 @@ public void addStreamContext(StreamContext streamContext) { streamContexts.add(streamContext); } + @Override + public void removeStreamContext(StreamContext streamContext) { + streamContexts.remove(streamContext); + } + @Override public ProcessingEngine getEngine() { return (ProcessingEngine) component; @@ -91,4 +96,9 @@ public Collection getControllerServiceConfigurat public void addControllerServiceConfiguration(ControllerServiceConfiguration config) { controllerServiceConfigurations.add(config); } + + @Override + public void removeControllerServiceConfiguration(ControllerServiceConfiguration config) { + controllerServiceConfigurations.remove(config); + } } diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index 5ebfeb9a2..0fa5627d9 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -30,13 +30,7 @@ http://www.w3.org/2001/XMLSchema-instance "> - 1.9.2 - 2.9.9 - 1.19.4 1.5.16 - 1.0.5 - 1.0.0 - 2.9.2 4.12 3.1.5 @@ -118,6 +112,19 @@ http://www.w3.org/2001/XMLSchema-instance "> com.fasterxml.jackson.core jackson-annotations + + com.fasterxml.jackson.module + jackson-module-parameter-names + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + @@ -154,7 +161,6 @@ http://www.w3.org/2001/XMLSchema-instance "> - io.swagger swagger-core @@ -178,7 +184,6 @@ http://www.w3.org/2001/XMLSchema-instance "> - diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index b44bcec85..e9492abb3 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.ISO8601DateFormat; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.hurence.logisland.engine.spark.remote.model.Pipeline; import okhttp3.*; import org.slf4j.Logger; @@ -68,7 +69,8 @@ public RemoteApiClient(String baseUrl, Duration socketTimeout, Duration connectT this.mapper = new ObjectMapper(); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - mapper.setDateFormat(new ISO8601DateFormat()); + mapper.registerModule(new JavaTimeModule()) + .findAndRegisterModules(); OkHttpClient.Builder builder = new OkHttpClient() .newBuilder() diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java new file mode 100644 index 000000000..673aae760 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -0,0 +1,124 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote; + +import com.hurence.logisland.config.ControllerServiceConfiguration; +import com.hurence.logisland.engine.spark.remote.model.Processor; +import com.hurence.logisland.engine.spark.remote.model.Property; +import com.hurence.logisland.engine.spark.remote.model.Service; +import com.hurence.logisland.engine.spark.remote.model.Stream; +import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.processor.StandardProcessContext; +import com.hurence.logisland.stream.RecordStream; +import com.hurence.logisland.stream.StandardStreamContext; +import com.hurence.logisland.stream.StreamContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.stream.Collectors; + +public class RemoteApiComponentFactory { + + private static final Logger logger = LoggerFactory.getLogger(RemoteApiComponentFactory.class); + + + /** + * Instantiates a stream from of configuration + * + * @param stream + * @return + */ + public Optional getStreamContext(Stream stream) { + try { + final RecordStream recordStream = + (RecordStream) Class.forName(stream.getComponent()).newInstance(); + final StreamContext instance = + new StandardStreamContext(recordStream, stream.getName()); + + // instantiate each related processor + stream.getProcessors().forEach(processor -> { + Optional processorContext = getProcessContext(processor); + if (processorContext.isPresent()) + instance.addProcessContext(processorContext.get()); + }); + + // set the config properties + stream.getConfig().forEach(e -> instance.setProperty(e.getKey(), e.getValue())); + + + logger.info("created stream {}", stream.getName()); + return Optional.of(instance); + + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + logger.error("unable to instantiate stream " + stream.getName(), e); + } + return Optional.empty(); + } + + /** + * Constructs processors. + * + * @param processor the processor bean. + * @return optionally the constructed processor context or nothing in case of error. + */ + public Optional getProcessContext(Processor processor) { + try { + final com.hurence.logisland.processor.Processor processorInstance = + (com.hurence.logisland.processor.Processor) Class.forName(processor.getComponent()).newInstance(); + final ProcessContext processContext = + new StandardProcessContext(processorInstance, processor.getName()); + + // set all properties + processor.getConfig().forEach(e -> processContext.setProperty(e.getKey(), e.getValue())); + + logger.info("created processor {}", processor); + return Optional.of(processContext); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + logger.error("unable to instantiate processor " + processor.getComponent(), e); + } + + return Optional.empty(); + } + + + /** + * Constructs controller services. + * + * @param service the service bean. + * @return optionally the constructed service configuration or nothing in case of error. + */ + public Optional getControllerServiceConfiguration(Service service) { + try { + ControllerServiceConfiguration configuration = new ControllerServiceConfiguration(); + configuration.setControllerService(service.getName()); + configuration.setComponent(service.getComponent()); + configuration.setDocumentation(service.getDocumentation()); + configuration.setType("service"); + configuration.setConfiguration(service.getConfig().stream() + .collect(Collectors.toMap(Property::getKey, Property::getValue))); + + logger.info("created service {}", service.getName()); + return Optional.of(configuration); + } catch (Exception e) { + logger.error("unable to configure service " + service.getComponent(), e); + } + + return Optional.empty(); + } +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java new file mode 100644 index 000000000..a34ed32ee --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java @@ -0,0 +1,68 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote; + +import com.hurence.logisland.engine.EngineContext; +import com.hurence.logisland.engine.spark.remote.model.Pipeline; +import com.hurence.logisland.stream.StreamContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class RemoteComponentRegistry { + + private static final Logger logger = LoggerFactory.getLogger(RemoteComponentRegistry.class); + + private final EngineContext engineContext; + private final Map> registry = new HashMap<>(); + private final RemoteApiComponentFactory remoteApiComponentFactory = new RemoteApiComponentFactory(); + + public RemoteComponentRegistry(EngineContext engineContext) { + this.engineContext = engineContext; + } + + public void updateEngineContext(Collection pipelines) { + //remove missing items + registry.keySet().stream() + .filter(pipeline -> !pipelines.contains(pipeline)) + .collect(Collectors.toList()) + //remove active streams inside this pipeline + .forEach(pipeline -> registry.remove(pipeline).forEach(engineContext -> { + logger.info("Pipeline {} : stopping engine {}", pipeline.getName(), engineContext.getName()); + try { + engineContext.getEngine().shutdown(engineContext); + logger.info("Pipeline {} : successfully stopped engine {}", pipeline.getName(), engineContext.getName()); + } catch (Exception e) { + logger.error("Pipeline {} : unexpected error stopping engine {}: {}", pipeline.getName(), engineContext.getName(), e.getMessage()); + } + })); + //add new items + pipelines.stream() + .filter(pipeline -> !registry.containsKey(pipeline)) + .collect(Collectors.toList()) + .forEach(pipeline -> { + + }); + + } +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java index ce0e3ceac..ca0e7ca71 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java @@ -23,7 +23,9 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; +import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Objects; @@ -33,9 +35,9 @@ @ApiModel(description = "A streaming pipeline.") @javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") -public class Pipeline { - @JsonProperty("name") - private String name = null; +public class Pipeline extends Component { + @JsonProperty("lastModified") + private OffsetDateTime lastModified = null; @JsonProperty("services") @Valid @@ -45,26 +47,25 @@ public class Pipeline { @Valid private List streams = null; - public Pipeline name(String name) { - this.name = name; + public Pipeline lastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; return this; } /** - * the name of the pipeline - * - * @return name + * the last modified timestamp of this pipeline (used to trigger changes). + * @return lastModified **/ - @ApiModelProperty(required = true, value = "the name of the pipeline") - @NotNull + @ApiModelProperty(value = "the last modified timestamp of this pipeline (used to trigger changes).") + @Valid - public String getName() { - return name; + public OffsetDateTime getLastModified() { + return lastModified; } - public void setName(String name) { - this.name = name; + public void setLastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; } public Pipeline services(List services) { @@ -82,7 +83,6 @@ public Pipeline addServicesItem(Service servicesItem) { /** * The service controllers. - * * @return services **/ @ApiModelProperty(value = "The service controllers.") @@ -112,7 +112,6 @@ public Pipeline addStreamsItem(Stream streamsItem) { /** * The engine properties. - * * @return streams **/ @ApiModelProperty(value = "The engine properties.") @@ -129,7 +128,7 @@ public void setStreams(List streams) { @Override - public boolean equals(Object o) { + public boolean equals(java.lang.Object o) { if (this == o) { return true; } @@ -137,22 +136,23 @@ public boolean equals(Object o) { return false; } Pipeline pipeline = (Pipeline) o; - return Objects.equals(this.name, pipeline.name) && + return Objects.equals(this.lastModified, pipeline.lastModified) && Objects.equals(this.services, pipeline.services) && - Objects.equals(this.streams, pipeline.streams); + Objects.equals(this.streams, pipeline.streams) && + super.equals(o); } @Override public int hashCode() { - return Objects.hash(name, services, streams); + return Objects.hash(lastModified, services, streams, super.hashCode()); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Pipeline {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" lastModified: ").append(toIndentedString(lastModified)).append("\n"); sb.append(" services: ").append(toIndentedString(services)).append("\n"); sb.append(" streams: ").append(toIndentedString(streams)).append("\n"); sb.append("}"); @@ -163,7 +163,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(Object o) { + private String toIndentedString(java.lang.Object o) { if (o == null) { return "null"; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java index d2a0a7ce1..62309c739 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java @@ -43,7 +43,7 @@ public void testAllUnsecured() throws Exception { try (MockWebServer mockWebServer = new MockWebServer()) { mockWebServer.enqueue(new MockResponse().setResponseCode(404)); mockWebServer.enqueue(new MockResponse().setBodyDelay(3, TimeUnit.SECONDS)); - mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\",\"services\":[{}],\"streams\":[{}]}]")); + mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\", \"lastModified\":\"1983-06-04T10:01:02.345\",\"services\":[{}],\"streams\":[{}]}]")); RemoteApiClient client = createInstance(mockWebServer, null, null); Assert.assertTrue(client.fetchPipelines().isEmpty()); Assert.assertTrue(client.fetchPipelines().isEmpty()); diff --git a/logisland-framework/logisland-utils/pom.xml b/logisland-framework/logisland-utils/pom.xml index 16025b2a2..fd51ae333 100644 --- a/logisland-framework/logisland-utils/pom.xml +++ b/logisland-framework/logisland-utils/pom.xml @@ -62,10 +62,6 @@ joda-time - - org.codehaus.jackson - jackson-mapper-asl - com.fasterxml.jackson.core jackson-databind diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/JsonUtil.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/JsonUtil.java index 0841fe005..513d0a231 100755 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/JsonUtil.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/JsonUtil.java @@ -15,9 +15,9 @@ */ package com.hurence.logisland.util.string; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.hurence.logisland.util.time.DateUtil; -import org.codehaus.jackson.map.ObjectMapper; -import org.codehaus.jackson.type.TypeReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +43,7 @@ public static Map convertJsonToMap(String json) { Map retMap = new HashMap<>(); if (json != null) { try { - retMap = mapper.readValue(json, new TypeReference>() { + retMap = mapper.readValue(json, new TypeReference> () { }); } catch (IOException e) { logger.warn("Error while reading Java Map from JSON response: " + json, e); diff --git a/logisland-plugins/logisland-cyber-security-plugin/pom.xml b/logisland-plugins/logisland-cyber-security-plugin/pom.xml index 17c57baf7..220b7e284 100644 --- a/logisland-plugins/logisland-cyber-security-plugin/pom.xml +++ b/logisland-plugins/logisland-cyber-security-plugin/pom.xml @@ -46,6 +46,18 @@ com.hurence.logisland logisland-utils + + com.google.guava + guava + + + com.googlecode.json-simple + json-simple + + + org.apache.commons + commons-lang3 + org.slf4j slf4j-simple diff --git a/logisland-plugins/logisland-elasticsearch-plugin/pom.xml b/logisland-plugins/logisland-elasticsearch-plugin/pom.xml index ae682bac5..a44b9114e 100644 --- a/logisland-plugins/logisland-elasticsearch-plugin/pom.xml +++ b/logisland-plugins/logisland-elasticsearch-plugin/pom.xml @@ -66,11 +66,13 @@ commons-io test - + + joda-time + joda-time + com.fasterxml.jackson.core jackson-core - 2.6.6 com.hurence.logisland diff --git a/logisland-plugins/logisland-excel-plugin/pom.xml b/logisland-plugins/logisland-excel-plugin/pom.xml index ae213a192..80f060093 100644 --- a/logisland-plugins/logisland-excel-plugin/pom.xml +++ b/logisland-plugins/logisland-excel-plugin/pom.xml @@ -51,6 +51,14 @@ poi-ooxml ${poi.version} + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + org.slf4j slf4j-simple diff --git a/logisland-plugins/logisland-hbase-plugin/pom.xml b/logisland-plugins/logisland-hbase-plugin/pom.xml index 5aa1c0e84..d072865b0 100644 --- a/logisland-plugins/logisland-hbase-plugin/pom.xml +++ b/logisland-plugins/logisland-hbase-plugin/pom.xml @@ -17,10 +17,7 @@ org.apache.commons commons-lang3 - - org.codehaus.jackson - jackson-mapper-asl - + org.mockito mockito-all @@ -34,7 +31,6 @@ com.fasterxml.jackson.core jackson-databind - test diff --git a/logisland-plugins/logisland-outlier-detection-plugin/pom.xml b/logisland-plugins/logisland-outlier-detection-plugin/pom.xml index 1b74322fa..7590f0d08 100644 --- a/logisland-plugins/logisland-outlier-detection-plugin/pom.xml +++ b/logisland-plugins/logisland-outlier-detection-plugin/pom.xml @@ -87,11 +87,22 @@ RELEASE + + com.google.guava + guava + + com.google.code.findbugs jsr305 3.0.2 + + + commons-io + commons-io + test + - - io.swagger - swagger-jersey2-jaxrs - compile - ${swagger-core-version} - - - javax.servlet - servlet-api - ${servlet-api-version} - - - org.glassfish.jersey.containers - jersey-container-servlet-core - ${jersey2-version} - - - org.glassfish.jersey.media - jersey-media-multipart - ${jersey2-version} - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${jersey2-version} - - - org.glassfish.jersey.core - jersey-client - ${jersey2-version} - - - org.glassfish.jersey.ext - jersey-proxy-client - ${jersey2-version} - + + org.glassfish + javax.el + 3.0.0 + + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index e9492abb3..24f528f3e 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -22,18 +22,23 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.TypeFactory; -import com.fasterxml.jackson.databind.util.ISO8601DateFormat; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.hurence.logisland.engine.spark.remote.model.Pipeline; import okhttp3.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import java.time.Duration; import java.util.Collections; +import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -47,6 +52,7 @@ public class RemoteApiClient { private static final String PIPELINES_RESOURCE_URI = "pipelines"; private static final CollectionType pipelineType = TypeFactory.defaultInstance().constructCollectionType(List.class, Pipeline.class); + private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private final OkHttpClient client; private final HttpUrl baseUrl; @@ -109,7 +115,10 @@ public List fetchPipelines() { logger.error("Error refreshing pipelines from remote server. Got code {}", response.code()); } - return mapper.readValue(response.body().byteStream(), pipelineType); + List ret = mapper.readValue(response.body().byteStream(), pipelineType); + //validate against javax.validation annotations. + ret.forEach(RemoteApiClient::doValidate); + return ret; } catch (Exception e) { logger.error("Unable to refresh pipelines from remote server", e); } @@ -119,5 +128,26 @@ public List fetchPipelines() { } + /** + * Perform validation of the given bean. + * + * @param bean the instance to validate + * @see javax.validation.Validator#validate + */ + private static void doValidate(Object bean) { + Set> result = validator.validate(bean); + if (!result.isEmpty()) { + StringBuilder sb = new StringBuilder("Bean validation failed: "); + for (Iterator> it = result.iterator(); it.hasNext(); ) { + ConstraintViolation violation = it.next(); + sb.append(violation.getPropertyPath()).append(" - ").append(violation.getMessage()); + if (it.hasNext()) { + sb.append("; "); + } + } + throw new ConstraintViolationException(sb.toString(), result); + } + } + } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java index 38b7e69b6..bc0022d04 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java @@ -33,9 +33,11 @@ public class Component { @JsonProperty("name") + @NotNull private String name = null; @JsonProperty("component") + @NotNull private String component = null; @JsonProperty("documentation") @@ -43,6 +45,7 @@ public class Component { @JsonProperty("config") @Valid + @NotNull private List config = new ArrayList(); public Component name(String name) { @@ -56,9 +59,6 @@ public Component name(String name) { * @return name **/ @ApiModelProperty(required = true, value = "") - @NotNull - - public String getName() { return name; } @@ -78,9 +78,6 @@ public Component component(String component) { * @return component **/ @ApiModelProperty(required = true, value = "") - @NotNull - - public String getComponent() { return component; } @@ -100,8 +97,6 @@ public Component documentation(String documentation) { * @return documentation **/ @ApiModelProperty(value = "") - - public String getDocumentation() { return documentation; } @@ -125,11 +120,7 @@ public Component addConfigItem(Property configItem) { * * @return config **/ - @ApiModelProperty(required = true, value = "") - @NotNull - - @Valid - + @ApiModelProperty(required = false, value = "") public List getConfig() { return config; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java index 67406e475..3aaa5ac20 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java @@ -29,9 +29,11 @@ @javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") public class Error { + @NotNull @JsonProperty("code") private Integer code = null; + @NotNull @JsonProperty("message") private String message = null; @@ -46,7 +48,6 @@ public Error code(Integer code) { * @return code **/ @ApiModelProperty(required = true, value = "") - @NotNull public Integer getCode() { @@ -68,9 +69,6 @@ public Error message(String message) { * @return message **/ @ApiModelProperty(required = true, value = "") - @NotNull - - public String getMessage() { return message; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java index ca0e7ca71..655a88366 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java @@ -23,9 +23,9 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; import java.time.OffsetDateTime; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Objects; @@ -34,9 +34,13 @@ */ @ApiModel(description = "A streaming pipeline.") @javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") +public class Pipeline { + @JsonProperty("name") + @NotNull + private String name = null; -public class Pipeline extends Component { @JsonProperty("lastModified") + @NotNull private OffsetDateTime lastModified = null; @JsonProperty("services") @@ -45,8 +49,29 @@ public class Pipeline extends Component { @JsonProperty("streams") @Valid + @Size(min = 1) + @NotNull private List streams = null; + public Pipeline name(String name) { + this.name = name; + return this; + } + + /** + * The pipeline name + * + * @return name + **/ + @ApiModelProperty(required = true, value = "The pipeline name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + public Pipeline lastModified(OffsetDateTime lastModified) { this.lastModified = lastModified; return this; @@ -54,12 +79,10 @@ public Pipeline lastModified(OffsetDateTime lastModified) { /** * the last modified timestamp of this pipeline (used to trigger changes). + * * @return lastModified **/ - @ApiModelProperty(value = "the last modified timestamp of this pipeline (used to trigger changes).") - - @Valid - + @ApiModelProperty(required = true, value = "the last modified timestamp of this pipeline (used to trigger changes).") public OffsetDateTime getLastModified() { return lastModified; } @@ -83,12 +106,10 @@ public Pipeline addServicesItem(Service servicesItem) { /** * The service controllers. + * * @return services **/ @ApiModelProperty(value = "The service controllers.") - - @Valid - public List getServices() { return services; } @@ -112,12 +133,10 @@ public Pipeline addStreamsItem(Stream streamsItem) { /** * The engine properties. + * * @return streams **/ @ApiModelProperty(value = "The engine properties.") - - @Valid - public List getStreams() { return streams; } @@ -136,22 +155,23 @@ public boolean equals(java.lang.Object o) { return false; } Pipeline pipeline = (Pipeline) o; - return Objects.equals(this.lastModified, pipeline.lastModified) && + return Objects.equals(this.name, pipeline.name) && + Objects.equals(this.lastModified, pipeline.lastModified) && Objects.equals(this.services, pipeline.services) && - Objects.equals(this.streams, pipeline.streams) && - super.equals(o); + Objects.equals(this.streams, pipeline.streams); } @Override public int hashCode() { - return Objects.hash(lastModified, services, streams, super.hashCode()); + return Objects.hash(name, lastModified, services, streams); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Pipeline {\n"); - sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + + sb.append(" name: ").append(toIndentedString(name)).append("\n"); sb.append(" lastModified: ").append(toIndentedString(lastModified)).append("\n"); sb.append(" services: ").append(toIndentedString(services)).append("\n"); sb.append(" streams: ").append(toIndentedString(streams)).append("\n"); @@ -171,3 +191,5 @@ private String toIndentedString(java.lang.Object o) { } } + + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java index 3cd8053d6..64aff47aa 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java @@ -31,12 +31,14 @@ public class Property { @JsonProperty("key") + @NotNull private String key = null; @JsonProperty("type") private String type = "string"; @JsonProperty("value") + @NotNull private String value = null; public Property key(String key) { @@ -50,9 +52,6 @@ public Property key(String key) { * @return key **/ @ApiModelProperty(required = true, value = "") - @NotNull - - public String getKey() { return key; } @@ -72,8 +71,6 @@ public Property type(String type) { * @return type **/ @ApiModelProperty(value = "") - - public String getType() { return type; } @@ -93,9 +90,6 @@ public Property value(String value) { * @return value **/ @ApiModelProperty(required = true, value = "") - @NotNull - - public String getValue() { return value; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java index 05896fae6..0e174fa35 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java @@ -54,9 +54,6 @@ public Stream addProcessorsItem(Processor processorsItem) { * @return processors **/ @ApiModelProperty(value = "") - - @Valid - public List getProcessors() { return processors; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml index f3d9e1a76..2ad733062 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml @@ -36,7 +36,7 @@ paths: - Add a new pipeline if any provided is not already running. - - Reconfigure a pipeline if the same is already running and its configuration differs + - Reconfigure a pipeline (stop and then start) if same pipiline already running and but its lastUpdated is older than the one provided - Remove a pipeline if running but no more present in returned ones. schema: @@ -113,7 +113,11 @@ definitions: properties: name: type: string - description: the name of the pipeline + description: The pipeline name + lastModified: + type: string + format: date-time + description: the last modified timestamp of this pipeline (used to trigger changes). services: type: array description: The service controllers. @@ -122,10 +126,12 @@ definitions: streams: type: array description: The engine properties. + minItems: 1 items: $ref: '#/definitions/Stream' required: - name + - lastModified Error: required: diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java index 62309c739..1899a0a71 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java @@ -21,13 +21,11 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.apache.commons.lang3.StringUtils; import org.junit.Assert; import org.junit.Test; -import org.mockito.BDDMockito; +import javax.validation.ConstraintViolationException; import javax.ws.rs.core.HttpHeaders; -import java.rmi.Remote; import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -43,7 +41,9 @@ public void testAllUnsecured() throws Exception { try (MockWebServer mockWebServer = new MockWebServer()) { mockWebServer.enqueue(new MockResponse().setResponseCode(404)); mockWebServer.enqueue(new MockResponse().setBodyDelay(3, TimeUnit.SECONDS)); - mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\", \"lastModified\":\"1983-06-04T10:01:02.345\",\"services\":[{}],\"streams\":[{}]}]")); + final String dummy = "\"name\":\"myName\", \"component\":\"myComponent\""; + mockWebServer.enqueue(new MockResponse().setBody("[{" + dummy + ",\"lastModified\":\"1983-06-04T10:01:02Z\"," + + "\"streams\":[{" + dummy +"}]}]")); RemoteApiClient client = createInstance(mockWebServer, null, null); Assert.assertTrue(client.fetchPipelines().isEmpty()); Assert.assertTrue(client.fetchPipelines().isEmpty()); @@ -51,6 +51,15 @@ public void testAllUnsecured() throws Exception { } + } + + @Test(expected = ConstraintViolationException.class) + public void testValidationFails() throws Exception { + try (MockWebServer mockWebServer = new MockWebServer()) { + mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\", \"lastModified\":\"1983-06-04T10:01:02Z\",\"services\":[{}],\"streams\":[{}]}]")); + RemoteApiClient client = createInstance(mockWebServer, null, null); + client.fetchPipelines(); + } } From 339ef84cbf45ebc3adb2812eb88705672f19f7de Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 28 May 2018 17:13:48 +0200 Subject: [PATCH 08/63] Updated API --- .../engine/spark/remote/model/Error.java | 121 ------------------ .../src/main/resources/api.yaml | 13 -- 2 files changed, 134 deletions(-) delete mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java deleted file mode 100755 index 3aaa5ac20..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Error.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.hurence.logisland.engine.spark.remote.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.annotations.ApiModelProperty; - -import javax.validation.constraints.NotNull; -import java.util.Objects; - -/** - * Error - */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") - -public class Error { - @NotNull - @JsonProperty("code") - private Integer code = null; - - @NotNull - @JsonProperty("message") - private String message = null; - - public Error code(Integer code) { - this.code = code; - return this; - } - - /** - * Get code - * - * @return code - **/ - @ApiModelProperty(required = true, value = "") - - - public Integer getCode() { - return code; - } - - public void setCode(Integer code) { - this.code = code; - } - - public Error message(String message) { - this.message = message; - return this; - } - - /** - * Get message - * - * @return message - **/ - @ApiModelProperty(required = true, value = "") - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Error error = (Error) o; - return Objects.equals(this.code, error.code) && - Objects.equals(this.message, error.message); - } - - @Override - public int hashCode() { - return Objects.hash(code, message); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class Error {\n"); - - sb.append(" code: ").append(toIndentedString(code)).append("\n"); - sb.append(" message: ").append(toIndentedString(message)).append("\n"); - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); - } -} - diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml index 2ad733062..2aecbdca4 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml @@ -45,8 +45,6 @@ paths: $ref: '#/definitions/Pipeline' default: description: unexpected error - schema: - $ref: '#/definitions/Error' definitions: @@ -68,7 +66,6 @@ definitions: type: object required: - component - - config - name properties: name: @@ -133,13 +130,3 @@ definitions: - name - lastModified - Error: - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string From 5c0911eeeb5ab9459407fb933bb064f5e4f6fc64 Mon Sep 17 00:00:00 2001 From: oalam Date: Mon, 28 May 2018 17:59:37 +0200 Subject: [PATCH 09/63] first implementation of ComputeTags based on nashorn sandboxed --- .../component/AbstractPropertyValue.java | 3 +- .../logisland/record/FieldDictionary.java | 4 - .../util/runner/MockPropertyValue.java | 3 +- .../pom.xml | 8 + .../hurence/logisland/processor/ModifyId.java | 2 +- .../logisland/processor/SplitText.java | 14 +- .../processor/alerting/ComputeTag.java | 299 ++++++++++++++++++ .../logisland/processor/ModifyIdTest.java | 2 +- .../logisland/processor/SplitTextTest.java | 12 +- .../processor/alerting/ComputeTagsTest.java | 146 +++++++++ .../logisland/processor/DetectOutliers.java | 2 +- 11 files changed, 471 insertions(+), 24 deletions(-) create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java b/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java index e03db2aac..4e5b2676a 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java @@ -87,8 +87,7 @@ public boolean isSet() { @Override public Record asRecord() { return (getRawValue() == null) ? null : new StandardRecord() - .setStringField(FieldDictionary.RECORD_VALUE,getRawValue().trim()) - .setStringField(FieldDictionary.RECORD_RAW_VALUE,getRawValue().trim()); + .setStringField(FieldDictionary.RECORD_VALUE,getRawValue().trim()); } @Override diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java b/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java index 0205e117c..f8aa6ef8b 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java @@ -28,8 +28,6 @@ public class FieldDictionary { public static String RECORD_DAYTIME = "record_daytime"; public static String RECORD_KEY = "record_key"; public static String RECORD_VALUE = "record_value"; - public static String RECORD_RAW_KEY = "record_raw_key"; - public static String RECORD_RAW_VALUE = "record_raw_value"; public static String RECORD_NAME = "record_name"; public static String PROCESSOR_NAME = "processor_name"; public static String RECORD_POSITION = "record_position"; @@ -59,8 +57,6 @@ public static List asList() { RECORD_DAYTIME, RECORD_KEY, RECORD_VALUE, - RECORD_RAW_KEY, - RECORD_RAW_VALUE, PROCESSOR_NAME, RECORD_POSITION, RECORD_BODY, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockPropertyValue.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockPropertyValue.java index 2bf5500bd..6009f023c 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockPropertyValue.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockPropertyValue.java @@ -95,8 +95,7 @@ public Integer asInteger() { @Override public Record asRecord() { return (getRawValue() == null) ? null : new StandardRecord() - .setStringField(FieldDictionary.RECORD_VALUE,rawValue) - .setStringField(FieldDictionary.RECORD_RAW_VALUE,rawValue); + .setStringField(FieldDictionary.RECORD_VALUE,rawValue); } @Override public Long asLong() { diff --git a/logisland-plugins/logisland-common-processors-plugin/pom.xml b/logisland-plugins/logisland-common-processors-plugin/pom.xml index b6f5b528e..ab077d3ed 100644 --- a/logisland-plugins/logisland-common-processors-plugin/pom.xml +++ b/logisland-plugins/logisland-common-processors-plugin/pom.xml @@ -81,6 +81,14 @@ com.hurence.logisland logisland-elasticsearch-plugin + + + + org.javadelight + delight-nashorn-sandbox + 0.1.14 + + com.hurence.logisland logisland-cache_key_value-service-api diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java index b4dd5bc61..4206b958e 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java @@ -104,7 +104,7 @@ public class ModifyId extends AbstractProcessor { .required(true) .addValidator(StandardValidators.COMMA_SEPARATED_LIST_VALIDATOR) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue(FieldDictionary.RECORD_RAW_VALUE) + .defaultValue(FieldDictionary.RECORD_VALUE) .build(); //TODO determines those values dynamically, used this code to determine those diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/SplitText.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/SplitText.java index a81928510..91b34686c 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/SplitText.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/SplitText.java @@ -81,7 +81,7 @@ public class SplitText extends AbstractProcessor { .required(false) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .addValidator(StandardValidators.COMMA_SEPARATED_LIST_VALIDATOR) - .defaultValue(FieldDictionary.RECORD_RAW_KEY) + .defaultValue(FieldDictionary.RECORD_KEY) .build(); public static final PropertyDescriptor RECORD_TYPE = new PropertyDescriptor.Builder() @@ -220,7 +220,7 @@ public Collection process(ProcessContext context, Collection rec if (keyMatcher.matches()) { if (keepRawContent) { - outputRecord.setField(FieldDictionary.RECORD_RAW_KEY, FieldType.STRING, keyMatcher.group(0)); + outputRecord.setField(FieldDictionary.RECORD_KEY, FieldType.STRING, keyMatcher.group(0)); } for (int i = 0; i < keyMatcher.groupCount() + 1 && i < keyFields.length; i++) { String content = keyMatcher.group(i); @@ -229,7 +229,7 @@ public Collection process(ProcessContext context, Collection rec } } } else { - outputRecord.setField(FieldDictionary.RECORD_RAW_KEY, FieldType.STRING, key); + outputRecord.setField(FieldDictionary.RECORD_KEY, FieldType.STRING, key); } } catch (Exception e) { String errorMessage = "error while matching key " + key + @@ -237,7 +237,7 @@ public Collection process(ProcessContext context, Collection rec " : " + e.getMessage(); logger.warn(errorMessage); outputRecord.addError(ProcessError.REGEX_MATCHING_ERROR.getName(), errorMessage); - outputRecord.setField(FieldDictionary.RECORD_RAW_KEY, FieldType.STRING, value); + outputRecord.setField(FieldDictionary.RECORD_KEY, FieldType.STRING, value); } } /** @@ -276,7 +276,7 @@ public Collection process(ProcessContext context, Collection rec // if we don't have any matches output an error if (!hasMatched) { outputRecord.addError(ProcessError.REGEX_MATCHING_ERROR.getName(), "check your conf"); - outputRecord.setField(FieldDictionary.RECORD_RAW_VALUE, FieldType.STRING, value); + outputRecord.setField(FieldDictionary.RECORD_VALUE, FieldType.STRING, value); } } @@ -287,7 +287,7 @@ public Collection process(ProcessContext context, Collection rec " : " + e.getMessage(); logger.warn(errorMessage); outputRecord.addError(ProcessError.REGEX_MATCHING_ERROR.getName(), errorMessage); - outputRecord.setField(FieldDictionary.RECORD_RAW_VALUE, FieldType.STRING, value); + outputRecord.setField(FieldDictionary.RECORD_VALUE, FieldType.STRING, value); } finally { outputRecords.add(outputRecord); } @@ -331,7 +331,7 @@ private List getAlternativePatterns(ProcessContext co private void extractValueFields(String[] valueFields, boolean keepRawContent, StandardRecord outputRecord, Matcher valueMatcher, TimeZone timezone) { if (keepRawContent) { - outputRecord.setField(FieldDictionary.RECORD_RAW_VALUE, FieldType.STRING, valueMatcher.group(0)); + outputRecord.setField(FieldDictionary.RECORD_VALUE, FieldType.STRING, valueMatcher.group(0)); } for (int i = 0; i < Math.min(valueMatcher.groupCount() + 1, valueFields.length); i++) { String content = valueMatcher.group(i + 1); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java new file mode 100644 index 000000000..a1775b606 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.annotation.behavior.DynamicProperty; +import com.hurence.logisland.annotation.documentation.CapabilityDescription; +import com.hurence.logisland.annotation.documentation.Tags; +import com.hurence.logisland.component.AllowableValue; +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.processor.AbstractProcessor; +import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.FieldType; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.service.datastore.DatastoreClientService; +import com.hurence.logisland.validator.StandardValidators; +import delight.nashornsandbox.NashornSandbox; +import delight.nashornsandbox.NashornSandboxes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.ScriptException; +import java.util.*; +import java.util.concurrent.Executors; + +@Tags({"record", "fields", "Add"}) +@CapabilityDescription("Add one or more field with a default value\n" + + "...") +@DynamicProperty(name = "field to add", + supportsExpressionLanguage = false, + value = "a default value", + description = "Add a field to the record with the default value") +public class ComputeTag extends AbstractProcessor { + + + private static final Logger logger = LoggerFactory.getLogger(ComputeTag.class); + + + private static final AllowableValue OVERWRITE_EXISTING = + new AllowableValue("overwrite_existing", "overwrite existing field", "if field already exist"); + + private static final AllowableValue KEEP_OLD_FIELD = + new AllowableValue("keep_only_old_field", "keep only old field value", "keep only old field"); + + + public static final PropertyDescriptor MAX_CPU_TIME = new PropertyDescriptor.Builder() + .name("max.cpu.time") + .description("maximum CPU time in milliseconds allowed for script execution.") + .required(false) + .defaultValue("100") + .addValidator(StandardValidators.LONG_VALIDATOR) + .build(); + + public static final PropertyDescriptor MAX_MEMORY = new PropertyDescriptor.Builder() + .name("max.memory") + .description("maximum memory in Bytes which JS executor thread can allocate") + .required(false) + .defaultValue("51200") + .addValidator(StandardValidators.LONG_VALIDATOR) + .build(); + + public static final PropertyDescriptor ALLOw_NO_BRACE = new PropertyDescriptor.Builder() + .name("allow.no.brace") + .description("Force, to check if all blocks are enclosed with curly braces \"{}\".\n" + + "

\n" + + " Explanation: all loops (for, do-while, while, and if-else, and functions\n" + + " should use braces, because poison_pill() function will be inserted after\n" + + " each open brace \"{\", to ensure interruption checking. Otherwise simple\n" + + " code like:\n" + + "

\n" +
+                    "    while(true) while(true) {\n" +
+                    "      // do nothing\n" +
+                    "    }\n" +
+                    "  
\n" + + " or even:\n" + + "
\n" +
+                    "    while(true)\n" +
+                    "  
\n" + + " cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n" + + " which make JVM unstable.\n" + + "

\n" + + "

\n" + + " Properly writen code (even in bad intention) like:\n" + + "

\n" +
+                    "    while(true) { while(true) {\n" +
+                    "      // do nothing\n" +
+                    "    }}\n" +
+                    "  
\n" + + " will be changed into:\n" + + "
\n" +
+                    "    while(true) {poison_pill(); \n" +
+                    "      while(true) {poison_pill();\n" +
+                    "        // do nothing\n" +
+                    "      }\n" +
+                    "    }\n" +
+                    "  
\n" + + " which finish nicely when interrupted.\n" + + "

\n" + + " For legacy code, this check can be turned off, but with no guarantee, the\n" + + " JS thread will gracefully finish when interrupted.\n" + + "

") + .required(false) + .defaultValue("false") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .build(); + + public static final PropertyDescriptor MAX_PREPARED_STATEMENTS = new PropertyDescriptor.Builder() + .name("max.prepared.statements") + .description("The size of prepared statements LRU cache. Default 0 (disabled).\n" + + "

\n" + + " Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n" + + " quit itself when time exceeded. To execute only once this procedure per\n" + + " statement set this value.\n" + + "

\n" + + "

\n" + + " When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n" + + "

") + .required(false) + .defaultValue("30") + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .build(); + + public static final PropertyDescriptor DATASTORE_CLIENT_SERVICE = new PropertyDescriptor.Builder() + .name("datastore.client.service") + .description("The instance of the Controller Service to use for accessing datastore.") + .required(true) + .identifiesControllerService(DatastoreClientService.class) + .build(); + + + protected DatastoreClientService datastoreClientService; + protected NashornSandbox sandbox; + + @Override + public List getSupportedPropertyDescriptors() { + List properties = new ArrayList<>(); + properties.add(MAX_CPU_TIME); + properties.add(MAX_MEMORY); + properties.add(ALLOw_NO_BRACE); + properties.add(MAX_PREPARED_STATEMENTS); + properties.add(DATASTORE_CLIENT_SERVICE); + + return properties; + } + + + @Override + protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) { + return new PropertyDescriptor.Builder() + .name(propertyDescriptorName) + .expressionLanguageSupported(false) + .addValidator(StandardValidators.COMMA_SEPARATED_LIST_VALIDATOR) + .required(false) + .dynamic(true) + .build(); + } + + + @Override + public boolean hasControllerService() { + return true; + } + + @Override + public void init(ProcessContext context) { + super.init(context); + sandbox = NashornSandboxes.create(); + + Long maxCpuTime = context.getPropertyValue(MAX_CPU_TIME).asLong(); + Long maxMemory = context.getPropertyValue(MAX_MEMORY).asLong(); + Boolean allowNoBrace = context.getPropertyValue(ALLOw_NO_BRACE).asBoolean(); + Integer maxPreparedStatements = context.getPropertyValue(MAX_PREPARED_STATEMENTS).asInteger(); + + + sandbox.setMaxCPUTime(maxCpuTime); + sandbox.setMaxMemory(maxMemory); + sandbox.allowNoBraces(allowNoBrace); + sandbox.setMaxPreparedStatements(maxPreparedStatements); // because preparing scripts for execution is expensive + sandbox.setExecutor(Executors.newSingleThreadExecutor()); + + datastoreClientService = context.getPropertyValue(DATASTORE_CLIENT_SERVICE).asControllerService(DatastoreClientService.class); + if (datastoreClientService == null) { + logger.error("Datastore client service is not initialized!"); + } + + sandbox.inject("cache", datastoreClientService); + sandbox.allow(DatastoreClientService.class); + sandbox.allow(Record.class); + sandbox.allow(StandardRecord.class); + sandbox.allow(FieldType.class); + sandbox.allow(FieldDictionary.class); + + + // loop over js expression to evaluate/* Build the JsonPath expressions from attributes */ + final Map dynamicTagValuesMap = new HashMap<>(); + + for (final Map.Entry entry : context.getProperties().entrySet()) { + if (!entry.getKey().isDynamic()) { + continue; + } + + /** + * cvib1: return cache("vib1").value * 10.2 * ( 1.0 - 1.0 / cache("vib2").value ); + * + * + * will be translated into + + function cvib1( cache ) { return cache("vib1").value * 10.2 * ( 1.0 - 1.0 / cache("vib2").value ); } + + var record_cvb1 = new com.hurence.logisland.record.StandardRecord("cvb1"); + record_cvb1.setField( + com.hurence.logisland.record.FieldDictionary.RECORD_VALUE, + com.hurence.logisland.record.FieldType.DOUBLE, + cvib1( cache ) + ); + + */ + String key = entry.getKey().getName(); + String value = entry.getValue() + .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") + .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); + // dynamicTagValuesMap.put(entry.getKey().getName(), entry.getValue()); + StringBuilder sb = new StringBuilder(); + + sb.append("function ") + .append(key) + .append("( ) { ") + .append(value) + + .append(" } \n"); + sb.append("var record_") + .append(key) + .append(" = new com.hurence.logisland.record.StandardRecord()") + .append(".setId(\"") + .append(key) + .append("\");\n"); + sb.append("record_") + .append(key) + .append(".setField( ") + .append("\"record_value\",") + .append(" com.hurence.logisland.record.FieldType.DOUBLE,") + .append(key) + .append("());\n"); + + try { + System.out.println(sb.toString()); + sandbox.eval(sb.toString()); + /* sandbox.eval( "function oula() { var recordToFetch = new com.hurence.logisland.record.StandardRecord();" + + "var test= cache.get(\"test\",recordToFetch.setId(\"cached_id1\")).getField(\"record_value\").asString(); " + + "return test;}" + + "oula();"); + + System.out.println(value); + sandbox.eval( "function oula() { " + value +"}" + + "oula();");*/ + } catch (ScriptException e) { + e.printStackTrace(); + } + + logger.debug(sb.toString()); + } + + } + + @Override + public Collection process(ProcessContext context, Collection records) { + + // check if we need initialization + if (datastoreClientService == null) { + init(context); + } + + List outputRecords = new ArrayList<>(); + for (Record record : records) { + + Record cached = (Record) sandbox.get("record_cvib1"); + /* Record comptedRecord = new StandardRecord("computed_record") + .setStringField("test_field", stored.toString());*/ + outputRecords.add(cached); + } + + return outputRecords; + } +} \ No newline at end of file diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java index 2b3ad8db2..da8866ba1 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java @@ -291,7 +291,7 @@ public void testEncoding() throws NoSuchAlgorithmException { * ERRORS */ String rawValue = "a,b,c,12.5"; - Record record1 = getRecord1().setStringField(FieldDictionary.RECORD_RAW_VALUE,rawValue); + Record record1 = getRecord1().setStringField(FieldDictionary.RECORD_VALUE,rawValue); testRunner.enqueue(record1); testRunner.run(); testRunner.assertAllInputRecordsProcessed(); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java index d7451fa49..8127bcdcd 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java @@ -160,7 +160,7 @@ public void testApacheLogWithBadRegex() { MockRecord out = testRunner.getOutputRecords().get(0); out.assertFieldNotExists("src_ip"); - out.assertFieldEquals(FieldDictionary.RECORD_RAW_VALUE, "10.3.10.134 - - [24/Jul/2016:08:45:28 +0200] \"GET /usr/rest/account/email HTTP/1.1\" 200 51"); + out.assertFieldEquals(FieldDictionary.RECORD_VALUE, "10.3.10.134 - - [24/Jul/2016:08:45:28 +0200] \"GET /usr/rest/account/email HTTP/1.1\" 200 51"); //out.assertFieldEquals(FieldDictionary.RECORD_ERRORS, ProcessError.REGEX_MATCHING_ERROR.toString()); out.assertRecordSizeEquals(2); } @@ -198,7 +198,7 @@ public void testAlternativeSingleMatch() { out.assertFieldEquals("http_version", "HTTP/1.1"); out.assertFieldEquals("identd", "-"); out.assertFieldEquals("user", "-"); - out.assertFieldEquals(FieldDictionary.RECORD_RAW_VALUE, "10.3.10.134 - - [24/Jul/2016:08:45:29 +0200] \"GET /usr/rest/bank/purses?activeOnly=true HTTP/1.1\" 200 239"); + out.assertFieldEquals(FieldDictionary.RECORD_VALUE, "10.3.10.134 - - [24/Jul/2016:08:45:29 +0200] \"GET /usr/rest/bank/purses?activeOnly=true HTTP/1.1\" 200 239"); //out.assertFieldEquals(FieldDictionary.RECORD_ERRORS, ProcessError.REGEX_MATCHING_ERROR.toString()); out.assertRecordSizeEquals(9); @@ -208,7 +208,7 @@ public void testAlternativeSingleMatch() { out.assertFieldEquals("src_ip", "10.3.10.134"); out.assertFieldEquals("http_status", 200); out.assertFieldEquals("bytes_out", 52); - out.assertFieldEquals(FieldDictionary.RECORD_RAW_VALUE, "10.3.10.134 200 52"); + out.assertFieldEquals(FieldDictionary.RECORD_VALUE, "10.3.10.134 200 52"); //out.assertFieldEquals(FieldDictionary.RECORD_ERRORS, ProcessError.REGEX_MATCHING_ERROR.toString()); out.assertRecordSizeEquals(4); } @@ -247,7 +247,7 @@ public void testAlternativeMultiMatch() { out.assertFieldEquals("http_version", "HTTP/1.1"); out.assertFieldEquals("identd", "-"); out.assertFieldEquals("user", "-"); - out.assertFieldEquals(FieldDictionary.RECORD_RAW_VALUE, "10.3.10.134 - - [24/Jul/2016:08:45:29 +0200] \"GET /usr/rest/bank/purses?activeOnly=true HTTP/1.1\" 200 239"); + out.assertFieldEquals(FieldDictionary.RECORD_VALUE, "10.3.10.134 - - [24/Jul/2016:08:45:29 +0200] \"GET /usr/rest/bank/purses?activeOnly=true HTTP/1.1\" 200 239"); //out.assertFieldEquals(FieldDictionary.RECORD_ERRORS, ProcessError.REGEX_MATCHING_ERROR.toString()); out.assertRecordSizeEquals(9); @@ -257,7 +257,7 @@ public void testAlternativeMultiMatch() { out.assertFieldEquals(FieldDictionary.RECORD_TIME, 1469342729000L); out.assertFieldNotExists("http_status"); out.assertFieldEquals("bytes_out", 52); - out.assertFieldEquals(FieldDictionary.RECORD_RAW_VALUE, "[24/Jul/2016:08:45:29 +0200] 52"); + out.assertFieldEquals(FieldDictionary.RECORD_VALUE, "[24/Jul/2016:08:45:29 +0200] 52"); //out.assertFieldEquals(FieldDictionary.RECORD_ERRORS, ProcessError.REGEX_MATCHING_ERROR.toString()); out.assertRecordSizeEquals(2); @@ -267,7 +267,7 @@ public void testAlternativeMultiMatch() { out.assertFieldEquals("src_ip", "10.3.10.134"); out.assertFieldEquals("http_status", 200); out.assertFieldEquals("bytes_out", 52); - out.assertFieldEquals(FieldDictionary.RECORD_RAW_VALUE, "10.3.10.134 200 52"); + out.assertFieldEquals(FieldDictionary.RECORD_VALUE, "10.3.10.134 200 52"); //out.assertFieldEquals(FieldDictionary.RECORD_ERRORS, ProcessError.REGEX_MATCHING_ERROR.toString()); out.assertRecordSizeEquals(4); } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java new file mode 100644 index 000000000..97f607703 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.component.InitializationException; +import com.hurence.logisland.processor.datastore.MockDatastoreService; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.FieldType; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.service.datastore.DatastoreClientService; +import com.hurence.logisland.util.runner.TestRunner; +import com.hurence.logisland.util.runner.TestRunners; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class ComputeTagsTest { + + @Test + public void testSimpleEnrichment() throws InitializationException { + + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getLookupRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(ComputeTag.class); + runner.setProperty(ComputeTag.MAX_CPU_TIME, "100"); + runner.setProperty(ComputeTag.MAX_MEMORY, "12800000"); + runner.setProperty(ComputeTag.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(ComputeTag.ALLOw_NO_BRACE, "false"); + runner.setProperty("cvib1", "return cache(\"cached_id1\").value * 10.2;"); + runner.setProperty(ComputeTag.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + + final DatastoreClientService lookupService = runner.getProcessContext() + .getPropertyValue(ComputeTag.DATASTORE_CLIENT_SERVICE) + .asControllerService(MockDatastoreService.class); + + + Collection recordsToEnrich = getRecords(); + + runner.assertValid(); + runner.enqueue(recordsToEnrich); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(3); + runner.assertOutputErrorCount(0); + + + Record enriched0 = runner.getOutputRecords().get(0); + + assertEquals(enriched0.getId(), "cvib1"); + assertEquals((double) enriched0.getField(FieldDictionary.RECORD_VALUE).asDouble(), 10.2 * 12.45, 0.0001); + + } + + private Collection getRecords() { + Collection recordsToEnrich = new ArrayList<>(); + + recordsToEnrich.add(new StandardRecord() + .setId("id1") + .setField("a", FieldType.STRING, "a1") + .setField("b", FieldType.STRING, "b1") + .setField("c", FieldType.LONG, 1)); + + recordsToEnrich.add(new StandardRecord() + .setId("id2") + .setField("a", FieldType.STRING, "a2") + .setField("b", FieldType.STRING, "b2") + .setField("c", FieldType.LONG, 2)); + + recordsToEnrich.add(new StandardRecord() + .setId("id3") + .setField("a", FieldType.STRING, "a3") + .setField("b", FieldType.STRING, "b3") + .setField("c", FieldType.LONG, 3)); + return recordsToEnrich; + } + + private Map getJoinedRecords() { + Map lookupRecords = new HashMap<>(); + + lookupRecords.put("id1", new StandardRecord() + .setId("id1") + .setField("f1", FieldType.STRING, "value1") + .setField("f2", FieldType.STRING, "falue2") + .setField("a", FieldType.STRING, "a1") + .setField("b", FieldType.STRING, "b1") + .setField("c", FieldType.LONG, 1)); + + lookupRecords.put("id2", new StandardRecord() + .setId("id2") + .setField("f1", FieldType.STRING, "value3") + .setField("f2", FieldType.STRING, "falue4") + .setField("a", FieldType.STRING, "a2") + .setField("b", FieldType.STRING, "b2") + .setField("c", FieldType.LONG, 2)); + + lookupRecords.put("id3", new StandardRecord() + .setId("id3") + .setField("f1", FieldType.STRING, "value5") + .setField("f2", FieldType.STRING, "falue6") + .setField("a", FieldType.STRING, "a3") + .setField("b", FieldType.STRING, "b3") + .setField("c", FieldType.LONG, 3)); + + return lookupRecords; + } + + private Collection getLookupRecords() { + Collection lookupRecords = new ArrayList<>(); + + lookupRecords.add(new StandardRecord() + .setId("cached_id1") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 12.45)); + + lookupRecords.add(new StandardRecord() + .setId("cached_id2") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 2.5)); + + return lookupRecords; + } +} diff --git a/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/hurence/logisland/processor/DetectOutliers.java b/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/hurence/logisland/processor/DetectOutliers.java index 07d23f192..53c75f1c5 100644 --- a/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/hurence/logisland/processor/DetectOutliers.java +++ b/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/hurence/logisland/processor/DetectOutliers.java @@ -618,7 +618,7 @@ public Collection process(final ProcessContext context, final Collection } catch (RuntimeException e) { list.add(new StandardRecord(OUTLIER_PROCESSING_EXCEPTION_TYPE) .setStringField(FieldDictionary.RECORD_ERRORS, ProcessError.RUNTIME_ERROR.toString()) - .setStringField(FieldDictionary.RECORD_RAW_VALUE, e.getMessage()) + .setStringField(FieldDictionary.RECORD_VALUE, e.getMessage()) .setStringField(FieldDictionary.PROCESSOR_NAME, DetectOutliers.class.getName()) ); } From 2cba3bbbb042001a85c0b0bf64b48d7a5f4ffd5a Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 29 May 2018 11:47:58 +0200 Subject: [PATCH 10/63] Adding remote component handling and testing --- .../logisland/engine/EngineContext.java | 16 +-- .../engine/StandardEngineContext.java | 12 +- .../logisland/processor/ProcessContext.java | 4 +- .../processor/StandardProcessContext.java | 7 ++ .../stream/StandardStreamContext.java | 18 ++- .../logisland/stream/StreamContext.java | 3 +- .../remote/RemoteApiComponentFactory.java | 32 ++++- .../spark/remote/RemoteComponentRegistry.java | 115 +++++++++++++++--- .../remote/RemoteComponentRegistryTest.java | 97 +++++++++++++++ .../spark/remote/mock/MockProcessor.java | 39 ++++++ .../remote/mock/MockServiceController.java | 31 +++++ .../engine/spark/remote/mock/MockStream.java | 31 +++++ .../util/runner/MockProcessContext.java | 6 +- 13 files changed, 360 insertions(+), 51 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java index a27be5961..08d6f4063 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java @@ -20,9 +20,10 @@ import com.hurence.logisland.config.ControllerServiceConfiguration; import com.hurence.logisland.stream.StreamContext; +import java.io.Closeable; import java.util.Collection; -public interface EngineContext extends ComponentContext { +public interface EngineContext extends ComponentContext , Closeable { /** * @return retrieve the list of stream contexts @@ -36,12 +37,6 @@ public interface EngineContext extends ComponentContext { */ void addStreamContext(StreamContext streamContext); - /** - * Removes a stream to the collection of Streams - * - * @param streamContext the Stream to add - */ - void removeStreamContext(StreamContext streamContext); /** @@ -62,10 +57,5 @@ public interface EngineContext extends ComponentContext { */ void addControllerServiceConfiguration(ControllerServiceConfiguration config); - /** - * Removes a {@link ControllerServiceConfiguration} - * - * @param config to remove - */ - void removeControllerServiceConfiguration(ControllerServiceConfiguration config); + } diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java index f515c7ab1..450a511ee 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java @@ -23,6 +23,7 @@ import com.hurence.logisland.config.ControllerServiceConfiguration; import com.hurence.logisland.stream.StreamContext; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -48,11 +49,6 @@ public void addStreamContext(StreamContext streamContext) { streamContexts.add(streamContext); } - @Override - public void removeStreamContext(StreamContext streamContext) { - streamContexts.remove(streamContext); - } - @Override public ProcessingEngine getEngine() { return (ProcessingEngine) component; @@ -98,7 +94,9 @@ public void addControllerServiceConfiguration(ControllerServiceConfiguration con } @Override - public void removeControllerServiceConfiguration(ControllerServiceConfiguration config) { - controllerServiceConfigurations.remove(config); + public void close() throws IOException { + while (!streamContexts.isEmpty()) { + streamContexts.remove(0).close(); + } } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java b/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java index b6ba1a58e..7f692e9b0 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java @@ -20,7 +20,9 @@ import com.hurence.logisland.component.InitializationException; import com.hurence.logisland.controller.ControllerServiceLookup; -public interface ProcessContext extends ComponentContext { +import java.io.Closeable; + +public interface ProcessContext extends ComponentContext, Closeable { /** * Adds the given {@link ControllerServiceLookup} so that the diff --git a/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java b/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java index 390336b96..aab391c41 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java @@ -19,6 +19,8 @@ import com.hurence.logisland.component.*; import com.hurence.logisland.controller.ControllerServiceLookup; +import java.io.IOException; + public class StandardProcessContext extends AbstractConfiguredComponent implements ProcessContext { private ControllerServiceLookup controllerServiceLookup; @@ -64,4 +66,9 @@ public PropertyValue newPropertyValue(final String rawValue) { public void verifyModifiable() throws IllegalStateException { } + + @Override + public void close() throws IOException { + controllerServiceLookup = null; + } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java b/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java index fa801a2d2..8c6c91a5d 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -19,8 +19,8 @@ import com.hurence.logisland.component.*; import com.hurence.logisland.controller.ControllerServiceLookup; import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.processor.Processor; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -88,4 +88,12 @@ public PropertyValue newPropertyValue(final String rawValue) { public void verifyModifiable() throws IllegalStateException { } + + @Override + public void close() throws IOException { + while (!processContexts.isEmpty()) { + processContexts.remove(0).close(); + } + controllerServiceLookup = null; + } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java b/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java index f1e11ba3a..a68dfd057 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java @@ -20,9 +20,10 @@ import com.hurence.logisland.controller.ControllerServiceLookup; import com.hurence.logisland.processor.ProcessContext; +import java.io.Closeable; import java.util.Collection; -public interface StreamContext extends ComponentContext { +public interface StreamContext extends ComponentContext, Closeable { /** * @return the Stream diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java index 673aae760..c6a755e3e 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -18,10 +18,9 @@ package com.hurence.logisland.engine.spark.remote; import com.hurence.logisland.config.ControllerServiceConfiguration; -import com.hurence.logisland.engine.spark.remote.model.Processor; -import com.hurence.logisland.engine.spark.remote.model.Property; -import com.hurence.logisland.engine.spark.remote.model.Service; -import com.hurence.logisland.engine.spark.remote.model.Stream; +import com.hurence.logisland.engine.EngineContext; +import com.hurence.logisland.engine.StandardEngineContext; +import com.hurence.logisland.engine.spark.remote.model.*; import com.hurence.logisland.processor.ProcessContext; import com.hurence.logisland.processor.StandardProcessContext; import com.hurence.logisland.stream.RecordStream; @@ -33,11 +32,36 @@ import java.util.Optional; import java.util.stream.Collectors; +/** + * ] + * Component factory resolving logisland components from remote api model. + * + * @author amarziali + */ public class RemoteApiComponentFactory { private static final Logger logger = LoggerFactory.getLogger(RemoteApiComponentFactory.class); + /** + * Create a child isolated engine context for a pipeline sharing the engine processor but having separated streams, + * processor and services. + * + * @param engineContext the master engine context + * @param pipeline the pipeline. + * @return a child {@link EngineContext} + */ + public EngineContext createScopedEngineContext(EngineContext engineContext, Pipeline pipeline) { + EngineContext ret = new StandardEngineContext(engineContext.getEngine(), pipeline.getName()); + ret.getProperties().forEach((k, v) -> { + if (v != null) { + ret.setProperty(k.getName(), v); + } + }); + return ret; + } + + /** * Instantiates a stream from of configuration * diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java index a34ed32ee..155507887 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java @@ -19,50 +19,127 @@ import com.hurence.logisland.engine.EngineContext; import com.hurence.logisland.engine.spark.remote.model.Pipeline; -import com.hurence.logisland.stream.StreamContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.Collection; import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +/** + * Stateful component registry. + * + * @author amarziali + */ public class RemoteComponentRegistry { private static final Logger logger = LoggerFactory.getLogger(RemoteComponentRegistry.class); private final EngineContext engineContext; - private final Map> registry = new HashMap<>(); + private final Map childContextes = new HashMap<>(); + private final Map pipelineMap = new HashMap<>(); + private final RemoteApiComponentFactory remoteApiComponentFactory = new RemoteApiComponentFactory(); + /** + * Create an instance. + * + * @param engineContext the master engine (initial state). + */ public RemoteComponentRegistry(EngineContext engineContext) { this.engineContext = engineContext; } + private void stopPipeline(Pipeline pipeline) { + EngineContext ctx = childContextes.remove(pipeline.getName()); + pipelineMap.remove(pipeline.getName()); + logger.info("Stopping everything for pipeline {}", ctx.getName()); + ctx.getStreamContexts().forEach(streamContext -> { + logger.info("Pipeline {} : stopping stream {}", pipeline.getName(), streamContext.getName()); + try { + streamContext.getStream().stop(); + logger.info("Pipeline {} : successfully stopped stream {}", pipeline.getName(), streamContext.getName()); + } catch (Exception e) { + logger.error("Pipeline {} : unexpected error stopping stream {}: {}", pipeline.getName(), streamContext.getName(), e.getMessage()); + } + }); + try { + engineContext.close(); + } catch (IOException e) { + logger.warn("Unable to properly close engine " + engineContext.getName(), e); + } + logger.info("Pipeline {} stopped", ctx.getName()); + + } + + private void startPipeline(Pipeline pipeline) { + { + logger.info("Creating engine for pipeline {}", pipeline.getName()); + //create a new scoped engine context corresponding to the pipeline + EngineContext ec = remoteApiComponentFactory.createScopedEngineContext(engineContext, pipeline); + //add every service controller it needs + pipeline.getServices().stream() + .map(remoteApiComponentFactory::getControllerServiceConfiguration) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(ec::addControllerServiceConfiguration); + //now instantiate every stream + pipeline.getStreams().stream() + .map(remoteApiComponentFactory::getStreamContext) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(ec::addStreamContext); + //start it + try { + logger.info("Starting engine for pipeline {}", pipeline.getName()); + ec.getEngine().start(ec); + //now add the engine context to the registry + childContextes.put(pipeline.getName(), ec); + pipelineMap.put(pipeline.getName(), pipeline); + logger.info("Pipeline {} successfully started", pipeline.getName()); + } catch (Exception e) { + logger.error("Unable to properly start pipeline " + pipeline.getName(), e); + try { + logger.info("Shutting down pipeline {}", pipeline.getName()); + ec.getEngine().shutdown(ec); + logger.info("Pipeline {} successfully shut down", pipeline.getName()); + } catch (Exception e1) { + logger.warn("Unable to properly shut down pipeline " + pipeline.getName(), e1); + } + } + } + } + + + private Optional findInPipelines(Collection list, Pipeline item) { + return list.stream().filter(p -> p.getName().equals(item.getName())).findFirst(); + } + + + /** + * Updates the state of the engine by adding / removing pipelines according to the new provided config. + * + * @param pipelines the list of pipelines (new state) + */ public void updateEngineContext(Collection pipelines) { - //remove missing items - registry.keySet().stream() - .filter(pipeline -> !pipelines.contains(pipeline)) + //remove missing or outdated items + pipelineMap.values().stream() + .filter(pipeline -> { + Optional found = findInPipelines(pipelines, pipeline); + return !found.isPresent() || pipeline.getLastModified().isBefore(found.get().getLastModified()); + }) .collect(Collectors.toList()) //remove active streams inside this pipeline - .forEach(pipeline -> registry.remove(pipeline).forEach(engineContext -> { - logger.info("Pipeline {} : stopping engine {}", pipeline.getName(), engineContext.getName()); - try { - engineContext.getEngine().shutdown(engineContext); - logger.info("Pipeline {} : successfully stopped engine {}", pipeline.getName(), engineContext.getName()); - } catch (Exception e) { - logger.error("Pipeline {} : unexpected error stopping engine {}: {}", pipeline.getName(), engineContext.getName(), e.getMessage()); - } - })); + .forEach(this::stopPipeline); + //add new items pipelines.stream() - .filter(pipeline -> !registry.containsKey(pipeline)) + .filter(pipeline -> !pipelineMap.containsKey(pipeline.getName())) .collect(Collectors.toList()) - .forEach(pipeline -> { - - }); + .forEach(this::startPipeline); } } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java new file mode 100644 index 000000000..d8b025b05 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java @@ -0,0 +1,97 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote; + +import com.hurence.logisland.engine.EngineContext; +import com.hurence.logisland.engine.MockProcessingEngine; +import com.hurence.logisland.engine.StandardEngineContext; +import com.hurence.logisland.engine.spark.remote.mock.MockProcessor; +import com.hurence.logisland.engine.spark.remote.mock.MockServiceController; +import com.hurence.logisland.engine.spark.remote.mock.MockStream; +import com.hurence.logisland.engine.spark.remote.model.Pipeline; +import com.hurence.logisland.engine.spark.remote.model.Processor; +import com.hurence.logisland.engine.spark.remote.model.Service; +import com.hurence.logisland.engine.spark.remote.model.Stream; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.OffsetDateTime; +import java.util.Arrays; +import java.util.Collections; + +public class RemoteComponentRegistryTest { + + private static final Logger logger = LoggerFactory.getLogger(RemoteComponentRegistryTest.class); + + + @Test + public void updateEngineContext() { + MockProcessingEngine engine = new MockProcessingEngine(); + EngineContext masterContext = new StandardEngineContext(engine, "master"); + engine.start(masterContext); + RemoteComponentRegistry registry = new RemoteComponentRegistry(masterContext); + Pipeline pipeline1 = createPipeline("pipeline1"); + Pipeline pipeline2 = createPipeline("pipeline2"); + logger.info("should create two new pipelines"); + registry.updateEngineContext(Arrays.asList(pipeline1, pipeline2)); + logger.info("should do nothing"); + registry.updateEngineContext(Arrays.asList(pipeline1, pipeline2)); + logger.info("should remove a pipeline"); + registry.updateEngineContext(Arrays.asList(pipeline1)); + logger.info("should update the pipeline (since touch is fresher)"); + pipeline1 = createPipeline("pipeline1"); + registry.updateEngineContext(Arrays.asList(pipeline1)); + logger.info("should do nothing"); + registry.updateEngineContext(Arrays.asList(pipeline1)); + logger.info("should remove everything (Stop)"); + registry.updateEngineContext(Collections.emptyList()); + engine.shutdown(masterContext); + + + } + + private Service createService(String name) { + return (Service) new Service() + .name(name) + .component(MockServiceController.class.getCanonicalName()); + } + + private Processor createProcessor(String name) { + return (Processor) new Processor() + .name(name) + .component(MockProcessor.class.getCanonicalName()); + } + + private Stream createStream(String name) { + return (Stream) new Stream() + .addProcessorsItem(createProcessor("processor1")) + .addProcessorsItem(createProcessor("processor2")) + .name(name) + .component(MockStream.class.getCanonicalName()); + + } + + private Pipeline createPipeline(String name) { + return new Pipeline() + .addServicesItem(createService("service")) + .addStreamsItem(createStream("stream1")) + .lastModified(OffsetDateTime.now()) + .name(name); + } +} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java new file mode 100644 index 000000000..bb8d00e43 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java @@ -0,0 +1,39 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.mock; + +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.processor.AbstractProcessor; +import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.record.Record; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class MockProcessor extends AbstractProcessor { + @Override + public List getSupportedPropertyDescriptors() { + return new ArrayList<>(); + } + + @Override + public Collection process(ProcessContext context, Collection records) { + return records; + } +} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java new file mode 100644 index 000000000..c91387941 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java @@ -0,0 +1,31 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.mock; + +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.controller.AbstractControllerService; + +import java.util.ArrayList; +import java.util.List; + +public class MockServiceController extends AbstractControllerService { + @Override + public List getSupportedPropertyDescriptors() { + return new ArrayList<>(); + } +} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java new file mode 100644 index 000000000..e4f020c93 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java @@ -0,0 +1,31 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.mock; + +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.stream.AbstractRecordStream; + +import java.util.ArrayList; +import java.util.List; + +public class MockStream extends AbstractRecordStream { + @Override + public List getSupportedPropertyDescriptors() { + return new ArrayList<>(); + } +} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java index b10edd99b..e4cf69cbc 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java @@ -24,6 +24,7 @@ import com.hurence.logisland.registry.VariableRegistry; import com.hurence.logisland.validator.ValidationResult; +import java.io.IOException; import java.util.*; import static java.util.Objects.requireNonNull; @@ -243,5 +244,8 @@ public void addControllerService(final String serviceIdentifier, final Controlle config.setAnnotationData(annotationData); } - + @Override + public void close() throws IOException { + //do nothing + } } From 164082ff8dac00f99c8a385bb10452b4e1f9636b Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 29 May 2018 14:39:03 +0200 Subject: [PATCH 11/63] Damn slf4j..... --- .../logisland-connect-spark/pom.xml | 2 + .../logisland-spark_2_1-engine/pom.xml | 23 ++++++++++ .../spark/BaseStreamProcessingEngine.scala | 3 +- .../RemoteApiStreamProcessingEngine.scala | 44 +++++++++++++++++++ ...stractStreamProcessingIntegrationTest.java | 44 ++++++++----------- pom.xml | 8 +--- 6 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala diff --git a/logisland-connect/logisland-connect-spark/pom.xml b/logisland-connect/logisland-connect-spark/pom.xml index fec941772..7c5cb7456 100644 --- a/logisland-connect/logisland-connect-spark/pom.xml +++ b/logisland-connect/logisland-connect-spark/pom.xml @@ -55,6 +55,8 @@ + + junit diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index 93552a5bd..22f1c9187 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -39,17 +39,40 @@ http://www.w3.org/2001/XMLSchema-instance "> + + org.slf4j + slf4j-simple + provided + com.hurence.logisland logisland-api + + + org.slf4j + slf4j-simple + + com.hurence.logisland logisland-utils + + + org.slf4j + slf4j-simple + + com.hurence.logisland logisland-common-processors-plugin + + + org.slf4j + slf4j-simple + + com.hurence.logisland diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala index 884abbe56..a31b5ba28 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala @@ -429,7 +429,8 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { } protected final def getCurrentSparkStreamingContext(): StreamingContext = { - val batchDuration = conf.get(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getName).toInt + val batchDuration = conf.get(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getName, + BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getDefaultValue).toInt StreamingContext.getActiveOrCreate(() => new StreamingContext(getCurrentSparkContext(), Milliseconds(batchDuration))) } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala new file mode 100644 index 000000000..499b3295d --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -0,0 +1,44 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark + +import com.hurence.logisland.engine.{EngineContext, ProcessingEngine} +import org.apache.spark.SparkConf +import org.apache.spark.streaming.StreamingContext + +class RemoteApiStreamProcessingEngine(processingEngine: ProcessingEngine) extends BaseStreamProcessingEngine { + + + + + /** + * Hook to customize spark configuration before creating a spark context. + * + * @param sparkConf the preinitialized configuration. + * @param engineContext the engine context. + */ + override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = ??? + + /** + * Override to setup streaming context before starting them. + * + * @param engineContext the engine context. + * @param scc the spark streaming context. + */ + override protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit = ??? +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java index 7cb9ff317..df3c2f28b 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -18,7 +18,6 @@ import com.hurence.logisland.record.Record; import com.hurence.logisland.serializer.KryoSerializer; import com.hurence.logisland.stream.StreamProperties; -import com.hurence.logisland.stream.spark.AbstractKafkaRecordStream; import com.hurence.logisland.util.spark.SparkUtils; import kafka.admin.AdminUtils; import kafka.admin.RackAwareMode; @@ -34,9 +33,8 @@ import org.apache.kafka.clients.producer.ProducerRecord; import org.junit.After; import org.junit.Before; +import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -47,6 +45,7 @@ import java.util.*; import static org.junit.Assert.assertTrue; + /** * Abstract class for integration testing */ @@ -61,7 +60,7 @@ public abstract class AbstractStreamProcessingIntegrationTest { protected static final String MAGIC_STRING = "the world is so big"; - private static Logger logger = (Logger)LoggerFactory.getLogger(AbstractStreamProcessingIntegrationTest.class); + private static Logger logger = LoggerFactory.getLogger(AbstractStreamProcessingIntegrationTest.class); private static KafkaProducer producer; private static KafkaConsumer consumer; @@ -94,8 +93,6 @@ public static int[] choosePorts(int count) { @Before public void setUp() throws InterruptedException, IOException { - Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - root.setLevel(Level.WARN); SparkUtils.customizeLogLevels(); // setup Zookeeper @@ -115,24 +112,21 @@ public void setUp() throws InterruptedException, IOException { kafkaServer = TestUtils.createServer(config, mock); // create topics - if(!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_ERRORS_TOPIC().getValue())) + if (!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_ERRORS_TOPIC().getValue())) AdminUtils.createTopic(zkUtils, StreamProperties.DEFAULT_ERRORS_TOPIC().getValue(), - 1, - 1, - new Properties(), - RackAwareMode.Disabled$.MODULE$); - if(!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RECORDS_TOPIC().getValue())) + 1, + 1, + new Properties(), + RackAwareMode.Disabled$.MODULE$); + if (!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RECORDS_TOPIC().getValue())) AdminUtils.createTopic(zkUtils, StreamProperties.DEFAULT_RECORDS_TOPIC().getValue(), 1, 1, new Properties(), RackAwareMode.Disabled$.MODULE$); - if(!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RAW_TOPIC().getValue())) + if (!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RAW_TOPIC().getValue())) AdminUtils.createTopic(zkUtils, StreamProperties.DEFAULT_RAW_TOPIC().getValue(), 1, 1, new Properties(), RackAwareMode.Disabled$.MODULE$); - if(!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_METRICS_TOPIC().getValue())) + if (!AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_METRICS_TOPIC().getValue())) AdminUtils.createTopic(zkUtils, StreamProperties.DEFAULT_METRICS_TOPIC().getValue(), 1, 1, new Properties(), RackAwareMode.Disabled$.MODULE$); - - - // deleting zookeeper information to make sure the consumer starts from the beginning zkClient.delete("/consumers/group0"); @@ -175,13 +169,13 @@ public void tearDown() throws NoSuchFieldException, IllegalAccessException, Inte } if (zkUtils != null) { - if(AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_ERRORS_TOPIC().getValue())) + if (AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_ERRORS_TOPIC().getValue())) AdminUtils.deleteTopic(zkUtils, StreamProperties.DEFAULT_ERRORS_TOPIC().getValue()); - if(AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RECORDS_TOPIC().getValue())) + if (AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RECORDS_TOPIC().getValue())) AdminUtils.deleteTopic(zkUtils, StreamProperties.DEFAULT_RECORDS_TOPIC().getValue()); - if(AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RAW_TOPIC().getValue())) + if (AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_RAW_TOPIC().getValue())) AdminUtils.deleteTopic(zkUtils, StreamProperties.DEFAULT_RAW_TOPIC().getValue()); - if(AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_METRICS_TOPIC().getValue())) + if (AdminUtils.topicExists(zkUtils, StreamProperties.DEFAULT_METRICS_TOPIC().getValue())) AdminUtils.deleteTopic(zkUtils, StreamProperties.DEFAULT_METRICS_TOPIC().getValue()); zkUtils.close(); } diff --git a/pom.xml b/pom.xml index 3835a2653..627a7394f 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ UTF-8 UTF-8 2014 - 1.7.12 + 1.7.16 ${project.version} 2.7.1 @@ -750,11 +750,7 @@ mockito-core test - - org.slf4j - slf4j-simple - test - + org.slf4j slf4j-api From b28de9b791ca2fccabe19804a11a9b872acc0b05 Mon Sep 17 00:00:00 2001 From: oalam Date: Tue, 29 May 2018 15:30:03 +0200 Subject: [PATCH 12/63] tests on computetags processor --- .../logisland/record/RecordDictionary.java | 3 + logisland-documentation/components.rst | 93 +++++++++++++++++- .../src/main/resources/components.json | 7 +- .../src/main/resources/docs/components.rst | 93 +++++++++++++++++- .../runner/StandardProcessorTestRunner.java | 5 +- .../processor/alerting/ComputeTag.java | 50 ++++------ .../logisland/processor/ModifyIdTest.java | 6 +- .../logisland/processor/SplitTextTest.java | 22 +++-- .../processor/alerting/ComputeTagsTest.java | 94 +++++++++++-------- .../networkpacket/ParseNetworkPacketTest.java | 6 +- .../TestMultiGetElasticsearch.java | 2 +- 11 files changed, 283 insertions(+), 98 deletions(-) diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java b/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java index 4c19b6103..9e28ebaa4 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java @@ -22,4 +22,7 @@ public class RecordDictionary { public static String METRIC = "metric"; public static String EVENT = "event"; public static String MESSAGE = "message"; + public static String ERROR = "error"; + public static String TAG = "tag"; + public static String COMPUTED_TAG = "computed_tag"; } diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index b8e778012..c74459dc6 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -147,6 +147,93 @@ In the list below, the names of required properties appear in **bold**. Any othe ---------- +.. _com.hurence.logisland.processor.alerting.ComputeTag: + +ComputeTag +---------- +Add one or more field with a default value +... + +Class +_____ +com.hurence.logisland.processor.alerting.ComputeTag + +Tags +____ +record, fields, Add + +Properties +__________ +In the list below, the names of required properties appear in **bold**. Any other properties (not in bold) are considered optional. The table also indicates any default values +. + +.. csv-table:: allowable-values + :header: "Name","Description","Allowable Values","Default Value","Sensitive","EL" + :widths: 20,60,30,20,10,10 + + "max.cpu.time", "maximum CPU time in milliseconds allowed for script execution.", "", "100", "", "" + "max.memory", "maximum memory in Bytes which JS executor thread can allocate", "", "51200", "", "" + "allow.no.brace", "Force, to check if all blocks are enclosed with curly braces "{}". +

+ Explanation: all loops (for, do-while, while, and if-else, and functions + should use braces, because poison_pill() function will be inserted after + each open brace "{", to ensure interruption checking. Otherwise simple + code like: +

+    while(true) while(true) {
+      // do nothing
+    }
+  
+ or even: +
+    while(true)
+  
+ cause unbreakable loop, which force this sandbox to use {@link Thread#stop()} + which make JVM unstable. +

+

+ Properly writen code (even in bad intention) like: +

+    while(true) { while(true) {
+      // do nothing
+    }}
+  
+ will be changed into: +
+    while(true) {poison_pill(); 
+      while(true) {poison_pill();
+        // do nothing
+      }
+    }
+  
+ which finish nicely when interrupted. +

+ For legacy code, this check can be turned off, but with no guarantee, the + JS thread will gracefully finish when interrupted. +

", "", "false", "", "" + "max.prepared.statements", "The size of prepared statements LRU cache. Default 0 (disabled). +

+ Each statements when {@link #setMaxCPUTime(long)} is set is prepared to + quit itself when time exceeded. To execute only once this procedure per + statement set this value. +

+

+ When {@link #setMaxCPUTime(long)} is set 0, this value is ignored. +

", "", "30", "", "" + "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + +Dynamic Properties +__________________ +Dynamic Properties allow the user to specify both the name and value of a property. + +.. csv-table:: dynamic-properties + :header: "Name","Value","Description","EL" + :widths: 20,20,40,10 + + "field to add", "a default value", "Add a field to the record with the default value", "" + +---------- + .. _com.hurence.logisland.processor.webAnalytics.ConsolidateSession: ConsolidateSession @@ -795,7 +882,7 @@ In the list below, the names of required properties appear in **bold**. Any othe :widths: 20,60,30,20,10,10 "**id.generation.strategy**", "the strategy to generate new Id", "generate a random uid (generate a randomUid using java library), generate a hash from fields (generate a hash from fields), generate a string from java pattern and fields (generate a string from java pattern and fields), generate a concatenation of type, time and a hash from fields (generate a concatenation of type, time and a hash from fields (as for generate_hash strategy))", "randomUuid", "", "" - "**fields.to.hash**", "the comma separated list of field names (e.g. : 'policyid,date_raw'", "", "record_raw_value", "", "" + "**fields.to.hash**", "the comma separated list of field names (e.g. : 'policyid,date_raw'", "", "record_value", "", "" "**hash.charset**", "the charset to use to hash id string (e.g. 'UTF-8')", "", "UTF-8", "", "" "**hash.algorithm**", "the algorithme to use to hash id string (e.g. 'SHA-256'", "SHA-384, SHA-224, SHA-256, MD2, SHA, SHA-512, MD5", "SHA-256", "", "" "java.formatter.string", "the format to use to build id string (e.g. '%4$2s %3$2s %2$2s %1$2s' (see java Formatter)", "", "null", "", "" @@ -1498,7 +1585,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "**value.regex**", "the regex to match for the message value", "", "null", "", "" "**value.fields**", "a comma separated list of fields corresponding to matching groups for the message value", "", "null", "", "" "key.regex", "the regex to match for the message key", "", ".*", "", "" - "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_raw_key", "", "" + "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_key", "", "" "record.type", "default type of record", "", "record", "", "" "keep.raw.content", "do we add the initial raw content ?", "", "true", "", "" "timezone.record.time", "what is the time zone of the string formatted date for 'record_time' field.", "", "UTC", "", "" @@ -1574,7 +1661,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "**value.regex**", "the regex to match for the message value", "", "null", "", "" "**value.fields**", "a comma separated list of fields corresponding to matching groups for the message value", "", "null", "", "" "key.regex", "the regex to match for the message key", "", ".*", "", "" - "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_raw_key", "", "" + "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_key", "", "" "record.type", "default type of record", "", "record", "", "" "keep.raw.content", "do we add the initial raw content ?", "", "true", "", "" "**properties.field**", "the field containing the properties to split and treat", "", "properties", "", "" diff --git a/logisland-framework/logisland-agent/src/main/resources/components.json b/logisland-framework/logisland-agent/src/main/resources/components.json index 2e45c727e..5e064df1d 100644 --- a/logisland-framework/logisland-agent/src/main/resources/components.json +++ b/logisland-framework/logisland-agent/src/main/resources/components.json @@ -3,6 +3,7 @@ {"name":"ApplyRegexp","description":"This processor is used to create a new set of fields from one field (using regexp).","component":"com.hurence.logisland.processor.ApplyRegexp","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"This processor is used to create a new set of fields from one field (using regexp).","isExpressionLanguageSupported":true}]}, {"name":"BulkAddElasticsearch","description":"Indexes the content of a Record in Elasticsearch using elasticsearch's bulk processor","component":"com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.index","isRequired":true,"description":"The name of the index to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"default.type","isRequired":true,"description":"The type of this document (used by Elasticsearch for indexing and searching)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.index","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":false,"description":"the name of the event field containing es doc type => will override type value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"BulkPut","description":"Indexes the content of a Record in a Datastore using bulk processor","component":"com.hurence.logisland.processor.datastore.BulkPut","type":"processor","tags":["datastore","record","put","bulk"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.collection","isRequired":true,"description":"The name of the collection/index/table to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.collection","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"date.format","isRequired":false,"description":"simple date format for date suffix. default : yyyy.MM.dd","defaultValue":"yyyy.MM.dd","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, +{"name":"ComputeTag","description":"Add one or more field with a default value\n...","component":"com.hurence.logisland.processor.alerting.ComputeTag","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, {"name":"ConsolidateSession","description":"The ConsolidateSession processor is the Logisland entry point to get and process events from the Web Analytics.As an example here is an incoming event from the Web Analytics:\n\n\"fields\": [{ \"name\": \"timestamp\", \"type\": \"long\" },{ \"name\": \"remoteHost\", \"type\": \"string\"},{ \"name\": \"record_type\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"record_id\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"location\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"hitType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventCategory\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventAction\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventLabel\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"localPath\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"q\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"n\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"referer\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"viewportPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"viewportPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"partyId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"sessionId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageViewId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_newSession\", \"type\": [\"null\", \"boolean\"],\"default\": null },{ \"name\": \"userAgentString\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"UserId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"B2Bunit\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pointOfService\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"companyID\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"GroupCode\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"userRoles\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_PunchOut\", \"type\": [\"null\", \"string\"], \"default\": null }]The ConsolidateSession processor groups the records by sessions and compute the duration between now and the last received event. If the distance from the last event is beyond a given threshold (by default 30mn), then the session is considered closed.The ConsolidateSession is building an aggregated session object for each active session.This aggregated object includes: - The actual session duration. - A boolean representing wether the session is considered active or closed. Note: it is possible to ressurect a session if for instance an event arrives after a session has been marked closed. - User related infos: userId, B2Bunit code, groupCode, userRoles, companyId - First visited page: URL - Last visited page: URL The properties to configure the processor are: - sessionid.field: Property name containing the session identifier (default: sessionId). - timestamp.field: Property name containing the timestamp of the event (default: timestamp). - session.timeout: Timeframe of inactivity (in seconds) after which a session is considered closed (default: 30mn). - visitedpage.field: Property name containing the page visited by the customer (default: location). - fields.to.return: List of fields to return in the aggregated object. (default: N/A)","component":"com.hurence.logisland.processor.webAnalytics.ConsolidateSession","type":"processor","tags":["analytics","web","session"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"session.timeout","isRequired":false,"description":"session timeout in sec","defaultValue":"1800","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionid.field","isRequired":false,"description":"the name of the field containing the session id => will override default value if set","defaultValue":"sessionId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timestamp.field","isRequired":false,"description":"the name of the field containing the timestamp => will override default value if set","defaultValue":"h2kTimestamp","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"visitedpage.field","isRequired":false,"description":"the name of the field containing the visited page => will override default value if set","defaultValue":"location","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"userid.field","isRequired":false,"description":"the name of the field containing the userId => will override default value if set","defaultValue":"userId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.return","isRequired":false,"description":"the list of fields to return","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the first visited page => will override default value if set","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the last visited page => will override default value if set","defaultValue":"lastVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"isSessionActive.out.field","isRequired":false,"description":"the name of the field stating whether the session is active or not => will override default value if set","defaultValue":"is_sessionActive","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionDuration.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"sessionDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"eventsCounter.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"eventsCounter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the first event => will override default value if set","defaultValue":"firstEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the last event => will override default value if set","defaultValue":"lastEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionInactivityDuration.out.field","isRequired":false,"description":"the name of the field containing the session inactivity duration => will override default value if set","defaultValue":"sessionInactivityDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"ConvertFieldsType","description":"Converts a field value into the given type. does nothing if conversion is not possible","component":"com.hurence.logisland.processor.ConvertFieldsType","type":"processor","tags":["type","fields","update","convert"],"dynamicProperties":[{"name":"field","value":"the new type","description":"convert field value into new type","isExpressionLanguageSupported":true}]}, {"name":"DebugStream","description":"This is a processor that logs incoming records","component":"com.hurence.logisland.processor.DebugStream","type":"processor","tags":["record","debug"],"properties":[{"name":"event.serializer","isRequired":true,"description":"the way to serialize event","Json serialization":"serialize events as json blocs","String serialization":"serialize events as toString() blocs","defaultValue":"json","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, @@ -19,7 +20,7 @@ {"name":"IpToGeo","description":"Looks up geolocation information for an IP address. The attribute that contains the IP address to lookup must be provided in the **ip.address.field** property. By default, the geo information are put in a hierarchical structure. That is, if the name of the IP field is 'X', then the the geo attributes added by enrichment are added under a father field named X_geo. \"_geo\" is the default hierarchical suffix that may be changed with the **geo.hierarchical.suffix** property. If one wants to put the geo fields at the same level as the IP field, then the **geo.hierarchical** property should be set to false and then the geo attributes are created at the same level as him with the naming pattern X_geo_. \"_geo_\" is the default flat suffix but this may be changed with the **geo.flat.suffix** property. The IpToGeo processor requires a reference to an Ip to Geo service. This must be defined in the **iptogeo.service** property. The added geo fields are dependant on the underlying Ip to Geo service. The **geo.fields** property must contain the list of geo fields that should be created if data is available for the IP to resolve. This property defaults to \"*\" which means to add every available fields. If one only wants a subset of the fields, one must define a comma separated list of fields as a value for the **geo.fields** property. The list of the available geo fields is in the description of the **geo.fields** property.","component":"com.hurence.logisland.processor.enrichment.IpToGeo","type":"processor","tags":["geo","enrich","ip"],"properties":[{"name":"ip.address.field","isRequired":true,"description":"The name of the field containing the ip address to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"iptogeo.service","isRequired":true,"description":"The reference to the IP to Geo service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.fields","isRequired":false,"description":"Comma separated list of geo information fields to add to the record. Defaults to '*', which means to include all available fields. If a list of fields is specified and the data is not available, the geo field is not created. The geo fields are dependant on the underlying defined Ip to Geo service. The currently only supported type of Ip to Geo service is the Maxmind Ip to Geo service. This means that the currently supported list of geo fields is the following:**continent**: the identified continent for this IP address. **continent_code**: the identified continent code for this IP address. **city**: the identified city for this IP address. **latitude**: the identified latitude for this IP address. **longitude**: the identified longitude for this IP address. **location**: the identified location for this IP address, defined as Geo-point expressed as a string with the format: 'latitude,longitude'. **accuracy_radius**: the approximate accuracy radius, in kilometers, around the latitude and longitude for the location. **time_zone**: the identified time zone for this IP address. **subdivision_N**: the identified subdivision for this IP address. N is a one-up number at the end of the attribute name, starting with 0. **subdivision_isocode_N**: the iso code matching the identified subdivision_N. **country**: the identified country for this IP address. **country_isocode**: the iso code for the identified country for this IP address. **postalcode**: the identified postal code for this IP address. **lookup_micros**: the number of microseconds that the geo lookup took. The Ip to Geo service must have the lookup_micros property enabled in order to have this field available.","defaultValue":"*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.hierarchical","isRequired":false,"description":"Should the additional geo information fields be added under a hierarchical father field or not.","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.hierarchical.suffix","isRequired":false,"description":"Suffix to use for the field holding geo information. If geo.hierarchical is true, then use this suffix appended to the IP field name to define the father field name. This may be used for instance to distinguish between geo fields with various locales using many Ip to Geo service instances.","defaultValue":"_geo","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"geo.flat.suffix","isRequired":false,"description":"Suffix to use for geo information fields when they are flat. If geo.hierarchical is false, then use this suffix appended to the IP field name but before the geo field name. This may be used for instance to distinguish between geo fields with various locales using many Ip to Geo service instances.","defaultValue":"_geo_","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.service","isRequired":true,"description":"The name of the cache service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"debug","isRequired":false,"description":"If true, an additional debug field is added. If the geo info fields prefix is X, a debug field named X_from_cache contains a boolean value to indicate the origin of the geo fields. The default value for this property is false (debug is disabled).","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"MatchIP","description":"IP address Query matching (using `Luwak )`_\n\nYou can use this processor to handle custom events matching IP address (CIDR)\nThe record sent from a matching an IP address record is tagged appropriately.\n\nA query is expressed as a lucene query against a field like for example: \n\n.. code::\n\n\tmessage:'bad exception'\n\terror_count:[10 TO *]\n\tbytes_out:5000\n\tuser_name:tom*\n\nPlease read the `Lucene syntax guide `_ for supported operations\n\n.. warning::\n\n\tdon't forget to set numeric fields property to handle correctly numeric ranges queries","component":"com.hurence.logisland.processor.MatchIP","type":"processor","tags":["analytic","percolator","record","record","query","lucene"],"properties":[{"name":"numeric.fields","isRequired":false,"description":"a comma separated string of numeric field to be matched","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"alert_match","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type.updatePolicy","isRequired":false,"description":"Record type update policy","defaultValue":"overwrite","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmatch","isRequired":false,"description":"the policy applied to match events: 'first' (default value) match events are tagged with the name and value of the first query that matched;'all' match events are tagged with all names and values of the queries that matched.","defaultValue":"first","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmiss","isRequired":false,"description":"the policy applied to miss events: 'discard' (default value) drop events that did not match any query;'forward' include also events that did not match any query.","defaultValue":"discard","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"include.input.records","isRequired":false,"description":"if set to true all the input records are copied to output","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"query","value":"some Lucene query","description":"generate a new record when this query is matched","isExpressionLanguageSupported":true}]}, {"name":"MatchQuery","description":"Query matching based on `Luwak `_\n\nyou can use this processor to handle custom events defined by lucene queries\na new record is added to output each time a registered query is matched\n\nA query is expressed as a lucene query against a field like for example: \n\n.. code::\n\n\tmessage:'bad exception'\n\terror_count:[10 TO *]\n\tbytes_out:5000\n\tuser_name:tom*\n\nPlease read the `Lucene syntax guide `_ for supported operations\n\n.. warning::\n\n\tdon't forget to set numeric fields property to handle correctly numeric ranges queries","component":"com.hurence.logisland.processor.MatchQuery","type":"processor","tags":["analytic","percolator","record","record","query","lucene"],"properties":[{"name":"numeric.fields","isRequired":false,"description":"a comma separated string of numeric field to be matched","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"alert_match","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type.updatePolicy","isRequired":false,"description":"Record type update policy","defaultValue":"overwrite","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmatch","isRequired":false,"description":"the policy applied to match events: 'first' (default value) match events are tagged with the name and value of the first query that matched;'all' match events are tagged with all names and values of the queries that matched.","defaultValue":"first","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"policy.onmiss","isRequired":false,"description":"the policy applied to miss events: 'discard' (default value) drop events that did not match any query;'forward' include also events that did not match any query.","defaultValue":"discard","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"include.input.records","isRequired":false,"description":"if set to true all the input records are copied to output","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"query","value":"some Lucene query","description":"generate a new record when this query is matched","isExpressionLanguageSupported":true}]}, -{"name":"ModifyId","description":"modify id of records or generate it following defined rules","component":"com.hurence.logisland.processor.ModifyId","type":"processor","tags":["record","id","idempotent","generate","modify"],"properties":[{"name":"id.generation.strategy","isRequired":true,"description":"the strategy to generate new Id","generate a random uid":"generate a randomUid using java library","generate a hash from fields":"generate a hash from fields","generate a string from java pattern and fields":"generate a string from java pattern and fields","generate a concatenation of type, time and a hash from fields":"generate a concatenation of type, time and a hash from fields (as for generate_hash strategy)","defaultValue":"randomUuid","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.hash","isRequired":true,"description":"the comma separated list of field names (e.g. : 'policyid,date_raw'","defaultValue":"record_raw_value","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"hash.charset","isRequired":true,"description":"the charset to use to hash id string (e.g. 'UTF-8')","defaultValue":"UTF-8","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"hash.algorithm","isRequired":true,"description":"the algorithme to use to hash id string (e.g. 'SHA-256'","SHA-384":null,"SHA-224":null,"SHA-256":null,"MD2":null,"SHA":null,"SHA-512":null,"MD5":null,"defaultValue":"SHA-256","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"java.formatter.string","isRequired":false,"description":"the format to use to build id string (e.g. '%4$2s %3$2s %2$2s %1$2s' (see java Formatter)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"language.tag","isRequired":true,"description":"the language to use to format numbers in string","aa":null,"ab":null,"ae":null,"af":null,"ak":null,"am":null,"an":null,"ar":null,"as":null,"av":null,"ay":null,"az":null,"ba":null,"be":null,"bg":null,"bh":null,"bi":null,"bm":null,"bn":null,"bo":null,"br":null,"bs":null,"ca":null,"ce":null,"ch":null,"co":null,"cr":null,"cs":null,"cu":null,"cv":null,"cy":null,"da":null,"de":null,"dv":null,"dz":null,"ee":null,"el":null,"en":null,"eo":null,"es":null,"et":null,"eu":null,"fa":null,"ff":null,"fi":null,"fj":null,"fo":null,"fr":null,"fy":null,"ga":null,"gd":null,"gl":null,"gn":null,"gu":null,"gv":null,"ha":null,"he":null,"hi":null,"ho":null,"hr":null,"ht":null,"hu":null,"hy":null,"hz":null,"ia":null,"id":null,"ie":null,"ig":null,"ii":null,"ik":null,"in":null,"io":null,"is":null,"it":null,"iu":null,"iw":null,"ja":null,"ji":null,"jv":null,"ka":null,"kg":null,"ki":null,"kj":null,"kk":null,"kl":null,"km":null,"kn":null,"ko":null,"kr":null,"ks":null,"ku":null,"kv":null,"kw":null,"ky":null,"la":null,"lb":null,"lg":null,"li":null,"ln":null,"lo":null,"lt":null,"lu":null,"lv":null,"mg":null,"mh":null,"mi":null,"mk":null,"ml":null,"mn":null,"mo":null,"mr":null,"ms":null,"mt":null,"my":null,"na":null,"nb":null,"nd":null,"ne":null,"ng":null,"nl":null,"nn":null,"no":null,"nr":null,"nv":null,"ny":null,"oc":null,"oj":null,"om":null,"or":null,"os":null,"pa":null,"pi":null,"pl":null,"ps":null,"pt":null,"qu":null,"rm":null,"rn":null,"ro":null,"ru":null,"rw":null,"sa":null,"sc":null,"sd":null,"se":null,"sg":null,"si":null,"sk":null,"sl":null,"sm":null,"sn":null,"so":null,"sq":null,"sr":null,"ss":null,"st":null,"su":null,"sv":null,"sw":null,"ta":null,"te":null,"tg":null,"th":null,"ti":null,"tk":null,"tl":null,"tn":null,"to":null,"tr":null,"ts":null,"tt":null,"tw":null,"ty":null,"ug":null,"uk":null,"ur":null,"uz":null,"ve":null,"vi":null,"vo":null,"wa":null,"wo":null,"xh":null,"yi":null,"yo":null,"za":null,"zh":null,"zu":null,"defaultValue":"en","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, +{"name":"ModifyId","description":"modify id of records or generate it following defined rules","component":"com.hurence.logisland.processor.ModifyId","type":"processor","tags":["record","id","idempotent","generate","modify"],"properties":[{"name":"id.generation.strategy","isRequired":true,"description":"the strategy to generate new Id","generate a random uid":"generate a randomUid using java library","generate a hash from fields":"generate a hash from fields","generate a string from java pattern and fields":"generate a string from java pattern and fields","generate a concatenation of type, time and a hash from fields":"generate a concatenation of type, time and a hash from fields (as for generate_hash strategy)","defaultValue":"randomUuid","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.hash","isRequired":true,"description":"the comma separated list of field names (e.g. : 'policyid,date_raw'","defaultValue":"record_value","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"hash.charset","isRequired":true,"description":"the charset to use to hash id string (e.g. 'UTF-8')","defaultValue":"UTF-8","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"hash.algorithm","isRequired":true,"description":"the algorithme to use to hash id string (e.g. 'SHA-256'","SHA-384":null,"SHA-224":null,"SHA-256":null,"MD2":null,"SHA":null,"SHA-512":null,"MD5":null,"defaultValue":"SHA-256","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"java.formatter.string","isRequired":false,"description":"the format to use to build id string (e.g. '%4$2s %3$2s %2$2s %1$2s' (see java Formatter)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"language.tag","isRequired":true,"description":"the language to use to format numbers in string","aa":null,"ab":null,"ae":null,"af":null,"ak":null,"am":null,"an":null,"ar":null,"as":null,"av":null,"ay":null,"az":null,"ba":null,"be":null,"bg":null,"bh":null,"bi":null,"bm":null,"bn":null,"bo":null,"br":null,"bs":null,"ca":null,"ce":null,"ch":null,"co":null,"cr":null,"cs":null,"cu":null,"cv":null,"cy":null,"da":null,"de":null,"dv":null,"dz":null,"ee":null,"el":null,"en":null,"eo":null,"es":null,"et":null,"eu":null,"fa":null,"ff":null,"fi":null,"fj":null,"fo":null,"fr":null,"fy":null,"ga":null,"gd":null,"gl":null,"gn":null,"gu":null,"gv":null,"ha":null,"he":null,"hi":null,"ho":null,"hr":null,"ht":null,"hu":null,"hy":null,"hz":null,"ia":null,"id":null,"ie":null,"ig":null,"ii":null,"ik":null,"in":null,"io":null,"is":null,"it":null,"iu":null,"iw":null,"ja":null,"ji":null,"jv":null,"ka":null,"kg":null,"ki":null,"kj":null,"kk":null,"kl":null,"km":null,"kn":null,"ko":null,"kr":null,"ks":null,"ku":null,"kv":null,"kw":null,"ky":null,"la":null,"lb":null,"lg":null,"li":null,"ln":null,"lo":null,"lt":null,"lu":null,"lv":null,"mg":null,"mh":null,"mi":null,"mk":null,"ml":null,"mn":null,"mo":null,"mr":null,"ms":null,"mt":null,"my":null,"na":null,"nb":null,"nd":null,"ne":null,"ng":null,"nl":null,"nn":null,"no":null,"nr":null,"nv":null,"ny":null,"oc":null,"oj":null,"om":null,"or":null,"os":null,"pa":null,"pi":null,"pl":null,"ps":null,"pt":null,"qu":null,"rm":null,"rn":null,"ro":null,"ru":null,"rw":null,"sa":null,"sc":null,"sd":null,"se":null,"sg":null,"si":null,"sk":null,"sl":null,"sm":null,"sn":null,"so":null,"sq":null,"sr":null,"ss":null,"st":null,"su":null,"sv":null,"sw":null,"ta":null,"te":null,"tg":null,"th":null,"ti":null,"tk":null,"tl":null,"tn":null,"to":null,"tr":null,"ts":null,"tt":null,"tw":null,"ty":null,"ug":null,"uk":null,"ur":null,"uz":null,"ve":null,"vi":null,"vo":null,"wa":null,"wo":null,"xh":null,"yi":null,"yo":null,"za":null,"zh":null,"zu":null,"defaultValue":"en","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"MultiGet","description":"Retrieves a content from datastore using datastore multiget queries.\nEach incoming record contains information regarding the datastore multiget query that will be performed. This information is stored in record fields whose names are configured in the plugin properties (see below) :\n- collection (String) : name of the datastore collection on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- type (String) : name of the datastore type on which the multiget query will be performed. This field is not mandatory.\n- ids (String) : comma separated list of document ids to fetch. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- includes (String) : comma separated list of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (String) : comma separated list of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds data of one datastore retrieved document. This data is stored in these fields :\n- collection (same field name as the incoming record) : name of the datastore collection.\n- type (same field name as the incoming record) : name of the datastore type.\n- id (same field name as the incoming record) : retrieved document id.\n- a list of String fields containing :\n * field name : the retrieved field name\n * field value : the retrieved field value","component":"com.hurence.logisland.processor.datastore.MultiGet","type":"processor","tags":["datastore","get","multiget"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":true,"description":"the name of the incoming records field containing es collection name to use in multiget query. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"type.field","isRequired":true,"description":"the name of the incoming records field containing es type name to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"ids.field","isRequired":true,"description":"the name of the incoming records field containing es document Ids to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"includes.field","isRequired":true,"description":"the name of the incoming records field containing es includes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"excludes.field","isRequired":true,"description":"the name of the incoming records field containing es excludes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"MultiGetElasticsearch","description":"Retrieves a content indexed in elasticsearch using elasticsearch multiget queries.\nEach incoming record contains information regarding the elasticsearch multiget query that will be performed. This information is stored in record fields whose names are configured in the plugin properties (see below) :\n- index (String) : name of the elasticsearch index on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- type (String) : name of the elasticsearch type on which the multiget query will be performed. This field is not mandatory.\n- ids (String) : comma separated list of document ids to fetch. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- includes (String) : comma separated list of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (String) : comma separated list of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds data of one elasticsearch retrieved document. This data is stored in these fields :\n- index (same field name as the incoming record) : name of the elasticsearch index.\n- type (same field name as the incoming record) : name of the elasticsearch type.\n- id (same field name as the incoming record) : retrieved document id.\n- a list of String fields containing :\n * field name : the retrieved field name\n * field value : the retrieved field value","component":"com.hurence.logisland.processor.elasticsearch.MultiGetElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":true,"description":"the name of the incoming records field containing es index name to use in multiget query. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":true,"description":"the name of the incoming records field containing es type name to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.ids.field","isRequired":true,"description":"the name of the incoming records field containing es document Ids to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.includes.field","isRequired":true,"description":"the name of the incoming records field containing es includes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.excludes.field","isRequired":true,"description":"the name of the incoming records field containing es excludes to use in multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"NormalizeFields","description":"Changes the name of a field according to a provided name mapping\n...","component":"com.hurence.logisland.processor.NormalizeFields","type":"processor","tags":["record","fields","normalizer"],"properties":[{"name":"conflict.resolution.policy","isRequired":true,"description":"what to do when a field with the same name already exists ?","nothing to do":"leave record as it was","overwrite existing field":"if field already exist","keep only old field and delete the other":"keep only old field and delete the other","keep old field and new one":"creates an alias for the new field","defaultValue":"do_nothing","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative mapping","value":"a comma separated list of possible field name","description":"when a field has a name contained in the list it will be renamed with this property field name","isExpressionLanguageSupported":true}]}, @@ -36,8 +37,8 @@ {"name":"SelectDistinctRecords","description":"Keep only distinct records based on a given field","component":"com.hurence.logisland.processor.SelectDistinctRecords","type":"processor","tags":["record","fields","remove","delete"],"properties":[{"name":"field.name","isRequired":true,"description":"the field to distinct records","defaultValue":"record_id","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"SendMail","description":"The SendMail processor is aimed at sending an email (like for instance an alert email) from an incoming record. There are three ways an incoming record can generate an email according to the special fields it must embed. Here is a list of the record fields that generate a mail and how they work:\n\n- **mail_text**: this is the simplest way for generating a mail. If present, this field means to use its content (value) as the payload of the mail to send. The mail is sent in text format if there is only this special field in the record. Otherwise, used with either mail_html or mail_use_template, the content of mail_text is the aletrnative text to the HTML mail that is generated.\n\n- **mail_html**: this field specifies that the mail should be sent as HTML and the value of the field is mail payload. If mail_text is also present, its value is used as the alternative text for the mail. mail_html cannot be used with mail_use_template: only one of those two fields should be present in the record.\n\n- **mail_use_template**: If present, this field specifies that the mail should be sent as HTML and the HTML content is to be generated from the template in the processor configuration key **html.template**. The template can contain parameters which must also be present in the record as fields. See documentation of html.template for further explanations. mail_use_template cannot be used with mail_html: only one of those two fields should be present in the record.\n\n If **allow_overwrite** configuration key is true, any mail.* (dot format) configuration key may be overwritten with a matching field in the record of the form mail_* (underscore format). For instance if allow_overwrite is true and mail.to is set to config_address@domain.com, a record generating a mail with a mail_to field set to record_address@domain.com will send a mail to record_address@domain.com.\n\n Apart from error records (when he is unable to process the incoming record or to send the mail), this processor is not expected to produce any output records.","component":"com.hurence.logisland.processor.SendMail","type":"processor","tags":["smtp","email","e-mail","mail","mailer","sendmail","message","alert","html"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, debug information are written to stdout.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.server","isRequired":true,"description":"FQDN, hostname or IP address of the SMTP server to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.port","isRequired":false,"description":"TCP port number of the SMTP server to use.","defaultValue":"25","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.security.username","isRequired":false,"description":"SMTP username.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.security.password","isRequired":false,"description":"SMTP password.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smtp.security.ssl","isRequired":false,"description":"Use SSL under SMTP or not (SMTPS). Default is false.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.from.address","isRequired":true,"description":"Valid mail sender email address.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.from.name","isRequired":false,"description":"Mail sender name.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.bounce.address","isRequired":true,"description":"Valid bounce email address (where error mail is sent if the mail is refused by the recipient server).","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.replyto.address","isRequired":false,"description":"Reply to email address.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.subject","isRequired":false,"description":"Mail subject.","defaultValue":"[LOGISLAND] Automatic email","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"mail.to","isRequired":false,"description":"Comma separated list of email recipients. If not set, the record must have a mail_to field and allow_overwrite configuration key should be true.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow_overwrite","isRequired":false,"description":"If true, allows to overwrite processor configuration with special record fields (mail_to, mail_from_address, mail_from_name, mail_bounce_address, mail_replyto_address, mail_subject). If false, special record fields are ignored and only processor configuration keys are used.","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"html.template","isRequired":false,"description":"HTML template to use. It is used when the incoming record contains a mail_use_template field. The template may contain some parameters. The parameter format in the template is of the form ${xxx}. For instance ${param_user} in the template means that a field named param_user must be present in the record and its value will replace the ${param_user} string in the HTML template when the mail will be sent. If some parameters are declared in the template, everyone of them must be present in the record as fields, otherwise the record will generate an error record. If an incoming record contains a mail_use_template field, a template must be present in the configuration and the HTML mail format will be used. If the record also contains a mail_text field, its content will be used as an alternative text message to be used in the mail reader program of the recipient if it does not supports HTML.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"SplitField","description":"This processor is used to create a new set of fields from one field (using split).","component":"com.hurence.logisland.processor.SplitField","type":"processor","tags":["parser","split","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"split.limit","isRequired":false,"description":"Specify the maximum number of split to allow","defaultValue":"10","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"split.counter.enable","isRequired":false,"description":"Enable the counter of items returned by the split","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"split.counter.suffix","isRequired":false,"description":"Enable the counter of items returned by the split","defaultValue":"Counter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative split field","value":"another split that could match","description":"This processor is used to create a new set of fields from one field (using split).","isExpressionLanguageSupported":true}]}, -{"name":"SplitText","description":"This is a processor that is used to split a String into fields according to a given Record mapping","component":"com.hurence.logisland.processor.SplitText","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"value.regex","isRequired":true,"description":"the regex to match for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"value.fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.regex","isRequired":false,"description":"the regex to match for the message key","defaultValue":".*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.fields","isRequired":false,"description":"a comma separated list of fields corresponding to matching groups for the message key","defaultValue":"record_raw_key","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"default type of record","defaultValue":"record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"keep.raw.content","isRequired":false,"description":"do we add the initial raw content ?","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timezone.record.time","isRequired":false,"description":"what is the time zone of the string formatted date for 'record_time' field.","defaultValue":"UTC","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"this regex will be tried if the main one has not matched. It must be in the form alt.value.regex.1 and alt.value.fields.1","isExpressionLanguageSupported":true}]}, +{"name":"SplitText","description":"This is a processor that is used to split a String into fields according to a given Record mapping","component":"com.hurence.logisland.processor.SplitText","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"value.regex","isRequired":true,"description":"the regex to match for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"value.fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.regex","isRequired":false,"description":"the regex to match for the message key","defaultValue":".*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.fields","isRequired":false,"description":"a comma separated list of fields corresponding to matching groups for the message key","defaultValue":"record_key","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"default type of record","defaultValue":"record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"keep.raw.content","isRequired":false,"description":"do we add the initial raw content ?","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timezone.record.time","isRequired":false,"description":"what is the time zone of the string formatted date for 'record_time' field.","defaultValue":"UTC","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"this regex will be tried if the main one has not matched. It must be in the form alt.value.regex.1 and alt.value.fields.1","isExpressionLanguageSupported":true}]}, {"name":"SplitTextMultiline","description":"No description provided.","component":"com.hurence.logisland.processor.SplitTextMultiline","type":"processor","properties":[{"name":"regex","isRequired":true,"description":"the regex to match","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"event.type","isRequired":true,"description":"the type of event","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, -{"name":"SplitTextWithProperties","description":"This is a processor that is used to split a String into fields according to a given Record mapping","component":"com.hurence.logisland.processor.SplitTextWithProperties","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"value.regex","isRequired":true,"description":"the regex to match for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"value.fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.regex","isRequired":false,"description":"the regex to match for the message key","defaultValue":".*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.fields","isRequired":false,"description":"a comma separated list of fields corresponding to matching groups for the message key","defaultValue":"record_raw_key","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"default type of record","defaultValue":"record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"keep.raw.content","isRequired":false,"description":"do we add the initial raw content ?","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"properties.field","isRequired":true,"description":"the field containing the properties to split and treat","defaultValue":"properties","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"this regex will be tried if the main one has not matched. It must be in the form alt.value.regex.1 and alt.value.fields.1","isExpressionLanguageSupported":true}]}, +{"name":"SplitTextWithProperties","description":"This is a processor that is used to split a String into fields according to a given Record mapping","component":"com.hurence.logisland.processor.SplitTextWithProperties","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"value.regex","isRequired":true,"description":"the regex to match for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"value.fields","isRequired":true,"description":"a comma separated list of fields corresponding to matching groups for the message value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.regex","isRequired":false,"description":"the regex to match for the message key","defaultValue":".*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"key.fields","isRequired":false,"description":"a comma separated list of fields corresponding to matching groups for the message key","defaultValue":"record_key","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.type","isRequired":false,"description":"default type of record","defaultValue":"record","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"keep.raw.content","isRequired":false,"description":"do we add the initial raw content ?","defaultValue":"true","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"properties.field","isRequired":true,"description":"the field containing the properties to split and treat","defaultValue":"properties","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"this regex will be tried if the main one has not matched. It must be in the form alt.value.regex.1 and alt.value.fields.1","isExpressionLanguageSupported":true}]}, {"name":"setSourceOfTraffic","description":"Compute the source of traffic of a web session. Users arrive at a website or application through a variety of sources, \nincluding advertising/paying campaigns, search engines, social networks, referring sites or direct access. \nWhen analysing user experience on a webshop, it is crucial to collects, processes, and reports the campaign and traffic-source data. \nTo compute the source of traffic of a web session, the user has to provide the utm_* related properties if available\ni-e: **utm_source.field**, **utm_medium.field**, **utm_campaign.field**, **utm_content.field**, **utm_term.field**)\n, the referer (**referer.field** property) and the first visited page of the session (**first.visited.page.field** property).\nBy default the source of traffic informations are placed in a flat structure (specified by the **source_of_traffic.suffix** property\n with a default value of source_of_traffic_). To work properly the setSourceOfTraffic processor needs to have access to an \nElasticsearch index containing a list of the most popular search engines and social networks. The ES index (specified by the **es.index** property) should be structured such that the _id of an ES document MUST be the name of the domain. If the domain is a search engine, the related ES doc MUST have a boolean field (default being search_engine) specified by the property **es.search_engine.field** with a value set to true. If the domain is a social network , the related ES doc MUST have a boolean field (default being social_network) specified by the property **es.social_network.field** with a value set to true. ","component":"com.hurence.logisland.processor.webAnalytics.setSourceOfTraffic","type":"processor","tags":["session","traffic","source","web","analytics"],"properties":[{"name":"referer.field","isRequired":false,"description":"Name of the field containing the referer value in the session","defaultValue":"referer","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"first.visited.page.field","isRequired":false,"description":"Name of the field containing the first visited page in the session","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_source.field","isRequired":false,"description":"Name of the field containing the utm_source value in the session","defaultValue":"utm_source","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_medium.field","isRequired":false,"description":"Name of the field containing the utm_medium value in the session","defaultValue":"utm_medium","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_campaign.field","isRequired":false,"description":"Name of the field containing the utm_campaign value in the session","defaultValue":"utm_campaign","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_content.field","isRequired":false,"description":"Name of the field containing the utm_content value in the session","defaultValue":"utm_content","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"utm_term.field","isRequired":false,"description":"Name of the field containing the utm_term value in the session","defaultValue":"utm_term","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"source_of_traffic.suffix","isRequired":false,"description":"Suffix for the source of the traffic related fields","defaultValue":"source_of_traffic","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"source_of_traffic.hierarchical","isRequired":false,"description":"Should the additional source of trafic information fields be added under a hierarchical father field or not.","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.service","isRequired":true,"description":"Name of the cache service to use.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"cache.validity.timeout","isRequired":false,"description":"Timeout validity (in seconds) of an entry in the cache.","defaultValue":"0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"debug","isRequired":false,"description":"If true, an additional debug field is added. If the source info fields prefix is X, a debug field named X_from_cache contains a boolean value to indicate the origin of the source fields. The default value for this property is false (debug is disabled).","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index","isRequired":true,"description":"Name of the ES index containing the list of search engines and social network. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type","isRequired":false,"description":"Name of the ES type to use.","defaultValue":"default","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.search_engine.field","isRequired":false,"description":"Name of the ES field used to specify that the domain is a search engine.","defaultValue":"search_engine","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.social_network.field","isRequired":false,"description":"Name of the ES field used to specify that the domain is a social network.","defaultValue":"social_network","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, ] diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index b8e778012..c74459dc6 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -147,6 +147,93 @@ In the list below, the names of required properties appear in **bold**. Any othe ---------- +.. _com.hurence.logisland.processor.alerting.ComputeTag: + +ComputeTag +---------- +Add one or more field with a default value +... + +Class +_____ +com.hurence.logisland.processor.alerting.ComputeTag + +Tags +____ +record, fields, Add + +Properties +__________ +In the list below, the names of required properties appear in **bold**. Any other properties (not in bold) are considered optional. The table also indicates any default values +. + +.. csv-table:: allowable-values + :header: "Name","Description","Allowable Values","Default Value","Sensitive","EL" + :widths: 20,60,30,20,10,10 + + "max.cpu.time", "maximum CPU time in milliseconds allowed for script execution.", "", "100", "", "" + "max.memory", "maximum memory in Bytes which JS executor thread can allocate", "", "51200", "", "" + "allow.no.brace", "Force, to check if all blocks are enclosed with curly braces "{}". +

+ Explanation: all loops (for, do-while, while, and if-else, and functions + should use braces, because poison_pill() function will be inserted after + each open brace "{", to ensure interruption checking. Otherwise simple + code like: +

+    while(true) while(true) {
+      // do nothing
+    }
+  
+ or even: +
+    while(true)
+  
+ cause unbreakable loop, which force this sandbox to use {@link Thread#stop()} + which make JVM unstable. +

+

+ Properly writen code (even in bad intention) like: +

+    while(true) { while(true) {
+      // do nothing
+    }}
+  
+ will be changed into: +
+    while(true) {poison_pill(); 
+      while(true) {poison_pill();
+        // do nothing
+      }
+    }
+  
+ which finish nicely when interrupted. +

+ For legacy code, this check can be turned off, but with no guarantee, the + JS thread will gracefully finish when interrupted. +

", "", "false", "", "" + "max.prepared.statements", "The size of prepared statements LRU cache. Default 0 (disabled). +

+ Each statements when {@link #setMaxCPUTime(long)} is set is prepared to + quit itself when time exceeded. To execute only once this procedure per + statement set this value. +

+

+ When {@link #setMaxCPUTime(long)} is set 0, this value is ignored. +

", "", "30", "", "" + "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + +Dynamic Properties +__________________ +Dynamic Properties allow the user to specify both the name and value of a property. + +.. csv-table:: dynamic-properties + :header: "Name","Value","Description","EL" + :widths: 20,20,40,10 + + "field to add", "a default value", "Add a field to the record with the default value", "" + +---------- + .. _com.hurence.logisland.processor.webAnalytics.ConsolidateSession: ConsolidateSession @@ -795,7 +882,7 @@ In the list below, the names of required properties appear in **bold**. Any othe :widths: 20,60,30,20,10,10 "**id.generation.strategy**", "the strategy to generate new Id", "generate a random uid (generate a randomUid using java library), generate a hash from fields (generate a hash from fields), generate a string from java pattern and fields (generate a string from java pattern and fields), generate a concatenation of type, time and a hash from fields (generate a concatenation of type, time and a hash from fields (as for generate_hash strategy))", "randomUuid", "", "" - "**fields.to.hash**", "the comma separated list of field names (e.g. : 'policyid,date_raw'", "", "record_raw_value", "", "" + "**fields.to.hash**", "the comma separated list of field names (e.g. : 'policyid,date_raw'", "", "record_value", "", "" "**hash.charset**", "the charset to use to hash id string (e.g. 'UTF-8')", "", "UTF-8", "", "" "**hash.algorithm**", "the algorithme to use to hash id string (e.g. 'SHA-256'", "SHA-384, SHA-224, SHA-256, MD2, SHA, SHA-512, MD5", "SHA-256", "", "" "java.formatter.string", "the format to use to build id string (e.g. '%4$2s %3$2s %2$2s %1$2s' (see java Formatter)", "", "null", "", "" @@ -1498,7 +1585,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "**value.regex**", "the regex to match for the message value", "", "null", "", "" "**value.fields**", "a comma separated list of fields corresponding to matching groups for the message value", "", "null", "", "" "key.regex", "the regex to match for the message key", "", ".*", "", "" - "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_raw_key", "", "" + "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_key", "", "" "record.type", "default type of record", "", "record", "", "" "keep.raw.content", "do we add the initial raw content ?", "", "true", "", "" "timezone.record.time", "what is the time zone of the string formatted date for 'record_time' field.", "", "UTC", "", "" @@ -1574,7 +1661,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "**value.regex**", "the regex to match for the message value", "", "null", "", "" "**value.fields**", "a comma separated list of fields corresponding to matching groups for the message value", "", "null", "", "" "key.regex", "the regex to match for the message key", "", ".*", "", "" - "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_raw_key", "", "" + "key.fields", "a comma separated list of fields corresponding to matching groups for the message key", "", "record_key", "", "" "record.type", "default type of record", "", "record", "", "" "keep.raw.content", "do we add the initial raw content ?", "", "true", "", "" "**properties.field**", "the field containing the properties to split and treat", "", "properties", "", "" diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/StandardProcessorTestRunner.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/StandardProcessorTestRunner.java index e73511fd6..0eda7dfd1 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/StandardProcessorTestRunner.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/StandardProcessorTestRunner.java @@ -218,8 +218,9 @@ public void assertAllInputRecordsProcessed() { @Override public void assertOutputRecordsCount(int count) { - assertTrue("expected output record count was " + count + " but is currently " + - outputRecordsList.size(), outputRecordsList.size() == count); + long recordsCount = + outputRecordsList.stream().filter(r -> !r.hasField(FieldDictionary.RECORD_ERRORS)).count(); + assertTrue("expected output record count was " + count + " but is currently " +recordsCount, recordsCount == count); } @Override diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java index a1775b606..b29134057 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java @@ -23,10 +23,7 @@ import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.processor.AbstractProcessor; import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.record.FieldDictionary; -import com.hurence.logisland.record.FieldType; -import com.hurence.logisland.record.Record; -import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.record.*; import com.hurence.logisland.service.datastore.DatastoreClientService; import com.hurence.logisland.validator.StandardValidators; import delight.nashornsandbox.NashornSandbox; @@ -145,6 +142,7 @@ public class ComputeTag extends AbstractProcessor { protected DatastoreClientService datastoreClientService; protected NashornSandbox sandbox; + protected Map dynamicTagValuesMap; @Override public List getSupportedPropertyDescriptors() { @@ -206,8 +204,7 @@ public void init(ProcessContext context) { sandbox.allow(FieldDictionary.class); - // loop over js expression to evaluate/* Build the JsonPath expressions from attributes */ - final Map dynamicTagValuesMap = new HashMap<>(); + dynamicTagValuesMap = new HashMap<>(); for (final Map.Entry entry : context.getProperties().entrySet()) { if (!entry.getKey().isDynamic()) { @@ -234,14 +231,12 @@ public void init(ProcessContext context) { String value = entry.getValue() .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); - // dynamicTagValuesMap.put(entry.getKey().getName(), entry.getValue()); - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); sb.append("function ") .append(key) - .append("( ) { ") + .append("() { ") .append(value) - .append(" } \n"); sb.append("var record_") .append(key) @@ -257,21 +252,8 @@ public void init(ProcessContext context) { .append(key) .append("());\n"); - try { - System.out.println(sb.toString()); - sandbox.eval(sb.toString()); - /* sandbox.eval( "function oula() { var recordToFetch = new com.hurence.logisland.record.StandardRecord();" + - "var test= cache.get(\"test\",recordToFetch.setId(\"cached_id1\")).getField(\"record_value\").asString(); " + - "return test;}" + - "oula();"); - - System.out.println(value); - sandbox.eval( "function oula() { " + value +"}" + - "oula();");*/ - } catch (ScriptException e) { - e.printStackTrace(); - } - + dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); + System.out.println(sb.toString()); logger.debug(sb.toString()); } @@ -286,12 +268,20 @@ public Collection process(ProcessContext context, Collection rec } List outputRecords = new ArrayList<>(); - for (Record record : records) { + for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { + - Record cached = (Record) sandbox.get("record_cvib1"); - /* Record comptedRecord = new StandardRecord("computed_record") - .setStringField("test_field", stored.toString());*/ - outputRecords.add(cached); + try { + sandbox.eval(entry.getValue()); + Record cached = (Record) sandbox.get("record_" + entry.getKey()); + outputRecords.add(cached); + } catch (ScriptException e) { + Record errorRecord = new StandardRecord(RecordDictionary.ERROR) + .setId(entry.getKey()) + .addError("ScriptException", e.getMessage() ); + outputRecords.add(errorRecord); + logger.error(e.toString()); + } } return outputRecords; diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java index da8866ba1..590e8bbab 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/ModifyIdTest.java @@ -131,7 +131,8 @@ public void testFormatStrategy() { testRunner.enqueue(record1); testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(1); + testRunner.assertOutputRecordsCount(0); + testRunner.assertOutputErrorCount(1); MockRecord outputRecord = testRunner.getOutputRecords().get(0); outputRecord.assertRecordSizeEquals(5); @@ -151,7 +152,8 @@ public void testFormatStrategy() { testRunner.enqueue(record1); testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(1); + testRunner.assertOutputErrorCount(1); + testRunner.assertOutputRecordsCount(0); outputRecord = testRunner.getOutputRecords().get(0); outputRecord.assertRecordSizeEquals(5); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java index 8127bcdcd..03df8e4fd 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/SplitTextTest.java @@ -68,7 +68,8 @@ public void testUsrBackend() { testRunner.clearQueues(); testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(244); + testRunner.assertOutputRecordsCount(8); + testRunner.assertOutputErrorCount(244-8); testRunner.setProperty(SplitText.VALUE_REGEX, USR_GATEWAY_REGEX); testRunner.setProperty(SplitText.VALUE_FIELDS, USR_GATEWAY_FIELDS); @@ -77,7 +78,8 @@ public void testUsrBackend() { testRunner.clearQueues(); testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(201); + testRunner.assertOutputRecordsCount(73); + testRunner.assertOutputErrorCount(201-73); } @@ -155,7 +157,7 @@ public void testApacheLogWithBadRegex() { testRunner.clearQueues(); testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(200); + testRunner.assertOutputRecordsCount(0); testRunner.assertOutputErrorCount(200); MockRecord out = testRunner.getOutputRecords().get(0); @@ -308,7 +310,7 @@ public void testSpecificAlternativematch() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 77.154.202.48"); out.assertFieldEquals("raw_date", "Jan 17 18:52:18"); - out.assertFieldEquals("record_raw_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 77.154.202.48"); + out.assertFieldEquals("record_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 77.154.202.48"); out.assertFieldEquals("record_time", 1484679138000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); @@ -319,7 +321,7 @@ public void testSpecificAlternativematch() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("raw_date", "Jan 17 18:52:18"); - out.assertFieldEquals("record_raw_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); + out.assertFieldEquals("record_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("record_time", 1484679138000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); @@ -330,7 +332,7 @@ public void testSpecificAlternativematch() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("raw_date", "Jan 17 18:52:18"); - out.assertFieldEquals("record_raw_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); + out.assertFieldEquals("record_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("record_time", 1484679138000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); @@ -341,7 +343,7 @@ public void testSpecificAlternativematch() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("raw_date", "Jan 17 18:52:18"); - out.assertFieldEquals("record_raw_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); + out.assertFieldEquals("record_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("record_time", 1484679138000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); @@ -352,7 +354,7 @@ public void testSpecificAlternativematch() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("raw_date", "Jan 17 18:52:18"); - out.assertFieldEquals("record_raw_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); + out.assertFieldEquals("record_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.130.95.204"); out.assertFieldEquals("record_time", 1484679138000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); @@ -364,7 +366,7 @@ public void testSpecificAlternativematch() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 92.143.11.69"); out.assertFieldEquals("raw_date", "Jan 24 19:59:56"); - out.assertFieldEquals("record_raw_value", "Jan 24 19:59:56 EagleP12.prod.hurence.fr/EagleP12.prod.hurence.fr 2017 Jan 24 19:59:56 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.143.11.69"); + out.assertFieldEquals("record_value", "Jan 24 19:59:56 EagleP12.prod.hurence.fr/EagleP12.prod.hurence.fr 2017 Jan 24 19:59:56 INFO SSL: Host www.hurence.fr received fin without close notify alert from 92.143.11.69"); out.assertFieldEquals("record_time", 1485287996000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); @@ -400,7 +402,7 @@ public void testTimeZoneProperty() { out.assertFieldEquals("level", "INFO"); out.assertFieldEquals("message", "SSL: Host www.hurence.fr received fin without close notify alert from 77.154.202.48"); out.assertFieldEquals("raw_date", "Jan 17 18:52:18"); - out.assertFieldEquals("record_raw_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 77.154.202.48"); + out.assertFieldEquals("record_value", "Jan 17 18:52:18 EagleP13.prod.hurence.fr/EagleP13.prod.hurence.fr 2017 Jan 17 18:52:18 INFO SSL: Host www.hurence.fr received fin without close notify alert from 77.154.202.48"); out.assertFieldEquals("record_time", 1484693538000L); out.assertFieldEquals("record_type", "apache_log"); out.assertRecordSizeEquals(6); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index 97f607703..0f3fd670a 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -29,39 +29,35 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import static org.junit.Assert.assertEquals; public class ComputeTagsTest { @Test - public void testSimpleEnrichment() throws InitializationException { - + public void testMultipleRules() throws InitializationException { // create the controller service and link it to the test processor final DatastoreClientService service = new MockDatastoreService(); - getLookupRecords().forEach(r -> service.put("test", r, false)); + getCacheRecords().forEach(r -> service.put("test", r, false)); final TestRunner runner = TestRunners.newTestRunner(ComputeTag.class); runner.setProperty(ComputeTag.MAX_CPU_TIME, "100"); runner.setProperty(ComputeTag.MAX_MEMORY, "12800000"); runner.setProperty(ComputeTag.MAX_PREPARED_STATEMENTS, "100"); runner.setProperty(ComputeTag.ALLOw_NO_BRACE, "false"); + runner.setProperty("cvib3", "return 37.2/10*3;"); + runner.setProperty("cvib2", "if( cache(\"cached_id2\").value > 10 ) return 0.0; else return 1.0;"); runner.setProperty("cvib1", "return cache(\"cached_id1\").value * 10.2;"); runner.setProperty(ComputeTag.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); runner.addControllerService(service.getIdentifier(), service); runner.enableControllerService(service); - final DatastoreClientService lookupService = runner.getProcessContext() .getPropertyValue(ComputeTag.DATASTORE_CLIENT_SERVICE) .asControllerService(MockDatastoreService.class); - Collection recordsToEnrich = getRecords(); - runner.assertValid(); runner.enqueue(recordsToEnrich); runner.run(); @@ -69,12 +65,57 @@ public void testSimpleEnrichment() throws InitializationException { runner.assertOutputRecordsCount(3); runner.assertOutputErrorCount(0); + for (Record enriched : runner.getOutputRecords()) { + if (enriched.getId().equals("cvib1")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asDouble(), 10.2 * 12.45, 0.0001); + } else if (enriched.getId().equals("cvib2")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asDouble(), 1.0, 0.00001); + } else if (enriched.getId().equals("cvib3")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asDouble(), 37.2 / 10.0 * 3.0, 0.00001); + } + } + } + - Record enriched0 = runner.getOutputRecords().get(0); + @Test + public void testBadRules() throws InitializationException { - assertEquals(enriched0.getId(), "cvib1"); - assertEquals((double) enriched0.getField(FieldDictionary.RECORD_VALUE).asDouble(), 10.2 * 12.45, 0.0001); + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + final TestRunner runner = TestRunners.newTestRunner(ComputeTag.class); + runner.setProperty(ComputeTag.MAX_CPU_TIME, "100"); + runner.setProperty(ComputeTag.MAX_MEMORY, "12800000"); + runner.setProperty(ComputeTag.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(ComputeTag.ALLOw_NO_BRACE, "false"); + runner.setProperty("cvib3", "return 37.2/++10*3;"); + /* runner.setProperty("cvib2", "if( cache(\"cached_id2\").value > 10 ) return 0.0; else return 1.0;"); + runner.setProperty("cvib1", "return cache(\"cached_id1\").value * 10.2;");*/ + runner.setProperty(ComputeTag.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + final DatastoreClientService lookupService = runner.getProcessContext() + .getPropertyValue(ComputeTag.DATASTORE_CLIENT_SERVICE) + .asControllerService(MockDatastoreService.class); + + Collection recordsToEnrich = getRecords(); + runner.assertValid(); + runner.enqueue(recordsToEnrich); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(0); + runner.assertOutputErrorCount(1); + + for (Record enriched : runner.getOutputRecords()) { + if (enriched.getId().equals("cvib3")) { + assertEquals(enriched.getErrors().toArray()[0], + "ScriptException: :3:17 Invalid left hand side for assignment\n" + + " return 37.2 / ++10 * 3;\n" + + " ^ in at line number 3 at column number 17"); + } + } } private Collection getRecords() { @@ -100,37 +141,8 @@ private Collection getRecords() { return recordsToEnrich; } - private Map getJoinedRecords() { - Map lookupRecords = new HashMap<>(); - - lookupRecords.put("id1", new StandardRecord() - .setId("id1") - .setField("f1", FieldType.STRING, "value1") - .setField("f2", FieldType.STRING, "falue2") - .setField("a", FieldType.STRING, "a1") - .setField("b", FieldType.STRING, "b1") - .setField("c", FieldType.LONG, 1)); - - lookupRecords.put("id2", new StandardRecord() - .setId("id2") - .setField("f1", FieldType.STRING, "value3") - .setField("f2", FieldType.STRING, "falue4") - .setField("a", FieldType.STRING, "a2") - .setField("b", FieldType.STRING, "b2") - .setField("c", FieldType.LONG, 2)); - - lookupRecords.put("id3", new StandardRecord() - .setId("id3") - .setField("f1", FieldType.STRING, "value5") - .setField("f2", FieldType.STRING, "falue6") - .setField("a", FieldType.STRING, "a3") - .setField("b", FieldType.STRING, "b3") - .setField("c", FieldType.LONG, 3)); - - return lookupRecords; - } - private Collection getLookupRecords() { + private Collection getCacheRecords() { Collection lookupRecords = new ArrayList<>(); lookupRecords.add(new StandardRecord() diff --git a/logisland-plugins/logisland-cyber-security-plugin/src/test/java/com/hurence/logisland/processor/networkpacket/ParseNetworkPacketTest.java b/logisland-plugins/logisland-cyber-security-plugin/src/test/java/com/hurence/logisland/processor/networkpacket/ParseNetworkPacketTest.java index f76daf904..edb3d4718 100644 --- a/logisland-plugins/logisland-cyber-security-plugin/src/test/java/com/hurence/logisland/processor/networkpacket/ParseNetworkPacketTest.java +++ b/logisland-plugins/logisland-cyber-security-plugin/src/test/java/com/hurence/logisland/processor/networkpacket/ParseNetworkPacketTest.java @@ -502,7 +502,7 @@ public void testARPPCapRecord() { testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(2); + testRunner.assertOutputRecordsCount(1); testRunner.assertOutputErrorCount(1); MockRecord out = testRunner.getOutputRecords().get(0); @@ -562,7 +562,7 @@ public void testMediumSizePCapRecord() { testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(14261); + testRunner.assertOutputRecordsCount(14261-52); testRunner.assertOutputErrorCount(52); //int i = 0; @@ -598,7 +598,7 @@ public void testDummyPCapRecord() { testRunner.run(); testRunner.assertAllInputRecordsProcessed(); - testRunner.assertOutputRecordsCount(1); + testRunner.assertOutputRecordsCount(0); testRunner.assertOutputErrorCount(1); MockRecord out = testRunner.getOutputRecords().get(0); diff --git a/logisland-plugins/logisland-elasticsearch-plugin/src/test/java/com/hurence/logisland/processor/elasticsearch/TestMultiGetElasticsearch.java b/logisland-plugins/logisland-elasticsearch-plugin/src/test/java/com/hurence/logisland/processor/elasticsearch/TestMultiGetElasticsearch.java index 3d32bfb5b..798786c2f 100644 --- a/logisland-plugins/logisland-elasticsearch-plugin/src/test/java/com/hurence/logisland/processor/elasticsearch/TestMultiGetElasticsearch.java +++ b/logisland-plugins/logisland-elasticsearch-plugin/src/test/java/com/hurence/logisland/processor/elasticsearch/TestMultiGetElasticsearch.java @@ -190,7 +190,7 @@ public void testMultiGetCorruptedRecords() throws IOException, InitializationExc runner.clearQueues(); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(12); + runner.assertOutputRecordsCount(8); runner.assertOutputErrorCount(4); From 2d7997adff14505f79bfd0a6b68e169eb5f6ad63 Mon Sep 17 00:00:00 2001 From: oalam Date: Tue, 29 May 2018 15:34:05 +0200 Subject: [PATCH 13/63] fix doc bug --- logisland-documentation/tutorials/prerequisites.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logisland-documentation/tutorials/prerequisites.rst b/logisland-documentation/tutorials/prerequisites.rst index 61c9995ae..b225aeefc 100644 --- a/logisland-documentation/tutorials/prerequisites.rst +++ b/logisland-documentation/tutorials/prerequisites.rst @@ -11,7 +11,7 @@ There are two main ways to launch a logisland job : ------------------------------------------ Logisland is packaged as a Docker container that you can build yourself or pull from Docker Hub. -To facilitate integration testing and to easily run tutorials, you can create a `docker-compose.yml` file with the following content, or directly download it from `a gist `_ +To facilitate integration testing and to easily run tutorials, you can create a `docker-compose.yml` file with the following content. .. code-block:: yaml From a10d52ef912b29d9d4fe51a08eef51112e872739 Mon Sep 17 00:00:00 2001 From: oalam Date: Tue, 29 May 2018 17:39:28 +0200 Subject: [PATCH 14/63] implement CheckThresholds processor --- .../logisland/record/RecordDictionary.java | 1 + ...a => AbstractNashornSandboxProcessor.java} | 108 +++------------ .../processor/alerting/CheckThresholds.java | 127 ++++++++++++++++++ .../processor/alerting/ComputeTags.java | 114 ++++++++++++++++ .../alerting/CheckThresholdsTest.java | 114 ++++++++++++++++ .../processor/alerting/ComputeTagsTest.java | 28 ++-- 6 files changed, 387 insertions(+), 105 deletions(-) rename logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/{ComputeTag.java => AbstractNashornSandboxProcessor.java} (69%) create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java b/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java index 9e28ebaa4..a957407b3 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java @@ -25,4 +25,5 @@ public class RecordDictionary { public static String ERROR = "error"; public static String TAG = "tag"; public static String COMPUTED_TAG = "computed_tag"; + public static String THRESHOLD = "threshold"; } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java similarity index 69% rename from logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java rename to logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java index b29134057..1d5342dcd 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTag.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java @@ -19,11 +19,13 @@ import com.hurence.logisland.annotation.behavior.DynamicProperty; import com.hurence.logisland.annotation.documentation.CapabilityDescription; import com.hurence.logisland.annotation.documentation.Tags; -import com.hurence.logisland.component.AllowableValue; import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.processor.AbstractProcessor; import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.record.*; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.FieldType; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.StandardRecord; import com.hurence.logisland.service.datastore.DatastoreClientService; import com.hurence.logisland.validator.StandardValidators; import delight.nashornsandbox.NashornSandbox; @@ -31,8 +33,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.script.ScriptException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.Executors; @Tags({"record", "fields", "Add"}) @@ -42,17 +46,10 @@ supportsExpressionLanguage = false, value = "a default value", description = "Add a field to the record with the default value") -public class ComputeTag extends AbstractProcessor { +public abstract class AbstractNashornSandboxProcessor extends AbstractProcessor { - private static final Logger logger = LoggerFactory.getLogger(ComputeTag.class); - - - private static final AllowableValue OVERWRITE_EXISTING = - new AllowableValue("overwrite_existing", "overwrite existing field", "if field already exist"); - - private static final AllowableValue KEEP_OLD_FIELD = - new AllowableValue("keep_only_old_field", "keep only old field value", "keep only old field"); + private static final Logger logger = LoggerFactory.getLogger(AbstractNashornSandboxProcessor.class); public static final PropertyDescriptor MAX_CPU_TIME = new PropertyDescriptor.Builder() @@ -174,8 +171,12 @@ public boolean hasControllerService() { return true; } + + abstract protected void setupDynamicProperties(ProcessContext context); + @Override public void init(ProcessContext context) { + super.init(context); sandbox = NashornSandboxes.create(); @@ -204,86 +205,11 @@ public void init(ProcessContext context) { sandbox.allow(FieldDictionary.class); - dynamicTagValuesMap = new HashMap<>(); - - for (final Map.Entry entry : context.getProperties().entrySet()) { - if (!entry.getKey().isDynamic()) { - continue; - } - - /** - * cvib1: return cache("vib1").value * 10.2 * ( 1.0 - 1.0 / cache("vib2").value ); - * - * - * will be translated into - - function cvib1( cache ) { return cache("vib1").value * 10.2 * ( 1.0 - 1.0 / cache("vib2").value ); } - - var record_cvb1 = new com.hurence.logisland.record.StandardRecord("cvb1"); - record_cvb1.setField( - com.hurence.logisland.record.FieldDictionary.RECORD_VALUE, - com.hurence.logisland.record.FieldType.DOUBLE, - cvib1( cache ) - ); - - */ - String key = entry.getKey().getName(); - String value = entry.getValue() - .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") - .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); - - StringBuilder sb = new StringBuilder(); - sb.append("function ") - .append(key) - .append("() { ") - .append(value) - .append(" } \n"); - sb.append("var record_") - .append(key) - .append(" = new com.hurence.logisland.record.StandardRecord()") - .append(".setId(\"") - .append(key) - .append("\");\n"); - sb.append("record_") - .append(key) - .append(".setField( ") - .append("\"record_value\",") - .append(" com.hurence.logisland.record.FieldType.DOUBLE,") - .append(key) - .append("());\n"); - - dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - System.out.println(sb.toString()); - logger.debug(sb.toString()); - } + dynamicTagValuesMap = new HashMap<>(); - } + this.setupDynamicProperties(context); - @Override - public Collection process(ProcessContext context, Collection records) { - - // check if we need initialization - if (datastoreClientService == null) { - init(context); - } - List outputRecords = new ArrayList<>(); - for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { - - - try { - sandbox.eval(entry.getValue()); - Record cached = (Record) sandbox.get("record_" + entry.getKey()); - outputRecords.add(cached); - } catch (ScriptException e) { - Record errorRecord = new StandardRecord(RecordDictionary.ERROR) - .setId(entry.getKey()) - .addError("ScriptException", e.getMessage() ); - outputRecords.add(errorRecord); - logger.error(e.toString()); - } - } - - return outputRecords; } + } \ No newline at end of file diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java new file mode 100644 index 000000000..3aa3f1813 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.annotation.behavior.DynamicProperty; +import com.hurence.logisland.annotation.documentation.CapabilityDescription; +import com.hurence.logisland.annotation.documentation.Tags; +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.RecordDictionary; +import com.hurence.logisland.record.StandardRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.ScriptException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Tags({"record", "fields", "Add"}) +@CapabilityDescription("Add one or more field with a default value\n" + + "...") +@DynamicProperty(name = "field to add", + supportsExpressionLanguage = false, + value = "a default value", + description = "Add a field to the record with the default value") +public class CheckThresholds extends AbstractNashornSandboxProcessor { + + + /* + - processor: compute_thresholds + component: com.hurence.logisland.processor.CheckThresholdCross + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + + a threshold_cross has the following properties : count, sum, avg, time, duration, value + configuration: + cache.client.service: cache + default.record_type: threshold_cross + default.el.language: js + default.ttl: 300000 + tvib1: cache("vib1").value > 10.0; + tvib2: cache("vib2").value >= 0 && cache("vib2").value < cache("vib1").value; + */ + + private static final Logger logger = LoggerFactory.getLogger(CheckThresholds.class); + + @Override + protected void setupDynamicProperties(ProcessContext context) { + for (final Map.Entry entry : context.getProperties().entrySet()) { + if (!entry.getKey().isDynamic()) { + continue; + } + + String key = entry.getKey().getName(); + String value = entry.getValue() + .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") + .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); + + StringBuilder sb = new StringBuilder(); + sb.append("var match=false;\n"); + sb.append("if( ") + .append(value) + .append(" ) { match=true; }\n"); + + dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); + System.out.println(sb.toString()); + logger.debug(sb.toString()); + } + } + + + @Override + public Collection process(ProcessContext context, Collection records) { + + // check if we need initialization + if (datastoreClientService == null) { + init(context); + } + + List outputRecords = new ArrayList<>(); + for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { + + + + + try { + sandbox.eval(entry.getValue()); + Boolean match = (Boolean) sandbox.get("match"); + if (match) { + outputRecords.add(new StandardRecord(RecordDictionary.THRESHOLD) + .setId(entry.getKey()) + .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(entry.getKey()).asString())); + } + } catch (ScriptException e) { + Record errorRecord = new StandardRecord(RecordDictionary.ERROR) + .setId(entry.getKey()) + .addError("ScriptException", e.getMessage()); + outputRecords.add(errorRecord); + logger.error(e.toString()); + } + } + + return outputRecords; + } +} \ No newline at end of file diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java new file mode 100644 index 000000000..63b67d724 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.annotation.behavior.DynamicProperty; +import com.hurence.logisland.annotation.documentation.CapabilityDescription; +import com.hurence.logisland.annotation.documentation.Tags; +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.RecordDictionary; +import com.hurence.logisland.record.StandardRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.ScriptException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Tags({"record", "fields", "Add"}) +@CapabilityDescription("Add one or more field with a default value\n" + + "...") +@DynamicProperty(name = "field to add", + supportsExpressionLanguage = false, + value = "a default value", + description = "Add a field to the record with the default value") +public class ComputeTags extends AbstractNashornSandboxProcessor { + + + private static final Logger logger = LoggerFactory.getLogger(ComputeTags.class); + + + @Override + protected void setupDynamicProperties(ProcessContext context) { + for (final Map.Entry entry : context.getProperties().entrySet()) { + if (!entry.getKey().isDynamic()) { + continue; + } + + String key = entry.getKey().getName(); + String value = entry.getValue() + .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") + .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); + + StringBuilder sb = new StringBuilder(); + sb.append("function ") + .append(key) + .append("() { ") + .append(value) + .append(" } \n"); + sb.append("var record_") + .append(key) + .append(" = new com.hurence.logisland.record.StandardRecord()") + .append(".setId(\"") + .append(key) + .append("\");\n"); + sb.append("record_") + .append(key) + .append(".setField( ") + .append("\"record_value\",") + .append(" com.hurence.logisland.record.FieldType.DOUBLE,") + .append(key) + .append("());\n"); + + dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); + System.out.println(sb.toString()); + logger.debug(sb.toString()); + } + } + + @Override + public Collection process(ProcessContext context, Collection records) { + + // check if we need initialization + if (datastoreClientService == null) { + init(context); + } + + List outputRecords = new ArrayList<>(); + for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { + + + try { + sandbox.eval(entry.getValue()); + Record cached = (Record) sandbox.get("record_" + entry.getKey()); + outputRecords.add(cached); + } catch (ScriptException e) { + Record errorRecord = new StandardRecord(RecordDictionary.ERROR) + .setId(entry.getKey()) + .addError("ScriptException", e.getMessage()); + outputRecords.add(errorRecord); + logger.error(e.toString()); + } + } + + return outputRecords; + } +} \ No newline at end of file diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java new file mode 100644 index 000000000..7fb62ce54 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.component.InitializationException; +import com.hurence.logisland.processor.datastore.MockDatastoreService; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.FieldType; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.service.datastore.DatastoreClientService; +import com.hurence.logisland.util.runner.TestRunner; +import com.hurence.logisland.util.runner.TestRunners; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +public class CheckThresholdsTest { + + @Test + public void testMultipleRules() throws InitializationException { + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(CheckThresholds.class); + runner.setProperty(ComputeTags.MAX_CPU_TIME, "100"); + runner.setProperty(ComputeTags.MAX_MEMORY, "12800000"); + runner.setProperty(ComputeTags.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(ComputeTags.ALLOw_NO_BRACE, "false"); + runner.setProperty("tvib1","cache(\"cached_id1\").value > 10.0"); + runner.setProperty("tvib2", "cache(\"cached_id2\").value >= 0"); + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + final DatastoreClientService lookupService = runner.getProcessContext() + .getPropertyValue(ComputeTags.DATASTORE_CLIENT_SERVICE) + .asControllerService(MockDatastoreService.class); + + Collection recordsToEnrich = getRecords(); + runner.assertValid(); + runner.enqueue(recordsToEnrich); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(2); + runner.assertOutputErrorCount(0); + + for (Record enriched : runner.getOutputRecords()) { + if (enriched.getId().equals("tvib1")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asString(), "cache(\"cached_id1\").value > 10.0"); + } else if (enriched.getId().equals("tvib2")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asString(), "cache(\"cached_id2\").value >= 0"); + } + } + } + + + private Collection getRecords() { + Collection recordsToEnrich = new ArrayList<>(); + + recordsToEnrich.add(new StandardRecord() + .setId("id1") + .setField("a", FieldType.STRING, "a1") + .setField("b", FieldType.STRING, "b1") + .setField("c", FieldType.LONG, 1)); + + recordsToEnrich.add(new StandardRecord() + .setId("id2") + .setField("a", FieldType.STRING, "a2") + .setField("b", FieldType.STRING, "b2") + .setField("c", FieldType.LONG, 2)); + + recordsToEnrich.add(new StandardRecord() + .setId("id3") + .setField("a", FieldType.STRING, "a3") + .setField("b", FieldType.STRING, "b3") + .setField("c", FieldType.LONG, 3)); + return recordsToEnrich; + } + + + private Collection getCacheRecords() { + Collection lookupRecords = new ArrayList<>(); + + lookupRecords.add(new StandardRecord() + .setId("cached_id1") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 12.45)); + + lookupRecords.add(new StandardRecord() + .setId("cached_id2") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 2.5)); + + return lookupRecords; + } +} diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index 0f3fd670a..ca38d43c0 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -41,20 +41,20 @@ public void testMultipleRules() throws InitializationException { final DatastoreClientService service = new MockDatastoreService(); getCacheRecords().forEach(r -> service.put("test", r, false)); - final TestRunner runner = TestRunners.newTestRunner(ComputeTag.class); - runner.setProperty(ComputeTag.MAX_CPU_TIME, "100"); - runner.setProperty(ComputeTag.MAX_MEMORY, "12800000"); - runner.setProperty(ComputeTag.MAX_PREPARED_STATEMENTS, "100"); - runner.setProperty(ComputeTag.ALLOw_NO_BRACE, "false"); + final TestRunner runner = TestRunners.newTestRunner(ComputeTags.class); + runner.setProperty(ComputeTags.MAX_CPU_TIME, "100"); + runner.setProperty(ComputeTags.MAX_MEMORY, "12800000"); + runner.setProperty(ComputeTags.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(ComputeTags.ALLOw_NO_BRACE, "false"); runner.setProperty("cvib3", "return 37.2/10*3;"); runner.setProperty("cvib2", "if( cache(\"cached_id2\").value > 10 ) return 0.0; else return 1.0;"); runner.setProperty("cvib1", "return cache(\"cached_id1\").value * 10.2;"); - runner.setProperty(ComputeTag.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); runner.addControllerService(service.getIdentifier(), service); runner.enableControllerService(service); final DatastoreClientService lookupService = runner.getProcessContext() - .getPropertyValue(ComputeTag.DATASTORE_CLIENT_SERVICE) + .getPropertyValue(ComputeTags.DATASTORE_CLIENT_SERVICE) .asControllerService(MockDatastoreService.class); Collection recordsToEnrich = getRecords(); @@ -84,20 +84,20 @@ public void testBadRules() throws InitializationException { final DatastoreClientService service = new MockDatastoreService(); getCacheRecords().forEach(r -> service.put("test", r, false)); - final TestRunner runner = TestRunners.newTestRunner(ComputeTag.class); - runner.setProperty(ComputeTag.MAX_CPU_TIME, "100"); - runner.setProperty(ComputeTag.MAX_MEMORY, "12800000"); - runner.setProperty(ComputeTag.MAX_PREPARED_STATEMENTS, "100"); - runner.setProperty(ComputeTag.ALLOw_NO_BRACE, "false"); + final TestRunner runner = TestRunners.newTestRunner(ComputeTags.class); + runner.setProperty(ComputeTags.MAX_CPU_TIME, "100"); + runner.setProperty(ComputeTags.MAX_MEMORY, "12800000"); + runner.setProperty(ComputeTags.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(ComputeTags.ALLOw_NO_BRACE, "false"); runner.setProperty("cvib3", "return 37.2/++10*3;"); /* runner.setProperty("cvib2", "if( cache(\"cached_id2\").value > 10 ) return 0.0; else return 1.0;"); runner.setProperty("cvib1", "return cache(\"cached_id1\").value * 10.2;");*/ - runner.setProperty(ComputeTag.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); runner.addControllerService(service.getIdentifier(), service); runner.enableControllerService(service); final DatastoreClientService lookupService = runner.getProcessContext() - .getPropertyValue(ComputeTag.DATASTORE_CLIENT_SERVICE) + .getPropertyValue(ComputeTags.DATASTORE_CLIENT_SERVICE) .asControllerService(MockDatastoreService.class); Collection recordsToEnrich = getRecords(); From ca4b78cc82e6a1edd02dd9f57944e4f7ab53925c Mon Sep 17 00:00:00 2001 From: arnou Date: Wed, 30 May 2018 11:01:01 +0200 Subject: [PATCH 15/63] Add support for Expression language in the AddFields processor --- .../pom.xml | 4 + .../logisland/processor/AddFields.java | 4 +- .../logisland/processor/AddFieldsTest.java | 115 ++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/logisland-plugins/logisland-common-processors-plugin/pom.xml b/logisland-plugins/logisland-common-processors-plugin/pom.xml index b6f5b528e..9cd136bd4 100644 --- a/logisland-plugins/logisland-common-processors-plugin/pom.xml +++ b/logisland-plugins/logisland-common-processors-plugin/pom.xml @@ -29,6 +29,10 @@ jar + + com.hurence.logisland + logisland-scripting-mvel + com.hurence.logisland logisland-api diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/AddFields.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/AddFields.java index 036b97392..c10e662af 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/AddFields.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/AddFields.java @@ -66,7 +66,7 @@ public List getSupportedPropertyDescriptors() { protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) { return new PropertyDescriptor.Builder() .name(propertyDescriptorName) - .expressionLanguageSupported(false) + .expressionLanguageSupported(true) .addValidator(StandardValidators.COMMA_SEPARATED_LIST_VALIDATOR) .required(false) .dynamic(true) @@ -91,7 +91,7 @@ private void updateRecord(ProcessContext context, Record record, Map { - final String defaultValueToAdd = fieldsNameMapping.get(addedFieldName); + final String defaultValueToAdd = context.getPropertyValue(addedFieldName).evaluate(record).asString(); // field is already here if (record.hasField(addedFieldName)) { if (conflictPolicy.equals(OVERWRITE_EXISTING.getValue())) { diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java index db41e5970..ebccdcb94 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java @@ -190,4 +190,119 @@ public void testAddWithConflictKeepOnlyOld() { out.assertFieldEquals("string2", "value2"); } + + @Test + public void testMultipleFieldsWithExpressionLanguage() { + + Record record1 = new StandardRecord(); + record1.setField("string1", FieldType.STRING, "value1"); + record1.setField("string2", FieldType.STRING, "value_2G"); + record1.setField("string3", FieldType.STRING, "value3"); + + TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); + testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); + testRunner.setProperty("stringEL1", "${'string1:'+string1+',string2:'+string2}"); + testRunner.setProperty("stringEL2", "" + + "${if( string1 == 'value11' || string1 == 'value12') return 'string1'; " + + "else if ( string2 contains '_' ) return 'string2';}"); + testRunner.assertValid(); + testRunner.enqueue(record1); + testRunner.run(); + testRunner.assertAllInputRecordsProcessed(); + testRunner.assertOutputRecordsCount(1); + + MockRecord out = testRunner.getOutputRecords().get(0); + out.assertFieldEquals("stringEL1", "string1:value1,string2:value_2G"); + out.assertFieldEquals("stringEL2", "string2"); + out.assertFieldTypeEquals("stringEL1", FieldType.STRING); + } + + @Test + public void testMultipleFieldsWithComplexExpressionLanguageNonNullValues() { + + Record record1 = new StandardRecord(); + record1.setField("ImportanceCode", FieldType.STRING, "9003"); + record1.setField("B2BUnit", FieldType.STRING, "12_54"); + record1.setField("libelle_zone", FieldType.STRING, "EST"); + + TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); + testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); + testRunner.setProperty("category", "" + + "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'affiliates'; if ( B2BUnit contains '_' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + testRunner.assertValid(); + testRunner.enqueue(record1); + testRunner.run(); + testRunner.assertAllInputRecordsProcessed(); + testRunner.assertOutputRecordsCount(1); + + MockRecord out = testRunner.getOutputRecords().get(0); + out.assertFieldEquals("category", "affiliates"); + } + + @Test + public void testMultipleFieldsWithComplex2ExpressionLanguageNonNullValues() { + + Record record1 = new StandardRecord(); + record1.setField("ImportanceCode", FieldType.STRING, "9008"); + record1.setField("B2BUnit", FieldType.STRING, "12*_*54"); + record1.setField("libelle_zone", FieldType.STRING, "EST"); + + TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); + testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); + testRunner.setProperty("category", "" + + "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'affiliates'; if ( B2BUnit contains '*_*' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + testRunner.assertValid(); + testRunner.enqueue(record1); + testRunner.run(); + testRunner.assertAllInputRecordsProcessed(); + testRunner.assertOutputRecordsCount(1); + + MockRecord out = testRunner.getOutputRecords().get(0); + out.assertFieldEquals("category", "marketplace"); + } + + @Test + public void testMultipleFieldsWithComplexExpressionLanguageWithEmptyValues() { + + Record record1 = new StandardRecord(); + record1.setField("ImportanceCode", FieldType.STRING, ""); + record1.setField("B2BUnit", FieldType.STRING, ""); + record1.setField("libelle_zone", FieldType.STRING, ""); + + TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); + testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); + testRunner.setProperty("category", "" + + "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'affiliates'; if ( B2BUnit contains '_' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + testRunner.assertValid(); + testRunner.enqueue(record1); + testRunner.run(); + testRunner.assertAllInputRecordsProcessed(); + testRunner.assertOutputRecordsCount(1); + + MockRecord out = testRunner.getOutputRecords().get(0); + out.assertFieldEquals("category", "subsidiaries"); + } + + + @Test + public void testMultipleFieldsWithComplexExpressionLanguageWithNullValues() { + + Record record1 = new StandardRecord(); + record1.setField("ImportanceCode", FieldType.STRING, null); + record1.setField("B2BUnit", FieldType.STRING, null); + record1.setField("libelle_zone", FieldType.STRING, null); + + TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); + testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); + testRunner.setProperty("category", "" + + "${if ( ImportanceCode != null && (ImportanceCode == '9003' || ImportanceCode == '9004')) return 'affiliates'; if ( B2BUnit contains '_' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + testRunner.assertValid(); + testRunner.enqueue(record1); + testRunner.run(); + testRunner.assertAllInputRecordsProcessed(); + testRunner.assertOutputRecordsCount(1); + + MockRecord out = testRunner.getOutputRecords().get(0); + out.assertFieldEquals("category", "subsidiaries"); + } } From 922a283ef83aaabba555f03c5823bf417d955dd8 Mon Sep 17 00:00:00 2001 From: arnou Date: Wed, 30 May 2018 11:07:59 +0200 Subject: [PATCH 16/63] Upd tests --- .../logisland/processor/AddFieldsTest.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java index ebccdcb94..788215810 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/AddFieldsTest.java @@ -222,13 +222,13 @@ public void testMultipleFieldsWithComplexExpressionLanguageNonNullValues() { Record record1 = new StandardRecord(); record1.setField("ImportanceCode", FieldType.STRING, "9003"); - record1.setField("B2BUnit", FieldType.STRING, "12_54"); + record1.setField("ClientID", FieldType.STRING, "12_54"); record1.setField("libelle_zone", FieldType.STRING, "EST"); TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); testRunner.setProperty("category", "" + - "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'affiliates'; if ( B2BUnit contains '_' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'A'; if ( ClientID contains '_' ) return 'M'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'I';}"); testRunner.assertValid(); testRunner.enqueue(record1); testRunner.run(); @@ -236,7 +236,7 @@ public void testMultipleFieldsWithComplexExpressionLanguageNonNullValues() { testRunner.assertOutputRecordsCount(1); MockRecord out = testRunner.getOutputRecords().get(0); - out.assertFieldEquals("category", "affiliates"); + out.assertFieldEquals("category", "A"); } @Test @@ -244,13 +244,13 @@ public void testMultipleFieldsWithComplex2ExpressionLanguageNonNullValues() { Record record1 = new StandardRecord(); record1.setField("ImportanceCode", FieldType.STRING, "9008"); - record1.setField("B2BUnit", FieldType.STRING, "12*_*54"); + record1.setField("ClientID", FieldType.STRING, "12*_*54"); record1.setField("libelle_zone", FieldType.STRING, "EST"); TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); testRunner.setProperty("category", "" + - "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'affiliates'; if ( B2BUnit contains '*_*' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'A'; if ( ClientID contains '*_*' ) return 'M'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'I';}"); testRunner.assertValid(); testRunner.enqueue(record1); testRunner.run(); @@ -258,21 +258,21 @@ public void testMultipleFieldsWithComplex2ExpressionLanguageNonNullValues() { testRunner.assertOutputRecordsCount(1); MockRecord out = testRunner.getOutputRecords().get(0); - out.assertFieldEquals("category", "marketplace"); + out.assertFieldEquals("category", "M"); } @Test public void testMultipleFieldsWithComplexExpressionLanguageWithEmptyValues() { Record record1 = new StandardRecord(); - record1.setField("ImportanceCode", FieldType.STRING, ""); - record1.setField("B2BUnit", FieldType.STRING, ""); + record1.setField("ImportantFlag", FieldType.STRING, ""); + record1.setField("ClientID", FieldType.STRING, ""); record1.setField("libelle_zone", FieldType.STRING, ""); TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); testRunner.setProperty("category", "" + - "${if( ImportanceCode == '9003' || ImportanceCode == '9004') return 'affiliates'; if ( B2BUnit contains '_' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + "${if( ImportantFlag == '9003' || ImportantFlag == '9004') return 'A'; if ( ClientID contains '_' ) return 'M'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'I';}"); testRunner.assertValid(); testRunner.enqueue(record1); testRunner.run(); @@ -288,14 +288,14 @@ public void testMultipleFieldsWithComplexExpressionLanguageWithEmptyValues() { public void testMultipleFieldsWithComplexExpressionLanguageWithNullValues() { Record record1 = new StandardRecord(); - record1.setField("ImportanceCode", FieldType.STRING, null); - record1.setField("B2BUnit", FieldType.STRING, null); - record1.setField("libelle_zone", FieldType.STRING, null); + record1.setField("ImportantFlag", FieldType.STRING, null); + record1.setField("ClientID", FieldType.STRING, null); + record1.setField("zone", FieldType.STRING, null); TestRunner testRunner = TestRunners.newTestRunner(new AddFields()); testRunner.setProperty(NormalizeFields.CONFLICT_RESOLUTION_POLICY, NormalizeFields.OVERWRITE_EXISTING); testRunner.setProperty("category", "" + - "${if ( ImportanceCode != null && (ImportanceCode == '9003' || ImportanceCode == '9004')) return 'affiliates'; if ( B2BUnit contains '_' ) return 'marketplace'; if ( libelle_zone != 'EST' && libelle_zone != 'OUEST' && libelle_zone != 'NORD' ) return 'subsidiaries'; else return 'integrated';}"); + "${if ( ImportantFlag != null && (ImportantFlag == '9003' || ImportantFlag == '9004')) return 'A'; if ( ClientID contains '_' ) return 'M'; if ( zone != 'EST' && zone != 'OUEST' && zone != 'NORD' ) return 'S'; else return 'I';}"); testRunner.assertValid(); testRunner.enqueue(record1); testRunner.run(); @@ -303,6 +303,6 @@ public void testMultipleFieldsWithComplexExpressionLanguageWithNullValues() { testRunner.assertOutputRecordsCount(1); MockRecord out = testRunner.getOutputRecords().get(0); - out.assertFieldEquals("category", "subsidiaries"); + out.assertFieldEquals("category", "S"); } } From 55b74f57f4c12b50ecd2f0d70827306f1850ab37 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 30 May 2018 16:22:58 +0200 Subject: [PATCH 17/63] first API version --- .../engine/spark/remote/RemoteApiClient.java | 16 +- .../src/main/resources/api.yaml | 202 +++++++++++++++--- .../spark/BaseStreamProcessingEngine.scala | 19 +- .../spark/KafkaStreamProcessingEngine.scala | 5 +- .../RemoteApiStreamProcessingEngine.scala | 130 ++++++++++- .../spark/AbstractKafkaRecordStream.scala | 2 +- .../stream/spark/DummyRecordStream.scala | 70 ++++++ .../logisland/engine/RemoteApiEngineTest.java | 89 ++++++++ .../spark/remote/RemoteApiClientTest.java | 13 +- .../src/test/resources/conf/remote-engine.yml | 79 +++++++ 10 files changed, 566 insertions(+), 59 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index 24f528f3e..76751a646 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -35,9 +35,9 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import java.time.Duration; -import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -104,7 +104,7 @@ public RemoteApiClient(String baseUrl, Duration socketTimeout, Duration connectT * * @return a list of {@link Pipeline} (never null). Empty in case of error or no results. */ - public List fetchPipelines() { + public Optional> fetchPipelines() { Request request = new Request.Builder() .url(baseUrl.newBuilder().addPathSegment(PIPELINES_RESOURCE_URI).build()) .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) @@ -113,17 +113,17 @@ public List fetchPipelines() { try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { logger.error("Error refreshing pipelines from remote server. Got code {}", response.code()); - + } else { + List ret = mapper.readValue(response.body().byteStream(), pipelineType); + //validate against javax.validation annotations. + ret.forEach(RemoteApiClient::doValidate); + return Optional.of(ret); } - List ret = mapper.readValue(response.body().byteStream(), pipelineType); - //validate against javax.validation annotations. - ret.forEach(RemoteApiClient::doValidate); - return ret; } catch (Exception e) { logger.error("Unable to refresh pipelines from remote server", e); } - return Collections.emptyList(); + return Optional.empty(); } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml index 2aecbdca4..d3ca5841b 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml @@ -1,13 +1,32 @@ swagger: '2.0' info: + version: v1 + title: Logisland standard API description: >- - REST API for logisland. + The Logisland REST API for third party applications. - The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to trigger configuration changes. + The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to: + + - Ask for configuration changes to be triggered. + + - Report the latest configuration applied (to ease up resynchronisation and business continuity). + + In terms of APIs, two degrees of freedom are possible: + + - Dataflows (mandatory): + + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). + + Use the dataflow api if you want to keep a high level point of view on your data operations. In this case, every stream created in a dataflow box will be destroyed if any change occours inside the stream itself (e.g. A processor configuration change.) + + + - Streams (optional): + Obviously, you can have a finer-grained control of what is going on inside your streams! + In case you marked your dataflow as mutable Logisland will poll as well on the streams API endpoint in order to know if any stream has been affected by some configuration change. + + In this case, the processor chain will be dynamically reconfigured without the need of stopping the stream and restarting a new one. - version: v1 - title: Logisland standard API contact: name: Hurence email: support@hurence.com @@ -20,14 +39,106 @@ consumes: produces: - application/json paths: + /{jobId}/dataflows: + get: + tags: + - dataflows + operationId: pollActiveDataflows + summary: Retrieves the data flow to run. + description: >- + A dataflow is a set of services and streams allowing a data flowing from one or more sources, + being transformed and reach one or more destinations (sinks). + parameters: + - name: jobId + in: path + type: string + required: true + description: logisland job id (aka the engine name) + responses: + "200": + description: >- + should return every pipeline that should be running. + On server side, logisland will do the delta and apply the following: + + - Add a new dataflow if any provided is not already running. + In this case streams and services will be created. + + - Fully reconfigure a dataflow (stop and then start) if another one with + the same name is already running but its lastUpdated is older than the one provided. + + In this case be aware that old stream and services will be destroyed and + new ones will be created. + + - Remove a pipeline if running but no more present in returned ones. + schema: + type: array + items: + $ref: '#/definitions/DataFlow' + "304": + description: | + Nothing has been modified since the last call. + + In this case the body content will be completely ignored + (hence the server can answer with an empty body to save network and resources). + + "404": + description: Not found (the server probably does not handle this job) + default : + description: Unexpected error + post: + tags: + - dataflows + operationId: updateCurrentConfiguration + summary: Communicates the configuration of running dataflows. + description: >- + In order to ensure business continuity, Logisland will regularly contact the third party application in order to communicate a snapshot of the current configuration. + + This service can be seen as well as a ping. + parameters: + - name: jobId + in: path + type: string + required: true + description: logisland job id (aka the engine name) + - in: body + name: dataflows + required: true + schema: + type: array + items: + $ref: '#/definitions/DataFlow' + responses: + default : + description: | + The server should return HTTP 200 OK. + By the way, the response is ignored by Logisland since the operation + has a fire and forget nature. + - /pipelines: + /{jobId}/dataflows/{dataflowId}/streams/: get: tags: - - pipelines - operationId: pollActivePipelines - summary: Retrieves the streaming pipelines to run. + - streams + operationId: pollStreamConfigurationChanges + summary: Retrieves the description: Logisland will poll this API in order to start, reconfigure or stop the pipelines according to the received response. + parameters: + - name: jobId + in: path + type: string + required: true + description: logisland job id (aka the engine name) + - name: dataflowId + in: path + type: string + required: true + description: The name of the dataflow + - name: streamId + in: path + type: string + required: true + description: the name of the stream + responses: "200": description: >- @@ -36,15 +147,24 @@ paths: - Add a new pipeline if any provided is not already running. - - Reconfigure a pipeline (stop and then start) if same pipiline already running and but its lastUpdated is older than the one provided + - Reconfigure a pipeline (stop and then start) if same pipeline already running and but its lastUpdated is older than the one provided - Remove a pipeline if running but no more present in returned ones. schema: type: array items: - $ref: '#/definitions/Pipeline' - default: - description: unexpected error + $ref: '#/definitions/StreamConfiguration' + "304": + description: | + Nothing has been modified since the last call. + + In this case the body content will be completely ignored + (hence the server can answer with an empty body to save network and resources). + + "404": + description: Not found (the server probably does not handle this job) + default : + description: Unexpected error definitions: @@ -79,8 +199,6 @@ definitions: items: $ref: '#/definitions/Property' - - Service: type: object description: A logisland 'controller service'. @@ -90,7 +208,7 @@ definitions: Processor: type: object - description: A logisland 'controller service'. + description: A logisland 'processor'. allOf: - $ref: '#/definitions/Component' @@ -99,14 +217,23 @@ definitions: allOf: - $ref: '#/definitions/Component' - properties: + mutable: + type: boolean + description: >- + if the stream is mutable, logisland will poll for changes. + + See API /{jobId}/dataflows/{dataflowId}/stream/{streamId} for + more information. + + default: false processors: type: array items: $ref: '#/definitions/Processor' - Pipeline: + Versioned: type: object - description: A streaming pipeline. + description: a versioned component properties: name: type: string @@ -115,18 +242,37 @@ definitions: type: string format: date-time description: the last modified timestamp of this pipeline (used to trigger changes). - services: - type: array - description: The service controllers. - items: - $ref: '#/definitions/Service' - streams: - type: array - description: The engine properties. - minItems: 1 - items: - $ref: '#/definitions/Stream' required: - name - lastModified + StreamConfiguration: + type: object + description: Tracks versioned stream configurations. + allOf: + - $ref: '#/definitions/Versioned' + - properties: + processors: + type: array + items: + $ref: '#/definitions/Processor' + + + DataFlow: + type: object + description: A streaming pipeline. + allOf: + - $ref: "#/definitions/Versioned" + - properties: + services: + type: array + description: The service controllers. + items: + $ref: '#/definitions/Service' + streams: + type: array + description: The engine properties. + minItems: 1 + items: + $ref: '#/definitions/Stream' + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala index a31b5ba28..1ffd744ee 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala @@ -23,7 +23,6 @@ import java.util.regex.Pattern import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} -import com.hurence.logisland.stream.StreamContext import com.hurence.logisland.stream.spark.SparkRecordStream import com.hurence.logisland.util.spark.SparkUtils import com.hurence.logisland.validator.StandardValidators @@ -330,6 +329,20 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { } + /** + * Called after the engine has been started. + * + * @param engineContext + */ + protected def onStart(engineContext: EngineContext) : Unit = {} + + /** + * Called before the engine is being stopped. + * + * @param engineContext + */ + protected def onStop(engineContext: EngineContext) : Unit = {} + /** * start the engine * @@ -352,6 +365,7 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { } streamingContext.start() + onStart(engineContext) if (timeout != -1) streamingContext.awaitTerminationOrTimeout(timeout) else streamingContext.awaitTermination() @@ -365,7 +379,7 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { * @param sparkConf the preinitialized configuration. * @param engineContext the engine context. */ - protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit + protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit final def createStreamingContext(engineContext: EngineContext): StreamingContext = { @@ -453,6 +467,7 @@ abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { final override def shutdown(engineContext: EngineContext) = { logger.info(s"shutting down Spark engine") + onStop(engineContext) engineContext.getStreamContexts foreach (streamingContext => { try { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index b26977911..8f841348b 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -55,6 +55,7 @@ class KafkaStreamProcessingEngine extends BaseStreamProcessingEngine { private val logger = LoggerFactory.getLogger(classOf[KafkaStreamProcessingEngine]) + override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { val descriptors: util.List[PropertyDescriptor] = new util.ArrayList[PropertyDescriptor] descriptors.addAll(super.getSupportedPropertyDescriptors) @@ -64,13 +65,13 @@ class KafkaStreamProcessingEngine extends BaseStreamProcessingEngine { } - override def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { + override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) } - override def setupStreamingContexts(engineContext: EngineContext, ssc: StreamingContext): Unit = { + override protected def setupStreamingContexts(engineContext: EngineContext, ssc: StreamingContext): Unit = { val appName = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_APP_NAME).asString engineContext.getStreamContexts.asScala.foreach(streamContext => { try { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala index 499b3295d..4e74765f1 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -17,28 +17,136 @@ package com.hurence.logisland.engine.spark -import com.hurence.logisland.engine.{EngineContext, ProcessingEngine} -import org.apache.spark.SparkConf +import java.time.Duration +import java.util +import java.util.Collections +import java.util.concurrent.{Executors, TimeUnit} + +import com.hurence.logisland.component.PropertyDescriptor +import com.hurence.logisland.engine.EngineContext +import com.hurence.logisland.engine.spark.remote.{RemoteApiClient, RemoteComponentRegistry} +import com.hurence.logisland.stream.StandardStreamContext +import com.hurence.logisland.stream.spark.DummyRecordStream +import com.hurence.logisland.validator.StandardValidators import org.apache.spark.streaming.StreamingContext +import org.slf4j.LoggerFactory + +object RemoteApiStreamProcessingEngine { + val REMOTE_API_BASE_URL = new PropertyDescriptor.Builder() + .name("remote.api.baseUrl") + .description("The base URL of the remote server providing logisland configuration") + .required(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build + + val REMOTE_API_POLLING_RATE = new PropertyDescriptor.Builder() + .name("remote.api.polling.rate") + .description("Remote api polling rate in milliseconds") + .required(true) + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .build + + val REMOTE_API_CONNECT_TIMEOUT = new PropertyDescriptor.Builder() + .name("remote.api.timeouts.connect") + .description("Remote api connection timeout in milliseconds") + .required(false) + .defaultValue("10000") + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .build -class RemoteApiStreamProcessingEngine(processingEngine: ProcessingEngine) extends BaseStreamProcessingEngine { + val REMOTE_API_SOCKET_TIMEOUT = new PropertyDescriptor.Builder() + .name("remote.api.timeouts.socket") + .description("Remote api default read/write socket timeout in milliseconds") + .required(false) + .defaultValue("10000") + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .build + + val REMOTE_API_USER = new PropertyDescriptor.Builder() + .name("remote.api.auth.user") + .description("The basic authentication user for the remote api endpoint.") + .required(false) + .build + + val REMOTE_API_PASSWORD = new PropertyDescriptor.Builder() + .name("remote.api.auth.password") + .description("The basic authentication password for the remote api endpoint.") + .required(false) + .build +} +class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { + private val logger = LoggerFactory.getLogger(classOf[RemoteApiStreamProcessingEngine]) + private val executor = Executors.newSingleThreadScheduledExecutor() + private var remoteApiRegistry: RemoteComponentRegistry = _ + override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { + val ret = new util.ArrayList(super.getSupportedPropertyDescriptors) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_BASE_URL) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_CONNECT_TIMEOUT) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_USER) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_PASSWORD) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_SOCKET_TIMEOUT) + return Collections.unmodifiableList(ret) + } + + override protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit = { + if (!engineContext.getControllerServiceConfigurations.isEmpty) { + logger.warn("This engine will not load service controllers from the configuration file!") + engineContext.getControllerServiceConfigurations.clear() + } + if (!engineContext.getStreamContexts.isEmpty()) { + logger.warn("This engine will not handle streams from the configuration file!") + engineContext.getStreamContexts.clear() + } + engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")); + super.setupStreamingContexts(engineContext, scc) + remoteApiRegistry = new RemoteComponentRegistry(engineContext); + + } + /** - * Hook to customize spark configuration before creating a spark context. + * Called after the engine has been started. * - * @param sparkConf the preinitialized configuration. - * @param engineContext the engine context. + * @param engineContext */ - override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = ??? + override protected def onStart(engineContext: EngineContext): Unit = { + super.onStart(engineContext) + val remoteApiClient = new RemoteApiClient( + engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_BASE_URL), + Duration.ofMillis(engineContext.getPropertyValue(RemoteApiStreamProcessingEngine.REMOTE_API_SOCKET_TIMEOUT).asLong()), + Duration.ofMillis(engineContext.getPropertyValue(RemoteApiStreamProcessingEngine.REMOTE_API_CONNECT_TIMEOUT).asLong()), + engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_USER), + engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_PASSWORD)) + + implicit def funToRunnable(fun: () => Unit) = new Runnable() { + def run() = fun() + } + + executor.scheduleWithFixedDelay(() => { + val pipelines = remoteApiClient.fetchPipelines() + if (pipelines.isPresent) { + remoteApiRegistry.updateEngineContext(pipelines.get()) + } + + }, 0, engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE).toInt, + TimeUnit.MILLISECONDS) + } /** - * Override to setup streaming context before starting them. + * Called before the engine is being stopped. * - * @param engineContext the engine context. - * @param scc the spark streaming context. + * @param engineContext */ - override protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit = ??? + override protected def onStop(engineContext: EngineContext): Unit = { + super.onStop(engineContext) + executor.shutdown() + //stop everything started from remote side. + if (remoteApiRegistry != null) { + remoteApiRegistry.updateEngineContext(Collections.emptyList()) + } + } } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala index 8366cbb0f..be349fb7f 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala @@ -89,7 +89,7 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark } - override def setup(appName: String, ssc: StreamingContext, streamContext: StreamContext, engineContext: EngineContext) = { + override def setup(appName: String, ssc: StreamingContext, streamContext: StreamContext, engineContext: EngineContext) = { this.appName = appName this.ssc = ssc this.streamContext = streamContext diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala new file mode 100644 index 000000000..46bb4123e --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala @@ -0,0 +1,70 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.stream.spark + +import java.util + +import com.hurence.logisland.component.PropertyDescriptor +import com.hurence.logisland.engine.EngineContext +import com.hurence.logisland.stream.{AbstractRecordStream, StreamContext} +import com.hurence.logisland.util.spark.SparkUtils +import org.apache.spark.storage.StorageLevel +import org.apache.spark.streaming.StreamingContext +import org.apache.spark.streaming.receiver.Receiver + +class DummyRecordStream extends AbstractRecordStream with SparkRecordStream { + + private var streamingContext: StreamingContext = _ + + /** + * Allows subclasses to register which property descriptor objects are + * supported. + * + * @return PropertyDescriptor objects this processor currently supports + */ + override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { + return new util.ArrayList[PropertyDescriptor]() + } + + override def start(): Unit = { + val stream = streamingContext.receiverStream(new Receiver[Long](StorageLevel.NONE) { + override def onStart(): Unit = {} + + override def onStop(): Unit = {} + }) + stream.foreachRDD(rdd => { + //do nothing :) + }) + stream.start() + + } + + /** + * setup the stream with spark app properties + * + * @param appName + * @param ssc + * @param streamContext + */ + override def setup(appName: String, ssc: StreamingContext, streamContext: StreamContext, engineContext: EngineContext): Unit = { + streamingContext = ssc + SparkUtils.customizeLogLevels + } + + override def getStreamContext(): StreamingContext = streamingContext +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java new file mode 100644 index 000000000..ca2e3ef39 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java @@ -0,0 +1,89 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.hurence.logisland.engine; + +import com.hurence.logisland.component.ComponentFactory; +import com.hurence.logisland.config.ConfigReader; +import com.hurence.logisland.config.LogislandConfiguration; +import com.hurence.logisland.util.spark.SparkUtils; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + + +public class RemoteApiEngineTest { + private static Logger logger = LoggerFactory.getLogger(RemoteApiEngineTest.class); + + private static final String JOB_CONF_FILE = "/conf/remote-engine.yml"; + + @Test + @Ignore + public void remoteTest() { + + MockWebServer webServer = new MockWebServer(); + webServer.enqueue(new MockResponse().setBody("")); + + + logger.info("starting StreamProcessingRunner"); + + Optional engineInstance = Optional.empty(); + try { + + String configFile = RemoteApiEngineTest.class.getResource(JOB_CONF_FILE).getPath(); + + // load the YAML config + LogislandConfiguration sessionConf = ConfigReader.loadConfig(configFile); + + // instanciate engine and all the processor from the config + engineInstance = ComponentFactory.getEngineContext(sessionConf.getEngine()); + assert engineInstance.isPresent(); + assert engineInstance.get().isValid(); + + logger.info("starting Logisland session version {}", sessionConf.getVersion()); + logger.info(sessionConf.getDocumentation()); + } catch (Exception e) { + logger.error("unable to launch runner : {}", e); + } + + try { + // start the engine + final EngineContext engineContext = engineInstance.get(); + Executors.newSingleThreadScheduledExecutor().schedule(()->engineContext.getEngine().shutdown(engineContext), + 10, TimeUnit.SECONDS); + engineInstance.get().getEngine().start(engineContext); + SparkUtils.customizeLogLevels(); + } catch (Exception e) { + logger.error("something went bad while running the job : {}", e); + System.exit(-1); + } + + + + + + + } + +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java index 1899a0a71..b5b40b9e0 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java @@ -24,7 +24,6 @@ import org.junit.Assert; import org.junit.Test; -import javax.validation.ConstraintViolationException; import javax.ws.rs.core.HttpHeaders; import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -43,22 +42,22 @@ public void testAllUnsecured() throws Exception { mockWebServer.enqueue(new MockResponse().setBodyDelay(3, TimeUnit.SECONDS)); final String dummy = "\"name\":\"myName\", \"component\":\"myComponent\""; mockWebServer.enqueue(new MockResponse().setBody("[{" + dummy + ",\"lastModified\":\"1983-06-04T10:01:02Z\"," + - "\"streams\":[{" + dummy +"}]}]")); + "\"streams\":[{" + dummy + "}]}]")); RemoteApiClient client = createInstance(mockWebServer, null, null); - Assert.assertTrue(client.fetchPipelines().isEmpty()); - Assert.assertTrue(client.fetchPipelines().isEmpty()); - Assert.assertEquals(1, client.fetchPipelines().size()); + Assert.assertFalse(client.fetchPipelines().isPresent()); + Assert.assertFalse(client.fetchPipelines().isPresent()); + Assert.assertEquals(1, client.fetchPipelines().get().size()); } } - @Test(expected = ConstraintViolationException.class) + @Test public void testValidationFails() throws Exception { try (MockWebServer mockWebServer = new MockWebServer()) { mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\", \"lastModified\":\"1983-06-04T10:01:02Z\",\"services\":[{}],\"streams\":[{}]}]")); RemoteApiClient client = createInstance(mockWebServer, null, null); - client.fetchPipelines(); + Assert.assertFalse(client.fetchPipelines().isPresent()); } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml new file mode 100644 index 000000000..c27beae6f --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml @@ -0,0 +1,79 @@ +version: 0.13.0 +documentation: LogIsland remote controlled. + +engine: + component: com.hurence.logisland.engine.spark.RemoteApiStreamProcessingEngine + type: engine + documentation: Do some remote pipelines. + configuration: + spark.app.name: FutureFactory + spark.master: local[2] + spark.driver.memory: 512M + spark.driver.cores: 1 + spark.executor.memory: 512M + spark.executor.instances: 2 + spark.executor.cores: 2 + spark.yarn.queue: default + spark.yarn.maxAppAttempts: 4 + spark.yarn.am.attemptFailuresValidityInterval: 1h + spark.yarn.max.executor.failures: 20 + spark.yarn.executor.failuresValidityInterval: 1h + spark.task.maxFailures: 8 + spark.serializer: org.apache.spark.serializer.KryoSerializer + spark.streaming.batchDuration: 2000 + spark.streaming.backpressure.enabled: false + spark.streaming.blockInterval: 500 + spark.streaming.kafka.maxRatePerPartition: 10000 + spark.streaming.timeout: -1 + spark.streaming.unpersist: false + spark.streaming.kafka.maxRetries: 3 + spark.streaming.ui.retainedBatches: 200 + spark.streaming.receiver.writeAheadLog.enable: false + spark.ui.port: 4040 + remote.api.baseUrl: http://localhost:1234/api + remote.api.polling.rate: 5000 + + controllerServiceConfigurations: + + - controllerService: mqtt_service + component: com.hurence.logisland.stream.spark.structured.provider.MQTTStructuredStreamProviderService + configuration: + # mqtt.broker.url: tcp://51.15.164.141:1883 + mqtt.broker.url: tcp://localhost:1883 + mqtt.persistence: memory + mqtt.client.id: logisland + mqtt.qos: 0 + mqtt.topic: Account123/# + mqtt.username: User123 + mqtt.password: Kapu12345678+ + mqtt.clean.session: true + mqtt.connection.timeout: 30 + mqtt.keep.alive: 60 + mqtt.version: 3 + + - controllerService: console_service + component: com.hurence.logisland.stream.spark.structured.provider.ConsoleStructuredStreamProviderService + + streamConfigurations: + + # indexing stream + - stream: indexing_stream + component: com.hurence.logisland.stream.spark.structured.StructuredStream + configuration: + read.topics: /a/in + read.topics.serializer: com.hurence.logisland.serializer.KuraProtobufSerializer + read.topics.client.service: mqtt_service + write.topics: /a/out + write.topics.serializer: none + write.topics.client.service: console_service + processorConfigurations: + + - processor: flatten + component: com.hurence.logisland.processor.FlatMap + type: processor + documentation: "extract metrics from root record" + configuration: + keep.root.record: false + copy.root.record.fields: true + leaf.record.type: record_metric + concat.fields: record_name From c187b31c51501b7cb95e729d2e64ea709e70a6ef Mon Sep 17 00:00:00 2001 From: oalam Date: Wed, 30 May 2018 18:07:53 +0200 Subject: [PATCH 18/63] implement CheckAlerts processor --- .../logisland/record/RecordDictionary.java | 1 + .../processor/alerting/CheckAlerts.java | 169 ++++++++++++++++++ .../processor/alerting/CheckThresholds.java | 46 +++-- .../processor/alerting/ComputeTags.java | 7 +- .../processor/alerting/CheckAlertsTest.java | 124 +++++++++++++ 5 files changed, 328 insertions(+), 19 deletions(-) create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java create mode 100644 logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java b/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java index a957407b3..132f39504 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/RecordDictionary.java @@ -26,4 +26,5 @@ public class RecordDictionary { public static String TAG = "tag"; public static String COMPUTED_TAG = "computed_tag"; public static String THRESHOLD = "threshold"; + public static String ALERT = "alert"; } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java new file mode 100644 index 000000000..23a920a76 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.annotation.behavior.DynamicProperty; +import com.hurence.logisland.annotation.documentation.CapabilityDescription; +import com.hurence.logisland.annotation.documentation.Tags; +import com.hurence.logisland.component.PropertyDescriptor; +import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.RecordDictionary; +import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.validator.StandardValidators; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.script.ScriptException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@Tags({"record", "alerting", "thresholds", "opc", "tag"}) +@CapabilityDescription("Add one or more field with a default value") +@DynamicProperty(name = "field to add", + supportsExpressionLanguage = false, + value = "a default value", + description = "Add a field to the record with the default value") +public class CheckAlerts extends AbstractNashornSandboxProcessor { + + + public static final PropertyDescriptor PROFILE_ACTIVATION_CONDITION = new PropertyDescriptor.Builder() + .name("profile.activation.condition") + .description("A javascript expression that activates this alerting profile when true") + .required(false) + .defaultValue("0==0") + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + + @Override + public List getSupportedPropertyDescriptors() { + List properties = new ArrayList<>(super.getSupportedPropertyDescriptors()); + properties.add(PROFILE_ACTIVATION_CONDITION); + + return properties; + } + + + + /* + - processor: compute_alerts1 + component: com.hurence.logisland.processor.CheckAlertOnThresholds + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + cache.client.service: cache + default.record_type: alert + default.el.language: js + default.criticity.level: 1 + profile.activation.condition: cache("cvib1").value < 10.0 && cache("vib2").value > 2; + avib1: cache("tvib1").count > 5.0 * cache("cvib1"); + avib2: cache("tvib2").avg > 12.0; + avib3: cache("tvib2").duration > 12000.0; + */ + + private static final Logger logger = LoggerFactory.getLogger(CheckAlerts.class); + + + private String expandCode(String rawCode) { + /* return rawCode + .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") + .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()") + .replaceAll("\\.count", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_COUNT).asDouble()") + .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_AVG).asDouble()");*/ + + return rawCode.replaceAll("cache\\((\\S*)\\).value", "getValue($1)") + .replaceAll("cache\\((\\S*)\\).count", "getCount($1)") + .replaceAll("cache\\((\\S*)\\).duration", "getDuration($1)"); + } + + + @Override + protected void setupDynamicProperties(ProcessContext context) { + String profileActivationRule = context.getPropertyValue(PROFILE_ACTIVATION_CONDITION).asString(); + + StringBuilder sbActivation = new StringBuilder(); + sbActivation.append("var alert = false;\n") + .append("function getValue(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n") + .append("function getDuration(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(\"duration\").asDouble(); \n};\n") + .append("function getCount(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(\"count\").asDouble(); \n};\n") + .append("if( ") + .append(expandCode(profileActivationRule)) + .append(" ) { \n"); + + + for (final Map.Entry entry : context.getProperties().entrySet()) { + if (!entry.getKey().isDynamic()) { + continue; + } + + String key = entry.getKey().getName(); + String value = expandCode(entry.getValue()); + StringBuilder sb = new StringBuilder(sbActivation); + sb.append(" if( ") + .append(value) + .append(" ) { alert = true; }\n") + .append("}\n"); + + dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); + + System.out.println(sb.toString()); + logger.debug(sb.toString()); + } + + + } + + + @Override + public Collection process(ProcessContext context, Collection records) { + + // check if we need initialization + if (datastoreClientService == null) { + init(context); + } + + List outputRecords = new ArrayList<>(); + for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { + + try { + sandbox.eval(entry.getValue()); + Boolean alert = (Boolean) sandbox.get("alert"); + if (alert) { + outputRecords.add(new StandardRecord(RecordDictionary.ALERT) + .setId(entry.getKey()) + .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(entry.getKey()).asString())); + } + } catch (ScriptException e) { + Record errorRecord = new StandardRecord(RecordDictionary.ERROR) + .setId(entry.getKey()) + .addError("ScriptException", e.getMessage()); + outputRecords.add(errorRecord); + logger.error(e.toString()); + } + } + + return outputRecords; + } +} \ No newline at end of file diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java index 3aa3f1813..f3c5a1586 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java @@ -21,22 +21,18 @@ import com.hurence.logisland.annotation.documentation.Tags; import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.record.FieldDictionary; -import com.hurence.logisland.record.Record; -import com.hurence.logisland.record.RecordDictionary; -import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.record.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.script.ScriptException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -@Tags({"record", "fields", "Add"}) -@CapabilityDescription("Add one or more field with a default value\n" + - "...") +import java.util.*; + +@Tags({"record", "threshold", "tag", "alerting"}) +@CapabilityDescription("Compute threshold cross from given formulas.\n" + + " each dynamic property will return a new record according to the formula definition\n" + + " the record name will be set to the property name\n" + + " the record time will be set to the current timestamp") @DynamicProperty(name = "field to add", supportsExpressionLanguage = false, value = "a default value", @@ -103,15 +99,31 @@ public Collection process(ProcessContext context, Collection rec for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { - - try { sandbox.eval(entry.getValue()); Boolean match = (Boolean) sandbox.get("match"); if (match) { - outputRecords.add(new StandardRecord(RecordDictionary.THRESHOLD) - .setId(entry.getKey()) - .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(entry.getKey()).asString())); + + String key = entry.getKey(); + Record cachedThreshold = datastoreClientService.get("test", new StandardRecord().setId(key)); + if (cachedThreshold != null) { + + Long count = cachedThreshold.getField("count").asLong(); + Long duration = System.currentTimeMillis() - cachedThreshold.getField("first_record_time").asLong(); + cachedThreshold.setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) + .setField("count", FieldType.LONG, count + 1) + .setField("duration", FieldType.LONG, duration); + outputRecords.add(cachedThreshold); + } else { + Record threshold = new StandardRecord(RecordDictionary.THRESHOLD) + .setId(key) + .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) + .setField("count", FieldType.LONG, 1L) + .setField("first_record_time", FieldType.LONG, new Date().getTime()) + .setField("duration", FieldType.LONG, 0); + datastoreClientService.put("test", threshold, true); + outputRecords.add(threshold); + } } } catch (ScriptException e) { Record errorRecord = new StandardRecord(RecordDictionary.ERROR) diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java index 63b67d724..da60f17df 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java @@ -34,8 +34,11 @@ import java.util.Map; @Tags({"record", "fields", "Add"}) -@CapabilityDescription("Add one or more field with a default value\n" + - "...") +@CapabilityDescription("Compute tag cross from given formulas.\n" + + "- each dynamic property will return a new record according to the formula definition\n" + + "- the record name will be set to the property name\n" + + "- the record time will be set to the current timestamp\n\n" + + "a threshold_cross has the following properties : count, sum, avg, time, duration, value") @DynamicProperty(name = "field to add", supportsExpressionLanguage = false, value = "a default value", diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java new file mode 100644 index 000000000..c1aa3b450 --- /dev/null +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.processor.alerting; + +import com.hurence.logisland.component.InitializationException; +import com.hurence.logisland.processor.datastore.MockDatastoreService; +import com.hurence.logisland.record.FieldDictionary; +import com.hurence.logisland.record.FieldType; +import com.hurence.logisland.record.Record; +import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.service.datastore.DatastoreClientService; +import com.hurence.logisland.util.runner.TestRunner; +import com.hurence.logisland.util.runner.TestRunners; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collection; + +import static org.junit.Assert.assertEquals; + +public class CheckAlertsTest { + + @Test + public void testMultipleRules() throws InitializationException { + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(CheckAlerts.class); + runner.setProperty(CheckAlerts.MAX_CPU_TIME, "100"); + runner.setProperty(CheckAlerts.MAX_MEMORY, "12800000"); + runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); + runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); + runner.setProperty("avib1","cache(\"cached_id1\").value > 12.0"); + runner.setProperty("avib2","cache(\"cached_id2\").value < 5"); + runner.setProperty("avib3","cache(\"cached_id3\").value < 5"); + runner.setProperty("avib4","cache(\"noone\").value < 5"); + runner.setProperty("avib5","brousoufparty++++"); + + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + runner.assertValid(); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(2); + runner.assertOutputErrorCount(2); + + for (Record enriched : runner.getOutputRecords()) { + if (enriched.getId().equals("avib1")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asString(), "cache(\"cached_id1\").value > 12.0"); + } + } + } + + @Test + public void testMultipleThresholds() throws InitializationException { + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(CheckAlerts.class); + runner.setProperty(CheckAlerts.MAX_CPU_TIME, "100"); + runner.setProperty(CheckAlerts.MAX_MEMORY, "12800000"); + runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); + runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); + runner.setProperty("avib1","cache(\"cached_id1\").value > 12.0"); + + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + runner.assertValid(); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(2); + runner.assertOutputErrorCount(2); + + for (Record enriched : runner.getOutputRecords()) { + if (enriched.getId().equals("avib1")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asString(), "cache(\"cached_id1\").value > 12.0"); + } + } + } + + + + private Collection getCacheRecords() { + Collection lookupRecords = new ArrayList<>(); + + lookupRecords.add(new StandardRecord() + .setId("cached_id1") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 12.45)); + + lookupRecords.add(new StandardRecord() + .setId("cached_id2") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 2.5)); + + lookupRecords.add(new StandardRecord() + .setId("cached_id3") + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 5.5)); + + return lookupRecords; + } +} From 9619ad56da544b8099c90c6506d6fb3e16108c81 Mon Sep 17 00:00:00 2001 From: oalam Date: Thu, 31 May 2018 11:54:14 +0200 Subject: [PATCH 19/63] improve documentation for tutorials prerequisites --- logisland-documentation/developer.rst | 2 +- .../tutorials/prerequisites.rst | 90 ++----------------- .../main/resources/conf/docker-compose.yml | 2 + 3 files changed, 12 insertions(+), 82 deletions(-) diff --git a/logisland-documentation/developer.rst b/logisland-documentation/developer.rst index 76ffccd5c..3e6b9c2f0 100644 --- a/logisland-documentation/developer.rst +++ b/logisland-documentation/developer.rst @@ -222,7 +222,7 @@ Publish release assets to github please refer to `https://developer.github.com/v3/repos/releases `_ -curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/8905079/assets?name=logisland-0.13.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' +curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.13.0/assets?name=logisland-0.13.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' diff --git a/logisland-documentation/tutorials/prerequisites.rst b/logisland-documentation/tutorials/prerequisites.rst index b225aeefc..596e3d85e 100644 --- a/logisland-documentation/tutorials/prerequisites.rst +++ b/logisland-documentation/tutorials/prerequisites.rst @@ -11,92 +11,20 @@ There are two main ways to launch a logisland job : ------------------------------------------ Logisland is packaged as a Docker container that you can build yourself or pull from Docker Hub. -To facilitate integration testing and to easily run tutorials, you can create a `docker-compose.yml` file with the following content. - -.. code-block:: yaml - - version: "2" - services: - - zookeeper: - container_name: zookeeper - image: hurence/zookeeper - hostname: zookeeper - ports: - - "2181:2181" - - kafka: - container_name: kafka - image: hurence/kafka - hostname: kafka - links: - - zookeeper - ports: - - "9092:9092" - environment: - KAFKA_ADVERTISED_PORT: 9092 - KAFKA_ADVERTISED_HOST_NAME: sandbox - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_JMX_PORT: 7071 - - # ES container - elasticsearch: - container_name: elasticsearch - environment: - - ES_JAVA_OPT="-Xms1G -Xmx1G" - - cluster.name=es-logisland - - http.host=0.0.0.0 - - transport.host=0.0.0.0 - - xpack.security.enabled=false - hostname: elasticsearch - container_name: elasticsearch - image: 'docker.elastic.co/elasticsearch/elasticsearch:5.4.0' - ports: - - '9200:9200' - - '9300:9300' - - # Kibana container - kibana: - container_name: kibana - environment: - - 'ELASTICSEARCH_URL=http://elasticsearch:9200' - image: 'docker.elastic.co/kibana/kibana:5.4.0' - container_name: kibana - links: - - elasticsearch - ports: - - '5601:5601' - - # Logisland container : does nothing but launching - logisland: - container_name: logisland - image: hurence/logisland:0.13.0 - command: tail -f bin/logisland.sh - #command: bin/logisland.sh --conf /conf/index-apache-logs.yml - links: - - zookeeper - - kafka - - elasticsearch - - redis - ports: - - "4050:4050" - volumes: - - ./conf/logisland:/conf - - ./data/logisland:/data - container_name: logisland - extra_hosts: - - "sandbox:172.17.0.1" - - redis: - container_name: redis - image: 'redis:latest' - ports: - - '6379:6379' +To facilitate integration testing and to easily run tutorials, you can use `docker-compose` with the following `docker-compose.yml `_. Once you have this file you can run a `docker-compose` command to launch all the needed services (zookeeper, kafka, es, kibana and logisland) +Elasticsearch on docker needs a special tweak as described `here `_ + .. code-block:: sh + # set vm.max_map_count kernel setting for elasticsearch + sudo sysctl -w vm.max_map_count=262144 + + # + cd /tmp + wget https://raw.githubusercontent.com/Hurence/logisland/master/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml docker-compose up .. note:: diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml b/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml index 8f71646e6..5dee26387 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml @@ -23,6 +23,8 @@ services: KAFKA_JMX_PORT: 7071 # ES container + # make sure to increase vm.max_map_count kernel setting like documented here : + # https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html elasticsearch: container_name: elasticsearch environment: From 5d661908b4ab93b22e99ceab2512b7f8ac9c3861 Mon Sep 17 00:00:00 2001 From: oalam Date: Thu, 31 May 2018 16:25:15 +0200 Subject: [PATCH 20/63] add ttl for thresholds --- .../logisland/record/FieldDictionary.java | 1 + .../datastore/DatastoreClientService.java | 11 + logisland-documentation/components.rst | 194 +++++++++++++++++- .../src/main/resources/components.json | 4 +- .../src/main/resources/docs/components.rst | 194 +++++++++++++++++- .../src/main/resources/docs/developer.rst | 2 +- .../docs/tutorials/prerequisites.rst | 90 +------- .../AbstractNashornSandboxProcessor.java | 10 + .../processor/alerting/CheckAlerts.java | 19 +- .../processor/alerting/CheckThresholds.java | 52 +++-- .../processor/alerting/CheckAlertsTest.java | 55 ++--- .../datastore/MockDatastoreService.java | 5 + .../cache/CSVKeyValueCacheService.java | 20 +- .../cache/LRUKeyValueCacheService.java | 2 +- .../service/RedisKeyValueCacheService.java | 10 + .../service/solr/api/SolrClientService.java | 12 ++ .../solr/Solr_6_4_2_ChronixClientService.java | 11 + 17 files changed, 530 insertions(+), 162 deletions(-) diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java b/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java index f8aa6ef8b..883466729 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java @@ -32,6 +32,7 @@ public class FieldDictionary { public static String PROCESSOR_NAME = "processor_name"; public static String RECORD_POSITION = "record_position"; public static String RECORD_BODY = "record_body"; + public static String RECORD_COUNT = "record_count"; public static String RECORD_POSITION_LATITUDE = "record_position_latitude"; diff --git a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientService.java b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientService.java index df3f373a5..6636d3b53 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientService.java +++ b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientService.java @@ -142,6 +142,17 @@ boolean putMapping(String indexName, String doctype, String mappingAsJsonString) void put(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException; + /** + * Remove the specified Record from the given collection. + * + * @param collectionName + * @param record + * @param asynchronous + * @throws Exception + */ + void remove(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException; + + /* ******************************************************************** diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index c74459dc6..78902f700 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -147,16 +147,199 @@ In the list below, the names of required properties appear in **bold**. Any othe ---------- -.. _com.hurence.logisland.processor.alerting.ComputeTag: +.. _com.hurence.logisland.processor.alerting.CheckAlerts: -ComputeTag ----------- +CheckAlerts +----------- Add one or more field with a default value -... Class _____ -com.hurence.logisland.processor.alerting.ComputeTag +com.hurence.logisland.processor.alerting.CheckAlerts + +Tags +____ +record, alerting, thresholds, opc, tag + +Properties +__________ +In the list below, the names of required properties appear in **bold**. Any other properties (not in bold) are considered optional. The table also indicates any default values +. + +.. csv-table:: allowable-values + :header: "Name","Description","Allowable Values","Default Value","Sensitive","EL" + :widths: 20,60,30,20,10,10 + + "max.cpu.time", "maximum CPU time in milliseconds allowed for script execution.", "", "100", "", "" + "max.memory", "maximum memory in Bytes which JS executor thread can allocate", "", "51200", "", "" + "allow.no.brace", "Force, to check if all blocks are enclosed with curly braces "{}". +

+ Explanation: all loops (for, do-while, while, and if-else, and functions + should use braces, because poison_pill() function will be inserted after + each open brace "{", to ensure interruption checking. Otherwise simple + code like: +

+    while(true) while(true) {
+      // do nothing
+    }
+  
+ or even: +
+    while(true)
+  
+ cause unbreakable loop, which force this sandbox to use {@link Thread#stop()} + which make JVM unstable. +

+

+ Properly writen code (even in bad intention) like: +

+    while(true) { while(true) {
+      // do nothing
+    }}
+  
+ will be changed into: +
+    while(true) {poison_pill(); 
+      while(true) {poison_pill();
+        // do nothing
+      }
+    }
+  
+ which finish nicely when interrupted. +

+ For legacy code, this check can be turned off, but with no guarantee, the + JS thread will gracefully finish when interrupted. +

", "", "false", "", "" + "max.prepared.statements", "The size of prepared statements LRU cache. Default 0 (disabled). +

+ Each statements when {@link #setMaxCPUTime(long)} is set is prepared to + quit itself when time exceeded. To execute only once this procedure per + statement set this value. +

+

+ When {@link #setMaxCPUTime(long)} is set 0, this value is ignored. +

", "", "30", "", "" + "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" + +Dynamic Properties +__________________ +Dynamic Properties allow the user to specify both the name and value of a property. + +.. csv-table:: dynamic-properties + :header: "Name","Value","Description","EL" + :widths: 20,20,40,10 + + "field to add", "a default value", "Add a field to the record with the default value", "" + +---------- + +.. _com.hurence.logisland.processor.alerting.CheckThresholds: + +CheckThresholds +--------------- +Compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + +Class +_____ +com.hurence.logisland.processor.alerting.CheckThresholds + +Tags +____ +record, threshold, tag, alerting + +Properties +__________ +In the list below, the names of required properties appear in **bold**. Any other properties (not in bold) are considered optional. The table also indicates any default values +. + +.. csv-table:: allowable-values + :header: "Name","Description","Allowable Values","Default Value","Sensitive","EL" + :widths: 20,60,30,20,10,10 + + "max.cpu.time", "maximum CPU time in milliseconds allowed for script execution.", "", "100", "", "" + "max.memory", "maximum memory in Bytes which JS executor thread can allocate", "", "51200", "", "" + "allow.no.brace", "Force, to check if all blocks are enclosed with curly braces "{}". +

+ Explanation: all loops (for, do-while, while, and if-else, and functions + should use braces, because poison_pill() function will be inserted after + each open brace "{", to ensure interruption checking. Otherwise simple + code like: +

+    while(true) while(true) {
+      // do nothing
+    }
+  
+ or even: +
+    while(true)
+  
+ cause unbreakable loop, which force this sandbox to use {@link Thread#stop()} + which make JVM unstable. +

+

+ Properly writen code (even in bad intention) like: +

+    while(true) { while(true) {
+      // do nothing
+    }}
+  
+ will be changed into: +
+    while(true) {poison_pill(); 
+      while(true) {poison_pill();
+        // do nothing
+      }
+    }
+  
+ which finish nicely when interrupted. +

+ For legacy code, this check can be turned off, but with no guarantee, the + JS thread will gracefully finish when interrupted. +

", "", "false", "", "" + "max.prepared.statements", "The size of prepared statements LRU cache. Default 0 (disabled). +

+ Each statements when {@link #setMaxCPUTime(long)} is set is prepared to + quit itself when time exceeded. To execute only once this procedure per + statement set this value. +

+

+ When {@link #setMaxCPUTime(long)} is set 0, this value is ignored. +

", "", "30", "", "" + "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" + +Dynamic Properties +__________________ +Dynamic Properties allow the user to specify both the name and value of a property. + +.. csv-table:: dynamic-properties + :header: "Name","Value","Description","EL" + :widths: 20,20,40,10 + + "field to add", "a default value", "Add a field to the record with the default value", "" + +---------- + +.. _com.hurence.logisland.processor.alerting.ComputeTags: + +ComputeTags +----------- +Compute tag cross from given formulas. +- each dynamic property will return a new record according to the formula definition +- the record name will be set to the property name +- the record time will be set to the current timestamp + +a threshold_cross has the following properties : count, sum, avg, time, duration, value + +Class +_____ +com.hurence.logisland.processor.alerting.ComputeTags Tags ____ @@ -221,6 +404,7 @@ In the list below, the names of required properties appear in **bold**. Any othe When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" Dynamic Properties __________________ diff --git a/logisland-framework/logisland-agent/src/main/resources/components.json b/logisland-framework/logisland-agent/src/main/resources/components.json index 5e064df1d..12fc832b6 100644 --- a/logisland-framework/logisland-agent/src/main/resources/components.json +++ b/logisland-framework/logisland-agent/src/main/resources/components.json @@ -3,7 +3,9 @@ {"name":"ApplyRegexp","description":"This processor is used to create a new set of fields from one field (using regexp).","component":"com.hurence.logisland.processor.ApplyRegexp","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"This processor is used to create a new set of fields from one field (using regexp).","isExpressionLanguageSupported":true}]}, {"name":"BulkAddElasticsearch","description":"Indexes the content of a Record in Elasticsearch using elasticsearch's bulk processor","component":"com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.index","isRequired":true,"description":"The name of the index to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"default.type","isRequired":true,"description":"The type of this document (used by Elasticsearch for indexing and searching)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.index","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":false,"description":"the name of the event field containing es doc type => will override type value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"BulkPut","description":"Indexes the content of a Record in a Datastore using bulk processor","component":"com.hurence.logisland.processor.datastore.BulkPut","type":"processor","tags":["datastore","record","put","bulk"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.collection","isRequired":true,"description":"The name of the collection/index/table to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.collection","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"date.format","isRequired":false,"description":"simple date format for date suffix. default : yyyy.MM.dd","defaultValue":"yyyy.MM.dd","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, -{"name":"ComputeTag","description":"Add one or more field with a default value\n...","component":"com.hurence.logisland.processor.alerting.ComputeTag","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"CheckAlerts","description":"Add one or more field with a default value","component":"com.hurence.logisland.processor.alerting.CheckAlerts","type":"processor","tags":["record","alerting","thresholds","opc","tag"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"profile.activation.condition","isRequired":false,"description":"A javascript expression that activates this alerting profile when true","defaultValue":"0==0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"CheckThresholds","description":"Compute threshold cross from given formulas.\n each dynamic property will return a new record according to the formula definition\n the record name will be set to the property name\n the record time will be set to the current timestamp","component":"com.hurence.logisland.processor.alerting.CheckThresholds","type":"processor","tags":["record","threshold","tag","alerting"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.ttl","isRequired":false,"description":"How long (in ms) do the record will remain in cache","defaultValue":"30000","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"ComputeTags","description":"Compute tag cross from given formulas.\n- each dynamic property will return a new record according to the formula definition\n- the record name will be set to the property name\n- the record time will be set to the current timestamp\n\na threshold_cross has the following properties : count, sum, avg, time, duration, value","component":"com.hurence.logisland.processor.alerting.ComputeTags","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, {"name":"ConsolidateSession","description":"The ConsolidateSession processor is the Logisland entry point to get and process events from the Web Analytics.As an example here is an incoming event from the Web Analytics:\n\n\"fields\": [{ \"name\": \"timestamp\", \"type\": \"long\" },{ \"name\": \"remoteHost\", \"type\": \"string\"},{ \"name\": \"record_type\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"record_id\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"location\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"hitType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventCategory\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventAction\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventLabel\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"localPath\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"q\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"n\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"referer\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"viewportPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"viewportPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"partyId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"sessionId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageViewId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_newSession\", \"type\": [\"null\", \"boolean\"],\"default\": null },{ \"name\": \"userAgentString\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"UserId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"B2Bunit\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pointOfService\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"companyID\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"GroupCode\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"userRoles\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_PunchOut\", \"type\": [\"null\", \"string\"], \"default\": null }]The ConsolidateSession processor groups the records by sessions and compute the duration between now and the last received event. If the distance from the last event is beyond a given threshold (by default 30mn), then the session is considered closed.The ConsolidateSession is building an aggregated session object for each active session.This aggregated object includes: - The actual session duration. - A boolean representing wether the session is considered active or closed. Note: it is possible to ressurect a session if for instance an event arrives after a session has been marked closed. - User related infos: userId, B2Bunit code, groupCode, userRoles, companyId - First visited page: URL - Last visited page: URL The properties to configure the processor are: - sessionid.field: Property name containing the session identifier (default: sessionId). - timestamp.field: Property name containing the timestamp of the event (default: timestamp). - session.timeout: Timeframe of inactivity (in seconds) after which a session is considered closed (default: 30mn). - visitedpage.field: Property name containing the page visited by the customer (default: location). - fields.to.return: List of fields to return in the aggregated object. (default: N/A)","component":"com.hurence.logisland.processor.webAnalytics.ConsolidateSession","type":"processor","tags":["analytics","web","session"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"session.timeout","isRequired":false,"description":"session timeout in sec","defaultValue":"1800","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionid.field","isRequired":false,"description":"the name of the field containing the session id => will override default value if set","defaultValue":"sessionId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timestamp.field","isRequired":false,"description":"the name of the field containing the timestamp => will override default value if set","defaultValue":"h2kTimestamp","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"visitedpage.field","isRequired":false,"description":"the name of the field containing the visited page => will override default value if set","defaultValue":"location","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"userid.field","isRequired":false,"description":"the name of the field containing the userId => will override default value if set","defaultValue":"userId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.return","isRequired":false,"description":"the list of fields to return","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the first visited page => will override default value if set","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the last visited page => will override default value if set","defaultValue":"lastVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"isSessionActive.out.field","isRequired":false,"description":"the name of the field stating whether the session is active or not => will override default value if set","defaultValue":"is_sessionActive","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionDuration.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"sessionDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"eventsCounter.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"eventsCounter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the first event => will override default value if set","defaultValue":"firstEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the last event => will override default value if set","defaultValue":"lastEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionInactivityDuration.out.field","isRequired":false,"description":"the name of the field containing the session inactivity duration => will override default value if set","defaultValue":"sessionInactivityDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"ConvertFieldsType","description":"Converts a field value into the given type. does nothing if conversion is not possible","component":"com.hurence.logisland.processor.ConvertFieldsType","type":"processor","tags":["type","fields","update","convert"],"dynamicProperties":[{"name":"field","value":"the new type","description":"convert field value into new type","isExpressionLanguageSupported":true}]}, {"name":"DebugStream","description":"This is a processor that logs incoming records","component":"com.hurence.logisland.processor.DebugStream","type":"processor","tags":["record","debug"],"properties":[{"name":"event.serializer","isRequired":true,"description":"the way to serialize event","Json serialization":"serialize events as json blocs","String serialization":"serialize events as toString() blocs","defaultValue":"json","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index c74459dc6..78902f700 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -147,16 +147,199 @@ In the list below, the names of required properties appear in **bold**. Any othe ---------- -.. _com.hurence.logisland.processor.alerting.ComputeTag: +.. _com.hurence.logisland.processor.alerting.CheckAlerts: -ComputeTag ----------- +CheckAlerts +----------- Add one or more field with a default value -... Class _____ -com.hurence.logisland.processor.alerting.ComputeTag +com.hurence.logisland.processor.alerting.CheckAlerts + +Tags +____ +record, alerting, thresholds, opc, tag + +Properties +__________ +In the list below, the names of required properties appear in **bold**. Any other properties (not in bold) are considered optional. The table also indicates any default values +. + +.. csv-table:: allowable-values + :header: "Name","Description","Allowable Values","Default Value","Sensitive","EL" + :widths: 20,60,30,20,10,10 + + "max.cpu.time", "maximum CPU time in milliseconds allowed for script execution.", "", "100", "", "" + "max.memory", "maximum memory in Bytes which JS executor thread can allocate", "", "51200", "", "" + "allow.no.brace", "Force, to check if all blocks are enclosed with curly braces "{}". +

+ Explanation: all loops (for, do-while, while, and if-else, and functions + should use braces, because poison_pill() function will be inserted after + each open brace "{", to ensure interruption checking. Otherwise simple + code like: +

+    while(true) while(true) {
+      // do nothing
+    }
+  
+ or even: +
+    while(true)
+  
+ cause unbreakable loop, which force this sandbox to use {@link Thread#stop()} + which make JVM unstable. +

+

+ Properly writen code (even in bad intention) like: +

+    while(true) { while(true) {
+      // do nothing
+    }}
+  
+ will be changed into: +
+    while(true) {poison_pill(); 
+      while(true) {poison_pill();
+        // do nothing
+      }
+    }
+  
+ which finish nicely when interrupted. +

+ For legacy code, this check can be turned off, but with no guarantee, the + JS thread will gracefully finish when interrupted. +

", "", "false", "", "" + "max.prepared.statements", "The size of prepared statements LRU cache. Default 0 (disabled). +

+ Each statements when {@link #setMaxCPUTime(long)} is set is prepared to + quit itself when time exceeded. To execute only once this procedure per + statement set this value. +

+

+ When {@link #setMaxCPUTime(long)} is set 0, this value is ignored. +

", "", "30", "", "" + "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" + +Dynamic Properties +__________________ +Dynamic Properties allow the user to specify both the name and value of a property. + +.. csv-table:: dynamic-properties + :header: "Name","Value","Description","EL" + :widths: 20,20,40,10 + + "field to add", "a default value", "Add a field to the record with the default value", "" + +---------- + +.. _com.hurence.logisland.processor.alerting.CheckThresholds: + +CheckThresholds +--------------- +Compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + +Class +_____ +com.hurence.logisland.processor.alerting.CheckThresholds + +Tags +____ +record, threshold, tag, alerting + +Properties +__________ +In the list below, the names of required properties appear in **bold**. Any other properties (not in bold) are considered optional. The table also indicates any default values +. + +.. csv-table:: allowable-values + :header: "Name","Description","Allowable Values","Default Value","Sensitive","EL" + :widths: 20,60,30,20,10,10 + + "max.cpu.time", "maximum CPU time in milliseconds allowed for script execution.", "", "100", "", "" + "max.memory", "maximum memory in Bytes which JS executor thread can allocate", "", "51200", "", "" + "allow.no.brace", "Force, to check if all blocks are enclosed with curly braces "{}". +

+ Explanation: all loops (for, do-while, while, and if-else, and functions + should use braces, because poison_pill() function will be inserted after + each open brace "{", to ensure interruption checking. Otherwise simple + code like: +

+    while(true) while(true) {
+      // do nothing
+    }
+  
+ or even: +
+    while(true)
+  
+ cause unbreakable loop, which force this sandbox to use {@link Thread#stop()} + which make JVM unstable. +

+

+ Properly writen code (even in bad intention) like: +

+    while(true) { while(true) {
+      // do nothing
+    }}
+  
+ will be changed into: +
+    while(true) {poison_pill(); 
+      while(true) {poison_pill();
+        // do nothing
+      }
+    }
+  
+ which finish nicely when interrupted. +

+ For legacy code, this check can be turned off, but with no guarantee, the + JS thread will gracefully finish when interrupted. +

", "", "false", "", "" + "max.prepared.statements", "The size of prepared statements LRU cache. Default 0 (disabled). +

+ Each statements when {@link #setMaxCPUTime(long)} is set is prepared to + quit itself when time exceeded. To execute only once this procedure per + statement set this value. +

+

+ When {@link #setMaxCPUTime(long)} is set 0, this value is ignored. +

", "", "30", "", "" + "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" + +Dynamic Properties +__________________ +Dynamic Properties allow the user to specify both the name and value of a property. + +.. csv-table:: dynamic-properties + :header: "Name","Value","Description","EL" + :widths: 20,20,40,10 + + "field to add", "a default value", "Add a field to the record with the default value", "" + +---------- + +.. _com.hurence.logisland.processor.alerting.ComputeTags: + +ComputeTags +----------- +Compute tag cross from given formulas. +- each dynamic property will return a new record according to the formula definition +- the record name will be set to the property name +- the record time will be set to the current timestamp + +a threshold_cross has the following properties : count, sum, avg, time, duration, value + +Class +_____ +com.hurence.logisland.processor.alerting.ComputeTags Tags ____ @@ -221,6 +404,7 @@ In the list below, the names of required properties appear in **bold**. Any othe When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" + "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" Dynamic Properties __________________ diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst b/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst index 76ffccd5c..3e6b9c2f0 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst @@ -222,7 +222,7 @@ Publish release assets to github please refer to `https://developer.github.com/v3/repos/releases `_ -curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/8905079/assets?name=logisland-0.13.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' +curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.13.0/assets?name=logisland-0.13.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst index 61c9995ae..596e3d85e 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst @@ -11,92 +11,20 @@ There are two main ways to launch a logisland job : ------------------------------------------ Logisland is packaged as a Docker container that you can build yourself or pull from Docker Hub. -To facilitate integration testing and to easily run tutorials, you can create a `docker-compose.yml` file with the following content, or directly download it from `a gist `_ - -.. code-block:: yaml - - version: "2" - services: - - zookeeper: - container_name: zookeeper - image: hurence/zookeeper - hostname: zookeeper - ports: - - "2181:2181" - - kafka: - container_name: kafka - image: hurence/kafka - hostname: kafka - links: - - zookeeper - ports: - - "9092:9092" - environment: - KAFKA_ADVERTISED_PORT: 9092 - KAFKA_ADVERTISED_HOST_NAME: sandbox - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_JMX_PORT: 7071 - - # ES container - elasticsearch: - container_name: elasticsearch - environment: - - ES_JAVA_OPT="-Xms1G -Xmx1G" - - cluster.name=es-logisland - - http.host=0.0.0.0 - - transport.host=0.0.0.0 - - xpack.security.enabled=false - hostname: elasticsearch - container_name: elasticsearch - image: 'docker.elastic.co/elasticsearch/elasticsearch:5.4.0' - ports: - - '9200:9200' - - '9300:9300' - - # Kibana container - kibana: - container_name: kibana - environment: - - 'ELASTICSEARCH_URL=http://elasticsearch:9200' - image: 'docker.elastic.co/kibana/kibana:5.4.0' - container_name: kibana - links: - - elasticsearch - ports: - - '5601:5601' - - # Logisland container : does nothing but launching - logisland: - container_name: logisland - image: hurence/logisland:0.13.0 - command: tail -f bin/logisland.sh - #command: bin/logisland.sh --conf /conf/index-apache-logs.yml - links: - - zookeeper - - kafka - - elasticsearch - - redis - ports: - - "4050:4050" - volumes: - - ./conf/logisland:/conf - - ./data/logisland:/data - container_name: logisland - extra_hosts: - - "sandbox:172.17.0.1" - - redis: - container_name: redis - image: 'redis:latest' - ports: - - '6379:6379' +To facilitate integration testing and to easily run tutorials, you can use `docker-compose` with the following `docker-compose.yml `_. Once you have this file you can run a `docker-compose` command to launch all the needed services (zookeeper, kafka, es, kibana and logisland) +Elasticsearch on docker needs a special tweak as described `here `_ + .. code-block:: sh + # set vm.max_map_count kernel setting for elasticsearch + sudo sysctl -w vm.max_map_count=262144 + + # + cd /tmp + wget https://raw.githubusercontent.com/Hurence/logisland/master/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml docker-compose up .. note:: diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java index 1d5342dcd..060269acf 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java @@ -137,6 +137,15 @@ public abstract class AbstractNashornSandboxProcessor extends AbstractProcessor .build(); + public static final PropertyDescriptor DATASTORE_CACHE_COLLECTION = new PropertyDescriptor.Builder() + .name("datastore.cache.collection") + .description("The collection where to find cached objects") + .required(false) + .defaultValue("test") + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + protected DatastoreClientService datastoreClientService; protected NashornSandbox sandbox; protected Map dynamicTagValuesMap; @@ -149,6 +158,7 @@ public List getSupportedPropertyDescriptors() { properties.add(ALLOw_NO_BRACE); properties.add(MAX_PREPARED_STATEMENTS); properties.add(DATASTORE_CLIENT_SERVICE); + properties.add(DATASTORE_CACHE_COLLECTION); return properties; } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java index 23a920a76..57ac9bea3 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java @@ -30,10 +30,7 @@ import org.slf4j.LoggerFactory; import javax.script.ScriptException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; +import java.util.*; @Tags({"record", "alerting", "thresholds", "opc", "tag"}) @CapabilityDescription("Add one or more field with a default value") @@ -93,20 +90,26 @@ private String expandCode(String rawCode) { .replaceAll("\\.count", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_COUNT).asDouble()") .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_AVG).asDouble()");*/ - return rawCode.replaceAll("cache\\((\\S*)\\).value", "getValue($1)") - .replaceAll("cache\\((\\S*)\\).count", "getCount($1)") - .replaceAll("cache\\((\\S*)\\).duration", "getDuration($1)"); + return rawCode.replaceAll("cache\\((\\S*)\\).value", "getValue($1)") + .replaceAll("cache\\((\\S*)\\).count", "getCount($1)") + .replaceAll("cache\\((\\S*)\\).duration", "getDuration($1)"); } @Override protected void setupDynamicProperties(ProcessContext context) { + + sandbox.allow(System.class); + sandbox.allow(Date.class); String profileActivationRule = context.getPropertyValue(PROFILE_ACTIVATION_CONDITION).asString(); StringBuilder sbActivation = new StringBuilder(); sbActivation.append("var alert = false;\n") .append("function getValue(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n") - .append("function getDuration(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(\"duration\").asDouble(); \n};\n") + .append("function getDuration(id) {\n") + .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") + .append(" var duration = new Date().getTime() - record.getTime().getTime();\n") + .append(" return duration; \n};\n") .append("function getCount(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(\"count\").asDouble(); \n};\n") .append("if( ") .append(expandCode(profileActivationRule)) diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java index f3c5a1586..900714531 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java @@ -22,6 +22,7 @@ import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.processor.ProcessContext; import com.hurence.logisland.record.*; +import com.hurence.logisland.validator.StandardValidators; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,7 +55,6 @@ the record time will be set to the current timestamp configuration: cache.client.service: cache default.record_type: threshold_cross - default.el.language: js default.ttl: 300000 tvib1: cache("vib1").value > 10.0; tvib2: cache("vib2").value >= 0 && cache("vib2").value < cache("vib1").value; @@ -62,6 +62,24 @@ the record time will be set to the current timestamp private static final Logger logger = LoggerFactory.getLogger(CheckThresholds.class); + + public static final PropertyDescriptor RECORD_TTL = new PropertyDescriptor.Builder() + .name("record.ttl") + .description("How long (in ms) do the record will remain in cache") + .required(false) + .defaultValue("30000") + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .build(); + + + @Override + public List getSupportedPropertyDescriptors() { + List properties = new ArrayList<>(super.getSupportedPropertyDescriptors()); + properties.add(RECORD_TTL); + + return properties; + } + @Override protected void setupDynamicProperties(ProcessContext context) { for (final Map.Entry entry : context.getProperties().entrySet()) { @@ -84,8 +102,12 @@ protected void setupDynamicProperties(ProcessContext context) { System.out.println(sb.toString()); logger.debug(sb.toString()); } + defaultCollection = context.getPropertyValue(DATASTORE_CACHE_COLLECTION).asString(); + recordTTL = context.getPropertyValue(RECORD_TTL).asInteger(); } + private String defaultCollection; + private Integer recordTTL; @Override public Collection process(ProcessContext context, Collection records) { @@ -98,30 +120,36 @@ public Collection process(ProcessContext context, Collection rec List outputRecords = new ArrayList<>(); for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { + // look for record into the cache + String key = entry.getKey(); + Record cachedThreshold = datastoreClientService.get(defaultCollection, new StandardRecord().setId(key)); + if (cachedThreshold != null) { + Long duration = System.currentTimeMillis() - cachedThreshold.getTime().getTime(); + if (duration > recordTTL) { + datastoreClientService.remove(defaultCollection, cachedThreshold, false); + cachedThreshold = null; + } + } try { sandbox.eval(entry.getValue()); Boolean match = (Boolean) sandbox.get("match"); if (match) { - String key = entry.getKey(); - Record cachedThreshold = datastoreClientService.get("test", new StandardRecord().setId(key)); - if (cachedThreshold != null) { - Long count = cachedThreshold.getField("count").asLong(); - Long duration = System.currentTimeMillis() - cachedThreshold.getField("first_record_time").asLong(); + if (cachedThreshold != null) { + Long count = cachedThreshold.getField(FieldDictionary.RECORD_COUNT).asLong(); + Date firstThresholdTime = cachedThreshold.getTime(); cachedThreshold.setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) - .setField("count", FieldType.LONG, count + 1) - .setField("duration", FieldType.LONG, duration); + .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, count + 1) + .setTime(firstThresholdTime); outputRecords.add(cachedThreshold); } else { Record threshold = new StandardRecord(RecordDictionary.THRESHOLD) .setId(key) .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) - .setField("count", FieldType.LONG, 1L) - .setField("first_record_time", FieldType.LONG, new Date().getTime()) - .setField("duration", FieldType.LONG, 0); - datastoreClientService.put("test", threshold, true); + .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, 1L); + datastoreClientService.put(defaultCollection, threshold, true); outputRecords.add(threshold); } } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java index c1aa3b450..b4a39a0d8 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java @@ -47,11 +47,13 @@ public void testMultipleRules() throws InitializationException { runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); - runner.setProperty("avib1","cache(\"cached_id1\").value > 12.0"); - runner.setProperty("avib2","cache(\"cached_id2\").value < 5"); - runner.setProperty("avib3","cache(\"cached_id3\").value < 5"); - runner.setProperty("avib4","cache(\"noone\").value < 5"); - runner.setProperty("avib5","brousoufparty++++"); + runner.setProperty("avib1", "cache(\"cached_id1\").value > 12.0"); // ok + runner.setProperty("avib2", "cache(\"cached_id2\").value < 5"); // ok + runner.setProperty("avib3", "cache(\"cached_id3\").value < 5"); // ko + runner.setProperty("avib4", "cache(\"noone\").value < 5"); // syntax error + runner.setProperty("avib5", "brousoufparty++++"); // syntax error + runner.setProperty("avib6", "cache(\"cached_id1\").count > 4"); // ok + runner.setProperty("avib7", "cache(\"cached_id2\").duration > 10000"); // ok runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); runner.addControllerService(service.getIdentifier(), service); @@ -60,7 +62,7 @@ public void testMultipleRules() throws InitializationException { runner.assertValid(); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(2); + runner.assertOutputRecordsCount(4); runner.assertOutputErrorCount(2); for (Record enriched : runner.getOutputRecords()) { @@ -70,50 +72,21 @@ public void testMultipleRules() throws InitializationException { } } - @Test - public void testMultipleThresholds() throws InitializationException { - - // create the controller service and link it to the test processor - final DatastoreClientService service = new MockDatastoreService(); - getCacheRecords().forEach(r -> service.put("test", r, false)); - - final TestRunner runner = TestRunners.newTestRunner(CheckAlerts.class); - runner.setProperty(CheckAlerts.MAX_CPU_TIME, "100"); - runner.setProperty(CheckAlerts.MAX_MEMORY, "12800000"); - runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); - runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); - runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); - runner.setProperty("avib1","cache(\"cached_id1\").value > 12.0"); - - runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); - runner.addControllerService(service.getIdentifier(), service); - runner.enableControllerService(service); - - runner.assertValid(); - runner.run(); - runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(2); - runner.assertOutputErrorCount(2); - - for (Record enriched : runner.getOutputRecords()) { - if (enriched.getId().equals("avib1")) { - assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asString(), "cache(\"cached_id1\").value > 12.0"); - } - } - } - - private Collection getCacheRecords() { Collection lookupRecords = new ArrayList<>(); + Long startTime = System.currentTimeMillis() - 30000; // 30" ago + lookupRecords.add(new StandardRecord() .setId("cached_id1") - .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 12.45)); + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 12.45) + .setField("count", FieldType.LONG, 5)); lookupRecords.add(new StandardRecord() .setId("cached_id2") - .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 2.5)); + .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 2.5) + .setTime(startTime)); lookupRecords.add(new StandardRecord() .setId("cached_id3") diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java index d10f698f1..05eecccd3 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java @@ -92,6 +92,11 @@ public void put(String collectionName, Record record, boolean asynchronous) thro collections.get(collectionName).put(record.getId(), record); } + @Override + public void remove(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException { + collections.get(collectionName).remove(record.getId()); + } + @Override public List multiGet(List multiGetQueryRecords) throws DatastoreClientServiceException { List results = new ArrayList<>(); diff --git a/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java b/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java index 6b7693594..5123fbba9 100644 --- a/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java +++ b/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java @@ -38,6 +38,7 @@ import org.apache.hadoop.fs.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; import java.io.*; import java.net.URI; @@ -299,12 +300,12 @@ private InputStream initFromUri(String dbUri) { @Override public void createCollection(String name, int partitionsCount, int replicationFactor) throws DatastoreClientServiceException { - + throw new NotImplementedException(); } @Override public void dropCollection(String name) throws DatastoreClientServiceException { - + throw new NotImplementedException(); } @Override @@ -319,17 +320,17 @@ public boolean existsCollection(String name) throws DatastoreClientServiceExcept @Override public void refreshCollection(String name) throws DatastoreClientServiceException { - + throw new NotImplementedException(); } @Override public void copyCollection(String reindexScrollTimeout, String src, String dst) throws DatastoreClientServiceException { - + throw new NotImplementedException(); } @Override public void createAlias(String collection, String alias) throws DatastoreClientServiceException { - + throw new NotImplementedException(); } @Override @@ -339,12 +340,12 @@ public boolean putMapping(String indexName, String doctype, String mappingAsJson @Override public void bulkFlush() throws DatastoreClientServiceException { - + throw new NotImplementedException(); } @Override public void bulkPut(String collectionName, Record record) throws DatastoreClientServiceException { - + set(record.getField(rowKey).asString(), record); } @Override @@ -353,6 +354,11 @@ public void put(String collectionName, Record record, boolean asynchronous) thro set(record.getField(rowKey).asString(), record); } + @Override + public void remove(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException { + throw new NotImplementedException(); + } + @Override public List multiGet(List multiGetQueryRecords) throws DatastoreClientServiceException { diff --git a/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/LRUKeyValueCacheService.java b/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/LRUKeyValueCacheService.java index 20faca8f6..4dfec083e 100644 --- a/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/LRUKeyValueCacheService.java +++ b/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/LRUKeyValueCacheService.java @@ -47,7 +47,7 @@ @CapabilityDescription("A controller service for caching data by key value pair with LRU (last recently used) strategy. using LinkedHashMap") public class LRUKeyValueCacheService extends AbstractControllerService implements CacheService { - private volatile Cache cache; + protected volatile Cache cache; @Override public V get(K k) { diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java index 7d9939951..11da01da0 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java @@ -398,6 +398,16 @@ public void put(String collectionName, Record record, boolean asynchronous) thro set(record.getId(),record); } + @Override + public void remove(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException { + try { + remove(record.getId(),stringSerializer); + } catch (IOException e) { + getLogger().warn("Error removing record : " + e.getMessage(), e); + } + + } + @Override public List multiGet(List multiGetQueryRecords) throws DatastoreClientServiceException { return null; diff --git a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrClientService.java b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrClientService.java index 4a95c2cd3..67c3cb7a8 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrClientService.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrClientService.java @@ -39,6 +39,7 @@ import org.apache.solr.client.solrj.response.CollectionAdminResponse; import org.apache.solr.client.solrj.response.CoreAdminResponse; import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.client.solrj.response.UpdateResponse; import org.apache.solr.client.solrj.response.schema.SchemaResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; @@ -625,4 +626,15 @@ public long queryCount(String queryString, String collectionName) { public long queryCount(String queryString) { return queryCount(queryString, null); } + + @Override + public void remove(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException { + try { + getClient().deleteById(collectionName, record.getId()); + + } catch (SolrServerException | IOException e) { + logger.error(e.toString()); + throw new DatastoreClientServiceException(e); + } + } } diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java index cc7d16299..13c6ae6ec 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java @@ -266,6 +266,17 @@ public void put(String collectionName, Record record, boolean asynchronous) thro } + @Override + public void remove(String collectionName, Record record, boolean asynchronous) throws DatastoreClientServiceException { + try { + solr.deleteById(collectionName, record.getId()); + + } catch (SolrServerException | IOException e) { + logger.error(e.toString()); + throw new DatastoreClientServiceException(e); + } + } + @Override public List multiGet(List multiGetQueryRecords) throws DatastoreClientServiceException { return null; From e43baec1d9c03babd753a277f8d99c563701c7f0 Mon Sep 17 00:00:00 2001 From: oalam Date: Fri, 1 Jun 2018 17:51:24 +0200 Subject: [PATCH 21/63] first test of REDIS & alerting features --- .../logisland/record/StandardRecord.java | 19 ++- logisland-documentation/components.rst | 3 + .../src/main/resources/components.json | 6 +- .../main/resources/conf/store-to-redis.yml | 149 +++++++++++++++++- .../src/main/resources/docs/components.rst | 3 + .../logisland/serializer/JsonSerializer.java | 119 ++++++++------ .../com/hurence/logisland/util/ListUtils.java | 33 ++++ .../logisland/util/string/StringUtils.java | 4 +- .../serializer/JsonSerializerTest.java | 105 +++++++----- .../logisland/processor/DebugStream.java | 11 ++ .../AbstractNashornSandboxProcessor.java | 16 +- .../processor/alerting/CheckAlerts.java | 35 +++- .../processor/alerting/CheckThresholds.java | 10 +- .../processor/alerting/ComputeTags.java | 27 ++-- .../processor/alerting/CheckAlertsTest.java | 97 +++++++++++- .../processor/alerting/ComputeTagsTest.java | 9 +- .../service/RedisKeyValueCacheService.java | 27 +++- 17 files changed, 525 insertions(+), 148 deletions(-) create mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java b/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java index 2d8c494f9..649659c2c 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -107,8 +107,8 @@ public int hashCode() { @Override public Position getPosition() { - if(hasPosition()) - return (Position)getField(FieldDictionary.RECORD_POSITION).asRecord(); + if (hasPosition()) + return (Position) getField(FieldDictionary.RECORD_POSITION).asRecord(); else return null; } @@ -249,7 +249,12 @@ public Record setStringField(String fieldName, String value) { */ @Override public Field removeField(String fieldName) { - return fields.remove(fieldName); + if (fieldName.equals(FieldDictionary.RECORD_TIME)) { + logger.debug("trying to remove record_time field. we won't let you do that !!"); + return fields.get(FieldDictionary.RECORD_TIME); + } else { + return fields.remove(fieldName); + } } /** diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index 78902f700..7051eb643 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -221,6 +221,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" Dynamic Properties @@ -312,6 +313,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" Dynamic Properties @@ -405,6 +407,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "output.record.type", "the type of the output record", "", "event", "", "" Dynamic Properties __________________ diff --git a/logisland-framework/logisland-agent/src/main/resources/components.json b/logisland-framework/logisland-agent/src/main/resources/components.json index 12fc832b6..951b5507f 100644 --- a/logisland-framework/logisland-agent/src/main/resources/components.json +++ b/logisland-framework/logisland-agent/src/main/resources/components.json @@ -3,9 +3,9 @@ {"name":"ApplyRegexp","description":"This processor is used to create a new set of fields from one field (using regexp).","component":"com.hurence.logisland.processor.ApplyRegexp","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"This processor is used to create a new set of fields from one field (using regexp).","isExpressionLanguageSupported":true}]}, {"name":"BulkAddElasticsearch","description":"Indexes the content of a Record in Elasticsearch using elasticsearch's bulk processor","component":"com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.index","isRequired":true,"description":"The name of the index to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"default.type","isRequired":true,"description":"The type of this document (used by Elasticsearch for indexing and searching)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.index","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":false,"description":"the name of the event field containing es doc type => will override type value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"BulkPut","description":"Indexes the content of a Record in a Datastore using bulk processor","component":"com.hurence.logisland.processor.datastore.BulkPut","type":"processor","tags":["datastore","record","put","bulk"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.collection","isRequired":true,"description":"The name of the collection/index/table to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.collection","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"date.format","isRequired":false,"description":"simple date format for date suffix. default : yyyy.MM.dd","defaultValue":"yyyy.MM.dd","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, -{"name":"CheckAlerts","description":"Add one or more field with a default value","component":"com.hurence.logisland.processor.alerting.CheckAlerts","type":"processor","tags":["record","alerting","thresholds","opc","tag"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"profile.activation.condition","isRequired":false,"description":"A javascript expression that activates this alerting profile when true","defaultValue":"0==0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, -{"name":"CheckThresholds","description":"Compute threshold cross from given formulas.\n each dynamic property will return a new record according to the formula definition\n the record name will be set to the property name\n the record time will be set to the current timestamp","component":"com.hurence.logisland.processor.alerting.CheckThresholds","type":"processor","tags":["record","threshold","tag","alerting"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.ttl","isRequired":false,"description":"How long (in ms) do the record will remain in cache","defaultValue":"30000","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, -{"name":"ComputeTags","description":"Compute tag cross from given formulas.\n- each dynamic property will return a new record according to the formula definition\n- the record name will be set to the property name\n- the record time will be set to the current timestamp\n\na threshold_cross has the following properties : count, sum, avg, time, duration, value","component":"com.hurence.logisland.processor.alerting.ComputeTags","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"CheckAlerts","description":"Add one or more field with a default value","component":"com.hurence.logisland.processor.alerting.CheckAlerts","type":"processor","tags":["record","alerting","thresholds","opc","tag"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"profile.activation.condition","isRequired":false,"description":"A javascript expression that activates this alerting profile when true","defaultValue":"0==0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"CheckThresholds","description":"Compute threshold cross from given formulas.\n each dynamic property will return a new record according to the formula definition\n the record name will be set to the property name\n the record time will be set to the current timestamp","component":"com.hurence.logisland.processor.alerting.CheckThresholds","type":"processor","tags":["record","threshold","tag","alerting"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.ttl","isRequired":false,"description":"How long (in ms) do the record will remain in cache","defaultValue":"30000","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"ComputeTags","description":"Compute tag cross from given formulas.\n- each dynamic property will return a new record according to the formula definition\n- the record name will be set to the property name\n- the record time will be set to the current timestamp\n\na threshold_cross has the following properties : count, sum, avg, time, duration, value","component":"com.hurence.logisland.processor.alerting.ComputeTags","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, {"name":"ConsolidateSession","description":"The ConsolidateSession processor is the Logisland entry point to get and process events from the Web Analytics.As an example here is an incoming event from the Web Analytics:\n\n\"fields\": [{ \"name\": \"timestamp\", \"type\": \"long\" },{ \"name\": \"remoteHost\", \"type\": \"string\"},{ \"name\": \"record_type\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"record_id\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"location\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"hitType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventCategory\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventAction\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventLabel\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"localPath\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"q\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"n\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"referer\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"viewportPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"viewportPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"partyId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"sessionId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageViewId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_newSession\", \"type\": [\"null\", \"boolean\"],\"default\": null },{ \"name\": \"userAgentString\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"UserId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"B2Bunit\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pointOfService\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"companyID\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"GroupCode\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"userRoles\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_PunchOut\", \"type\": [\"null\", \"string\"], \"default\": null }]The ConsolidateSession processor groups the records by sessions and compute the duration between now and the last received event. If the distance from the last event is beyond a given threshold (by default 30mn), then the session is considered closed.The ConsolidateSession is building an aggregated session object for each active session.This aggregated object includes: - The actual session duration. - A boolean representing wether the session is considered active or closed. Note: it is possible to ressurect a session if for instance an event arrives after a session has been marked closed. - User related infos: userId, B2Bunit code, groupCode, userRoles, companyId - First visited page: URL - Last visited page: URL The properties to configure the processor are: - sessionid.field: Property name containing the session identifier (default: sessionId). - timestamp.field: Property name containing the timestamp of the event (default: timestamp). - session.timeout: Timeframe of inactivity (in seconds) after which a session is considered closed (default: 30mn). - visitedpage.field: Property name containing the page visited by the customer (default: location). - fields.to.return: List of fields to return in the aggregated object. (default: N/A)","component":"com.hurence.logisland.processor.webAnalytics.ConsolidateSession","type":"processor","tags":["analytics","web","session"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"session.timeout","isRequired":false,"description":"session timeout in sec","defaultValue":"1800","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionid.field","isRequired":false,"description":"the name of the field containing the session id => will override default value if set","defaultValue":"sessionId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timestamp.field","isRequired":false,"description":"the name of the field containing the timestamp => will override default value if set","defaultValue":"h2kTimestamp","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"visitedpage.field","isRequired":false,"description":"the name of the field containing the visited page => will override default value if set","defaultValue":"location","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"userid.field","isRequired":false,"description":"the name of the field containing the userId => will override default value if set","defaultValue":"userId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.return","isRequired":false,"description":"the list of fields to return","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the first visited page => will override default value if set","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the last visited page => will override default value if set","defaultValue":"lastVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"isSessionActive.out.field","isRequired":false,"description":"the name of the field stating whether the session is active or not => will override default value if set","defaultValue":"is_sessionActive","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionDuration.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"sessionDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"eventsCounter.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"eventsCounter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the first event => will override default value if set","defaultValue":"firstEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the last event => will override default value if set","defaultValue":"lastEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionInactivityDuration.out.field","isRequired":false,"description":"the name of the field containing the session inactivity duration => will override default value if set","defaultValue":"sessionInactivityDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"ConvertFieldsType","description":"Converts a field value into the given type. does nothing if conversion is not possible","component":"com.hurence.logisland.processor.ConvertFieldsType","type":"processor","tags":["type","fields","update","convert"],"dynamicProperties":[{"name":"field","value":"the new type","description":"convert field value into new type","isExpressionLanguageSupported":true}]}, {"name":"DebugStream","description":"This is a processor that logs incoming records","component":"com.hurence.logisland.processor.DebugStream","type":"processor","tags":["record","debug"],"properties":[{"name":"event.serializer","isRequired":true,"description":"the way to serialize event","Json serialization":"serialize events as json blocs","String serialization":"serialize events as toString() blocs","defaultValue":"json","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml b/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml index 4e2efeaca..f9c63f458 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml @@ -69,7 +69,7 @@ engine: kafka.output.topics: logisland_events kafka.error.topics: logisland_errors kafka.input.topics.serializer: none - kafka.output.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + kafka.output.topics.serializer: com.hurence.logisland.serializer.JsonSerializer kafka.error.topics.serializer: com.hurence.logisland.serializer.JsonSerializer kafka.metadata.broker.list: ${KAFKA_BROKERS} kafka.zookeeper.quorum: ${ZK_QUORUM} @@ -88,6 +88,55 @@ engine: value.regex: (\S+)\s+(\S+)\s+(\S+)\s+\[([\w:\/]+\s[+\-]\d{4})\]\s+"(\S+)\s+(\S+)\s*(\S*)"\s+(\S+)\s+(\S+) value.fields: src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out + - processor: normalize_fields + component: com.hurence.logisland.processor.NormalizeFields + type: parser + documentation: change current id to src_ip + configuration: + conflict.resolution.policy: overwrite_existing + record_value: bytes_out + + - processor: modify_id + component: com.hurence.logisland.processor.ModifyId + type: parser + documentation: change current id to src_ip + configuration: + id.generation.strategy: fromFields + fields.to.hash: src_ip + java.formatter.string: "%1$s" + + - processor: remove_fields + component: com.hurence.logisland.processor.RemoveFields + type: parser + documentation: remove useless fields + configuration: + fields.to.remove: src_ip,identd,user,http_method,http_query,http_version,http_status,bytes_out + + - processor: cast + component: com.hurence.logisland.processor.ConvertFieldsType + type: parser + documentation: cast values + configuration: + record_value: double + + - processor: compute_tag + component: com.hurence.logisland.processor.alerting.ComputeTags + type: processor + documentation: | + compute tags from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: computed_tag + max.cpu.time: 500 + max.memory: 64800000 + max.prepared.statements: 5 + allow.no.brace: false + computed1: return cache("ppp-mia-30.shadow.net").value * 10.2; + + # all the parsed records are added to datastore by bulk - processor: datastore_publisher component: com.hurence.logisland.processor.datastore.BulkPut @@ -95,3 +144,101 @@ engine: documentation: "indexes processed events in datastore" configuration: datastore.client.service: datastore_service + + + + - stream: alerting_stream + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + type: stream + documentation: a processor that converts raw apache logs into structured log records + configuration: + kafka.input.topics: logisland_events + kafka.output.topics: logisland_alerts + kafka.error.topics: logisland_errors + kafka.input.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.output.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.error.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.metadata.broker.list: ${KAFKA_BROKERS} + kafka.zookeeper.quorum: ${ZK_QUORUM} + kafka.topic.autoCreate: true + kafka.topic.default.partitions: 4 + kafka.topic.default.replicationFactor: 1 + processorConfigurations: + + + + - processor: compute_thresholds + component: com.hurence.logisland.processor.alerting.CheckThresholds + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + + a threshold_cross has the following properties : count, time, duration, value + configuration: + datastore.client.service: datastore_service + output.record.type: threshold_cross + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + record.ttl: 300000 + tvib1: cache("port26.annex2.nwlink.com").value > 2000.0 + + - processor: debug + component: com.hurence.logisland.processor.DebugStream + configuration: + event.serializer: json + + - processor: compute_alerts1 + component: com.hurence.logisland.processor.alerting.CheckAlerts + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: medium_alert + alert.criticity: 1 + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + profile.activation.condition: cache("port26.annex2.nwlink.com").value > 3000.0 + avib1: cache("tvib1").count > 5.0 + + + - processor: debug + component: com.hurence.logisland.processor.DebugStream + configuration: + event.serializer: json +# +# - processor: compute_alerts2 +# component: com.hurence.logisland.processor.alerting.CheckAlerts +# type: processor +# documentation: | +# compute threshold cross from given formulas. +# each dynamic property will return a new record according to the formula definition +# the record name will be set to the property name +# the record time will be set to the current timestamp +# configuration: +# datastore.client.service: datastore_service +# output.record.type: critical_alert +# alert.criticity: 2 +# max.cpu.time: 100 +# max.memory: 12800000 +# max.prepared.statements: 5 +# profile.activation.condition: cache("vib1").value <= 10.0 || cache("vib2").value <= 2 +# avib1: cache("tvib1").count > 10.0 +# avib3: cache("tvib12").duration > 30000.0 +# +# - processor: datastore_publisher +# component: com.hurence.logisland.processor.datastore.BulkPut +# type: processor +# documentation: "indexes processed events in datastore" +# configuration: +# datastore.client.service: datastore_service + + diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index 78902f700..7051eb643 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -221,6 +221,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" Dynamic Properties @@ -312,6 +313,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" Dynamic Properties @@ -405,6 +407,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "output.record.type", "the type of the output record", "", "event", "", "" Dynamic Properties __________________ diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java index 9260ce163..412f776af 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java @@ -39,23 +39,15 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.hurence.logisland.record.Field; -import com.hurence.logisland.record.FieldType; -import com.hurence.logisland.record.Record; -import com.hurence.logisland.record.StandardRecord; -import com.hurence.logisland.util.time.DateUtil; +import com.hurence.logisland.record.*; +import com.hurence.logisland.util.ListUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.TimeZone; +import java.util.*; public class JsonSerializer implements RecordSerializer { @@ -84,7 +76,7 @@ public void serialize(Record record, JsonGenerator jgen, com.fasterxml.jackson.d // retrieve event field String fieldName = entry.getKey(); Field field = entry.getValue(); - // Object fieldValue = field.getRawValue(); + // Object fieldValue = field.getRawValue(); String fieldType = field.getType().toString(); // dump event field as record attribute @@ -110,6 +102,16 @@ public void serialize(Record record, JsonGenerator jgen, com.fasterxml.jackson.d case "boolean": jgen.writeBooleanField(fieldName, field.asBoolean()); break; + case "array": + jgen.writeArrayFieldStart(fieldName); + // jgen.writeStartArray(); + String[] items = field.asString().split(","); + for (String item : items) { + jgen.writeString(item); + } + jgen.writeEndArray(); + break; + default: jgen.writeObjectField(fieldName, field.asString()); break; @@ -144,7 +146,7 @@ public void serialize(OutputStream out, Record record) throws RecordSerializatio out.write(jsonString.getBytes()); out.flush(); } catch (IOException e) { - logger.debug(e.toString()); + logger.debug(e.toString()); } } @@ -171,6 +173,8 @@ public Record deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE Map fields = new HashMap<>(); boolean processingFields = false; + Map> arrays = new HashMap<>(); + String currentArrayName = null; while ((currentToken = jp.nextValue()) != null) { switch (currentToken) { @@ -193,42 +197,49 @@ public Record deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE } catch (Exception e) { e.printStackTrace(); } - }else { + } else { fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.LONG, jp.getLongValue())); } } break; - case VALUE_NUMBER_FLOAT: - try { + case VALUE_NUMBER_FLOAT: + try { - fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.DOUBLE, jp.getDoubleValue())); - } catch (JsonParseException ex) { + fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.DOUBLE, jp.getDoubleValue())); + } catch (JsonParseException ex) { - fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.FLOAT, jp.getFloatValue())); - } - break; - case VALUE_FALSE: - case VALUE_TRUE: - fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.BOOLEAN, jp.getBooleanValue())); - break; - case START_ARRAY: - logger.info(jp.getCurrentName()); - break; - - case END_ARRAY: - break; - case VALUE_STRING: - - if (jp.getCurrentName() != null) { - switch (jp.getCurrentName()) { - case "id": - id = jp.getValueAsString(); - break; - case "type": - type = jp.getValueAsString(); - break; + fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.FLOAT, jp.getFloatValue())); + } + break; + case VALUE_FALSE: + case VALUE_TRUE: + fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.BOOLEAN, jp.getBooleanValue())); + break; + case START_ARRAY: + + currentArrayName = jp.getCurrentName(); + arrays.put(currentArrayName, new ArrayList<>()); + break; + + case END_ARRAY: + + String itemString = ListUtils.mkString(arrays.get(currentArrayName), String::toString, ", "); + + fields.put(currentArrayName, new Field(jp.getCurrentName(), FieldType.ARRAY, itemString)); + + break; + case VALUE_STRING: + + if (jp.getCurrentName() != null) { + switch (jp.getCurrentName()) { + case "id": + id = jp.getValueAsString(); + break; + case "type": + type = jp.getValueAsString(); + break; /* case "creationDate": try { creationDate = new Date(jp.getValueAsLong()); @@ -236,20 +247,24 @@ public Record deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE e.printStackTrace(); } break;*/ - default: - fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.STRING, jp.getValueAsString())); + default: + fields.put(jp.getCurrentName(), new Field(jp.getCurrentName(), FieldType.STRING, jp.getValueAsString())); + + break; + } + } else { + arrays.get(currentArrayName).add(jp.getValueAsString()); + - break; } - } - break; - default: - break; + break; + default: + break; + } } - } - Record record = new StandardRecord(); + Record record = new StandardRecord(); if (id != null) { record.setId(id); } @@ -263,9 +278,9 @@ public Record deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE return record; - } + } -} + } @Override diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java new file mode 100644 index 000000000..65c7f8ec0 --- /dev/null +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.hurence.logisland.util; + +import java.util.List; +import java.util.function.Function; + +public class ListUtils { + static public String mkString(List list, Function stringify, String delimiter) { + int i = 0; + StringBuilder s = new StringBuilder(); + for (E e : list) { + if (i != 0) { s.append(delimiter); } + s.append(stringify.apply(e)); + i++; + } + return s.toString(); + } +} \ No newline at end of file diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java index 077caed9f..d94a88ba0 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java @@ -132,9 +132,9 @@ public static String resolveEnvVars(String input, String defaultValue) { } } ); - if(!hasBeenReplaced[0]){ + /* if(!hasBeenReplaced[0]){ m.appendReplacement(sb, null == envVarValue ? defaultValue : envVarValue); - } + }*/ } m.appendTail(sb); diff --git a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java index ccc597ac0..f0fb9d188 100755 --- a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java +++ b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -39,51 +39,76 @@ public class JsonSerializerTest { - @Test - public void validateJsonSerialization() throws IOException { + @Test + public void validateArrays() throws IOException { + + final JsonSerializer serializer = new JsonSerializer(); + + + Record record = new StandardRecord("cisco"); + record.setId("firewall_record1"); + record.addError("fatal_error", "ouille"); + //record.setField("tags", FieldType.ARRAY, new ArrayList<>(Arrays.asList("spam", "filter", "mail"))); + + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.serialize(baos, record); + baos.close(); + + + String strEvent = new String(baos.toByteArray()); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Record deserializedRecord = serializer.deserialize(bais); + + // assertEquals(record.getAllFieldsSorted(), deserializedRecord.getAllFieldsSorted()); + + } + + @Test + public void validateJsonSerialization() throws IOException { + + final JsonSerializer serializer = new JsonSerializer(); + - final JsonSerializer serializer = new JsonSerializer(); + Record record = new StandardRecord("cisco"); + record.setId("firewall_record1"); + record.setField("timestamp", FieldType.LONG, new Date().getTime()); + record.setField("method", FieldType.STRING, "GET"); + record.setField("ip_source", FieldType.STRING, "123.34.45.123"); + record.setField("ip_target", FieldType.STRING, "255.255.255.255"); + record.setField("url_scheme", FieldType.STRING, "http"); + record.setField("url_host", FieldType.STRING, "origin-www.20minutes.fr"); + record.setField("url_port", FieldType.STRING, "80"); + record.setField("url_path", FieldType.STRING, "/r15lgc-100KB.js"); + record.setField("request_size", FieldType.INT, 1399); + record.setField("response_size", FieldType.INT, 452); + record.setField("is_outside_office_hours", FieldType.BOOLEAN, false); + record.setField("is_host_blacklisted", FieldType.BOOLEAN, false); + //record.setField("tags", FieldType.ARRAY, new ArrayList<>(Arrays.asList("spam", "filter", "mail"))); - Record record = new StandardRecord("cisco"); - record.setId("firewall_record1"); - record.setField("timestamp", FieldType.LONG, new Date().getTime()); - record.setField("method", FieldType.STRING, "GET"); - record.setField("ip_source", FieldType.STRING, "123.34.45.123"); - record.setField("ip_target", FieldType.STRING, "255.255.255.255"); - record.setField("url_scheme", FieldType.STRING, "http"); - record.setField("url_host", FieldType.STRING, "origin-www.20minutes.fr"); - record.setField("url_port", FieldType.STRING, "80"); - record.setField("url_path", FieldType.STRING, "/r15lgc-100KB.js"); - record.setField("request_size", FieldType.INT, 1399); - record.setField("response_size", FieldType.INT, 452); - record.setField("is_outside_office_hours", FieldType.BOOLEAN, false); - record.setField("is_host_blacklisted", FieldType.BOOLEAN, false); - //record.setField("tags", FieldType.ARRAY, new ArrayList<>(Arrays.asList("spam", "filter", "mail"))); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.serialize(baos, record); + baos.close(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - serializer.serialize(baos, record); - baos.close(); + String strEvent = new String(baos.toByteArray()); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + Record deserializedRecord = serializer.deserialize(bais); + assertEquals(record, deserializedRecord); - String strEvent = new String(baos.toByteArray()); - ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - Record deserializedRecord = serializer.deserialize(bais); + } - assertEquals(record,deserializedRecord); - } - - - @Test - public void issueWithDate(){ - final String recordStr = "{ \"id\" : \"0:vfBHTdrZCcFs3H6aO7Yb4UXWVppa80JiKQ7aW0\", \"type\" : \"pageView\", \"creationDate\" : \"Thu Apr 27 11:45:00 CEST 2017\", \"fields\" : { \"referer\" : \"https://orexad.preprod.group-iph.com/fr/equipement/c-45\", \"B2BUnit\" : null, \"eventCategory\" : null, \"remoteHost\" : \"149.202.66.102\", \"userAgentString\" : \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\", \"eventAction\" : null, \"categoryName\" : null, \"viewportPixelWidth\" : 1624, \"hitType\" : null, \"companyID\" : null, \"pageType\" : null, \"Userid\" : null, \"localPath\" : null, \"partyId\" : \"0:j1ymvypx:pO~iVklsXUYa6zMKVZeA2nC1YlVzTEw1\", \"codeProduct\" : null, \"is_newSession\" : false, \"GroupCode\" : null, \"sessionId\" : \"0:j20802wr:Py1qDrBry7UedH6my~6ebE58wRHUXWVp\", \"categoryCode\" : null, \"eventLabel\" :null, \"record_type\" : \"pageView\", \"n\" : null, \"record_id\" : \"0:vfBHTdrZCcFs3H6aO7Yb4pa80JiKQ7aW0\", \"q\" : null, \"userRoles\" : null, \"screenPixelWidth\" : 1855, \"viewportPixelHeight\" : 726, \"screenPixelHeight\" : 1056, \"is_PunchOut\" : null, \"h2kTimestamp\" : 1493286295730, \"pageViewId\" : \"0:vfBHTdrZCcFs3H6aO7Yb4pa80JiKQ7aW\", \"location\" : \"https://orexad.preprod.group-iph.com/fr/equipement/c-45\", \"record_time\" : 1493286300033, \"pointOfService\" : null }}"; + @Test + public void issueWithDate() { + final String recordStr = "{ \"id\" : \"0:vfBHTdrZCcFs3H6aO7Yb4UXWVppa80JiKQ7aW0\", \"type\" : \"pageView\", \"creationDate\" : \"Thu Apr 27 11:45:00 CEST 2017\", \"fields\" : { \"referer\" : \"https://orexad.preprod.group-iph.com/fr/equipement/c-45\", \"B2BUnit\" : null, \"eventCategory\" : null, \"remoteHost\" : \"149.202.66.102\", \"userAgentString\" : \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36\", \"eventAction\" : null, \"categoryName\" : null, \"viewportPixelWidth\" : 1624, \"hitType\" : null, \"companyID\" : null, \"pageType\" : null, \"Userid\" : null, \"localPath\" : null, \"partyId\" : \"0:j1ymvypx:pO~iVklsXUYa6zMKVZeA2nC1YlVzTEw1\", \"codeProduct\" : null, \"is_newSession\" : false, \"GroupCode\" : null, \"sessionId\" : \"0:j20802wr:Py1qDrBry7UedH6my~6ebE58wRHUXWVp\", \"categoryCode\" : null, \"eventLabel\" :null, \"record_type\" : \"pageView\", \"n\" : null, \"record_id\" : \"0:vfBHTdrZCcFs3H6aO7Yb4pa80JiKQ7aW0\", \"q\" : null, \"userRoles\" : null, \"screenPixelWidth\" : 1855, \"viewportPixelHeight\" : 726, \"screenPixelHeight\" : 1056, \"is_PunchOut\" : null, \"h2kTimestamp\" : 1493286295730, \"pageViewId\" : \"0:vfBHTdrZCcFs3H6aO7Yb4pa80JiKQ7aW\", \"location\" : \"https://orexad.preprod.group-iph.com/fr/equipement/c-45\", \"record_time\" : 1493286300033, \"pointOfService\" : null }}"; - final JsonSerializer serializer = new JsonSerializer(); - ByteArrayInputStream bais = new ByteArrayInputStream(recordStr.getBytes()); - Record deserializedRecord = serializer.deserialize(bais); - assertTrue(deserializedRecord.getTime().getTime() == 1493286300033L); - } + final JsonSerializer serializer = new JsonSerializer(); + ByteArrayInputStream bais = new ByteArrayInputStream(recordStr.getBytes()); + Record deserializedRecord = serializer.deserialize(bais); + assertTrue(deserializedRecord.getTime().getTime() == 1493286300033L); + } } \ No newline at end of file diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/DebugStream.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/DebugStream.java index f320526c7..7e31a48ec 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/DebugStream.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/DebugStream.java @@ -56,11 +56,20 @@ public class DebugStream extends AbstractProcessor { .allowableValues(JSON, STRING) .build(); + public static final PropertyDescriptor RECORD_TYPES = new PropertyDescriptor.Builder() + .name("record.types") + .description("comma separated list of record to include. all if empty") + .required(false) + .addValidator(StandardValidators.COMMA_SEPARATED_LIST_VALIDATOR) + .defaultValue("") + .build(); + @Override public final List getSupportedPropertyDescriptors() { final List descriptors = new ArrayList<>(); descriptors.add(SERIALIZER); + descriptors.add(RECORD_TYPES); return Collections.unmodifiableList(descriptors); } @@ -68,6 +77,8 @@ public final List getSupportedPropertyDescriptors() { @Override public Collection process(final ProcessContext context, final Collection collection) { + + String[] recordTypesToInclude = context.getPropertyValue(RECORD_TYPES).asString().split(","); if (collection.size() != 0) { RecordSerializer serializer = null; if (context.getPropertyValue(SERIALIZER).asString().equals(JSON.getValue())) { diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java index 060269acf..03f6bbec6 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java @@ -22,10 +22,7 @@ import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.processor.AbstractProcessor; import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.record.FieldDictionary; -import com.hurence.logisland.record.FieldType; -import com.hurence.logisland.record.Record; -import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.record.*; import com.hurence.logisland.service.datastore.DatastoreClientService; import com.hurence.logisland.validator.StandardValidators; import delight.nashornsandbox.NashornSandbox; @@ -146,9 +143,18 @@ public abstract class AbstractNashornSandboxProcessor extends AbstractProcessor .build(); + public static final PropertyDescriptor OUTPUT_RECORD_TYPE = new PropertyDescriptor.Builder() + .name("output.record.type") + .description("the type of the output record") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue(RecordDictionary.EVENT) + .build(); + protected DatastoreClientService datastoreClientService; protected NashornSandbox sandbox; protected Map dynamicTagValuesMap; + protected String outputRecordType; @Override public List getSupportedPropertyDescriptors() { @@ -159,6 +165,7 @@ public List getSupportedPropertyDescriptors() { properties.add(MAX_PREPARED_STATEMENTS); properties.add(DATASTORE_CLIENT_SERVICE); properties.add(DATASTORE_CACHE_COLLECTION); + properties.add(OUTPUT_RECORD_TYPE); return properties; } @@ -216,6 +223,7 @@ public void init(ProcessContext context) { dynamicTagValuesMap = new HashMap<>(); + outputRecordType = context.getPropertyValue(OUTPUT_RECORD_TYPE).asString(); this.setupDynamicProperties(context); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java index 57ac9bea3..5ff9ca418 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java @@ -49,11 +49,20 @@ public class CheckAlerts extends AbstractNashornSandboxProcessor { .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); + public static final PropertyDescriptor ALERT_CRITICITY = new PropertyDescriptor.Builder() + .name("alert.criticity") + .description("from 0 to ...") + .required(false) + .defaultValue("0") + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .build(); + @Override public List getSupportedPropertyDescriptors() { List properties = new ArrayList<>(super.getSupportedPropertyDescriptors()); properties.add(PROFILE_ACTIVATION_CONDITION); + properties.add(ALERT_CRITICITY); return properties; } @@ -101,16 +110,24 @@ protected void setupDynamicProperties(ProcessContext context) { sandbox.allow(System.class); sandbox.allow(Date.class); + sandbox.allow(Double.class); String profileActivationRule = context.getPropertyValue(PROFILE_ACTIVATION_CONDITION).asString(); StringBuilder sbActivation = new StringBuilder(); sbActivation.append("var alert = false;\n") - .append("function getValue(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n") + .append("function getValue(id) {\n") + .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") + .append(" if(record == null) return Double.NaN;\n") + .append(" return record.getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n") .append("function getDuration(id) {\n") .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") + .append(" if(record == null) return -1;\n") .append(" var duration = new Date().getTime() - record.getTime().getTime();\n") .append(" return duration; \n};\n") - .append("function getCount(id) {\n return cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id)).getField(\"count\").asDouble(); \n};\n") + .append("function getCount(id) {\n") + .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") + .append(" if(record == null) return -1;\n") + .append(" return record.getField(\"record_count\").asLong(); \n};\n") .append("if( ") .append(expandCode(profileActivationRule)) .append(" ) { \n"); @@ -131,8 +148,8 @@ protected void setupDynamicProperties(ProcessContext context) { dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - System.out.println(sb.toString()); - logger.debug(sb.toString()); + // System.out.println(sb.toString()); + // logger.debug(sb.toString()); } @@ -147,16 +164,20 @@ public Collection process(ProcessContext context, Collection rec init(context); } - List outputRecords = new ArrayList<>(); + List outputRecords = new ArrayList<>(records); for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { try { sandbox.eval(entry.getValue()); Boolean alert = (Boolean) sandbox.get("alert"); if (alert) { - outputRecords.add(new StandardRecord(RecordDictionary.ALERT) + Record alertRecord = new StandardRecord(outputRecordType) .setId(entry.getKey()) - .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(entry.getKey()).asString())); + .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(entry.getKey()).asString()); + outputRecords.add(alertRecord); + + + logger.info(alertRecord.toString()); } } catch (ScriptException e) { Record errorRecord = new StandardRecord(RecordDictionary.ERROR) diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java index 900714531..35aa6ff3c 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java @@ -99,8 +99,8 @@ protected void setupDynamicProperties(ProcessContext context) { .append(" ) { match=true; }\n"); dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - System.out.println(sb.toString()); - logger.debug(sb.toString()); + // System.out.println(sb.toString()); + // logger.debug(sb.toString()); } defaultCollection = context.getPropertyValue(DATASTORE_CACHE_COLLECTION).asString(); recordTTL = context.getPropertyValue(RECORD_TTL).asInteger(); @@ -117,7 +117,7 @@ public Collection process(ProcessContext context, Collection rec init(context); } - List outputRecords = new ArrayList<>(); + List outputRecords = new ArrayList<>(records); for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { // look for record into the cache @@ -143,9 +143,11 @@ public Collection process(ProcessContext context, Collection rec cachedThreshold.setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, count + 1) .setTime(firstThresholdTime); + + datastoreClientService.put(defaultCollection, cachedThreshold, true); outputRecords.add(cachedThreshold); } else { - Record threshold = new StandardRecord(RecordDictionary.THRESHOLD) + Record threshold = new StandardRecord(outputRecordType) .setId(key) .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, 1L); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java index da60f17df..50282ece9 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java @@ -51,25 +51,34 @@ public class ComputeTags extends AbstractNashornSandboxProcessor { @Override protected void setupDynamicProperties(ProcessContext context) { + + StringBuilder sbActivation = new StringBuilder(); + sbActivation + .append("function getValue(id) {\n") + .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") + .append(" if(record == null) return Double.NaN;\n") + .append(" return record.getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n"); + + for (final Map.Entry entry : context.getProperties().entrySet()) { if (!entry.getKey().isDynamic()) { continue; } String key = entry.getKey().getName(); - String value = entry.getValue() - .replaceAll("cache\\((\\S*\\))", "cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId($1)") - .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); + String value = entry.getValue().replaceAll("cache\\((\\S*)\\).value", "getValue($1)"); - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(sbActivation); sb.append("function ") .append(key) .append("() { ") .append(value) - .append(" } \n"); + .append(" }; \n"); sb.append("var record_") .append(key) - .append(" = new com.hurence.logisland.record.StandardRecord()") + .append(" = new com.hurence.logisland.record.StandardRecord(\"") + .append(outputRecordType) + .append("\")") .append(".setId(\"") .append(key) .append("\");\n"); @@ -82,8 +91,8 @@ protected void setupDynamicProperties(ProcessContext context) { .append("());\n"); dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - System.out.println(sb.toString()); - logger.debug(sb.toString()); + // System.out.println(sb.toString()); + // logger.debug(sb.toString()); } } @@ -95,7 +104,7 @@ public Collection process(ProcessContext context, Collection rec init(context); } - List outputRecords = new ArrayList<>(); + List outputRecords = new ArrayList<>(records); for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java index b4a39a0d8..0b5a6ec68 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java @@ -34,8 +34,9 @@ public class CheckAlertsTest { + @Test - public void testMultipleRules() throws InitializationException { + public void testSyntax() throws InitializationException { // create the controller service and link it to the test processor final DatastoreClientService service = new MockDatastoreService(); @@ -47,13 +48,9 @@ public void testMultipleRules() throws InitializationException { runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); - runner.setProperty("avib1", "cache(\"cached_id1\").value > 12.0"); // ok - runner.setProperty("avib2", "cache(\"cached_id2\").value < 5"); // ok - runner.setProperty("avib3", "cache(\"cached_id3\").value < 5"); // ko runner.setProperty("avib4", "cache(\"noone\").value < 5"); // syntax error runner.setProperty("avib5", "brousoufparty++++"); // syntax error - runner.setProperty("avib6", "cache(\"cached_id1\").count > 4"); // ok - runner.setProperty("avib7", "cache(\"cached_id2\").duration > 10000"); // ok + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); runner.addControllerService(service.getIdentifier(), service); @@ -62,9 +59,38 @@ public void testMultipleRules() throws InitializationException { runner.assertValid(); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(4); + runner.assertOutputRecordsCount(0); runner.assertOutputErrorCount(2); + } + + @Test + public void testValueRules() throws InitializationException { + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(CheckAlerts.class); + runner.setProperty(CheckAlerts.MAX_CPU_TIME, "100"); + runner.setProperty(CheckAlerts.MAX_MEMORY, "12800000"); + runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); + runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); + runner.setProperty("avib1", "cache(\"cached_id1\").value > 12.0"); // ok + runner.setProperty("avib2", "cache(\"cached_id2\").value < 5"); // ok + runner.setProperty("avib3", "cache(\"cached_id3\").value < 5"); // ko + + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + runner.assertValid(); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(2); + runner.assertOutputErrorCount(0); + for (Record enriched : runner.getOutputRecords()) { if (enriched.getId().equals("avib1")) { assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asString(), "cache(\"cached_id1\").value > 12.0"); @@ -73,6 +99,61 @@ public void testMultipleRules() throws InitializationException { } + @Test + public void testCountRules() throws InitializationException { + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(CheckAlerts.class); + runner.setProperty(CheckAlerts.MAX_CPU_TIME, "100"); + runner.setProperty(CheckAlerts.MAX_MEMORY, "12800000"); + runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); + runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); + runner.setProperty("avib6", "cache(\"cached_id1\").count > 4"); // ok + + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + runner.assertValid(); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(1); + runner.assertOutputErrorCount(0); + + } + + @Test + public void testDurationRules() throws InitializationException { + + // create the controller service and link it to the test processor + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + final TestRunner runner = TestRunners.newTestRunner(CheckAlerts.class); + runner.setProperty(CheckAlerts.MAX_CPU_TIME, "100"); + runner.setProperty(CheckAlerts.MAX_MEMORY, "12800000"); + runner.setProperty(CheckAlerts.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(CheckAlerts.ALLOw_NO_BRACE, "false"); + runner.setProperty(CheckAlerts.PROFILE_ACTIVATION_CONDITION, "cache(\"cached_id1\").value > 10.0 && cache(\"cached_id2\").value >= 0"); + runner.setProperty("avib7", "cache(\"cached_id2\").duration > 10000"); // ok + + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + runner.assertValid(); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.assertOutputRecordsCount(1); + runner.assertOutputErrorCount(0); + + } + + private Collection getCacheRecords() { Collection lookupRecords = new ArrayList<>(); @@ -81,7 +162,7 @@ private Collection getCacheRecords() { lookupRecords.add(new StandardRecord() .setId("cached_id1") .setField(FieldDictionary.RECORD_VALUE, FieldType.DOUBLE, 12.45) - .setField("count", FieldType.LONG, 5)); + .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, 5L)); lookupRecords.add(new StandardRecord() .setId("cached_id2") diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index ca38d43c0..811f423a8 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -110,10 +110,11 @@ public void testBadRules() throws InitializationException { for (Record enriched : runner.getOutputRecords()) { if (enriched.getId().equals("cvib3")) { - assertEquals(enriched.getErrors().toArray()[0], - "ScriptException: :3:17 Invalid left hand side for assignment\n" + - " return 37.2 / ++10 * 3;\n" + - " ^ in at line number 3 at column number 17"); + assertEquals( + "ScriptException: :9:17 Invalid left hand side for assignment\n" + + " return 37.2 / ++10 * 3;\n" + + " ^ in at line number 9 at column number 17", + enriched.getErrors().toArray()[0]); } } } diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java index 11da01da0..7174c3af2 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java @@ -222,8 +222,12 @@ public Record get(final String key, final Serializer ke return withConnection(redisConnection -> { final byte[] k = serialize(key, keySerializer); final byte[] v = redisConnection.get(k); - InputStream input = new ByteArrayInputStream(v); - return valueDeserializer.deserialize(input); + if (v == null) { + return null; + }else { + InputStream input = new ByteArrayInputStream(v); + return valueDeserializer.deserialize(input); + } }); } @@ -284,14 +288,23 @@ private byte[][] getKeys(final List keys) { private Tuple serialize(final K key, final Record value, final Serializer keySerializer, final Serializer valueSerializer) throws IOException { final ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] k = null; - keySerializer.serialize(out, key); - final byte[] k = out.toByteArray(); - + try { + keySerializer.serialize(out, key); + k= out.toByteArray(); + }catch (Throwable t){ + // do nothing + } out.reset(); - valueSerializer.serialize(out, value); - final byte[] v = out.toByteArray(); + byte[] v = null; + try { + valueSerializer.serialize(out, value); + v = out.toByteArray(); + }catch (Throwable t){ + // do nothing + } return new Tuple<>(k, v); } From 650ea3d13e4a5990eedcb949b887f704a3300bef Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Sun, 3 Jun 2018 21:23:52 +0200 Subject: [PATCH 22/63] New Api: support stream pipeline dynamic change without touching spark context --- .../AbstractConfiguredComponent.java | 2 + .../logisland/component/ComponentContext.java | 10 +- .../logisland/engine/EngineContext.java | 4 +- .../engine/MockProcessingEngine.java | 17 +- .../logisland/engine/ProcessingEngine.java | 21 +- .../engine/StandardEngineContext.java | 15 +- .../logisland/processor/ProcessContext.java | 2 +- .../processor/StandardProcessContext.java | 4 - .../stream/StandardStreamContext.java | 11 +- .../logisland/stream/StreamContext.java | 11 +- ...PipelineConfigurationBroadcastWrapper.java | 67 +++ .../engine/spark/remote/RemoteApiClient.java | 178 +++++-- .../remote/RemoteApiComponentFactory.java | 95 +++- .../spark/remote/RemoteComponentRegistry.java | 145 ----- .../engine/spark/remote/model/Component.java | 23 +- .../engine/spark/remote/model/DataFlow.java | 146 ++++++ .../engine/spark/remote/model/Pipeline.java | 139 +---- .../engine/spark/remote/model/Processor.java | 6 +- .../engine/spark/remote/model/Property.java | 13 +- .../engine/spark/remote/model/Service.java | 2 +- .../engine/spark/remote/model/Stream.java | 47 +- .../engine/spark/remote/model/Versioned.java | 127 +++++ .../src/main/resources/api.yaml | 288 +++++----- .../spark/BaseStreamProcessingEngine.scala | 494 ------------------ .../spark/KafkaStreamProcessingEngine.scala | 490 ++++++++++++++++- .../RemoteApiStreamProcessingEngine.scala | 127 +++-- .../spark/AbstractKafkaRecordStream.scala | 87 +-- .../logisland/engine/RemoteApiEngineTest.java | 9 +- .../spark/remote/RemoteApiClientTest.java | 37 +- .../remote/RemoteComponentRegistryTest.java | 97 ---- .../src/test/resources/conf/remote-engine.yml | 47 +- .../logisland/component/ComponentFactory.java | 5 +- .../component/ComponentFactoryV2.java | 129 ----- .../config/v2/AbstractComponentConfig.java | 74 --- .../logisland/config/v2/ConfigReader.java | 48 -- .../logisland/config/v2/EngineConfig.java | 21 - .../logisland/config/v2/JobConfig.java | 90 ---- .../logisland/config/v2/ProcessorConfig.java | 39 -- .../logisland/config/v2/ServiceConfig.java | 56 -- .../config/v2/SinkProviderConfig.java | 20 - .../config/v2/SourceProviderConfig.java | 35 -- .../logisland/config/v2/StreamConfig.java | 74 --- .../util/runner/MockProcessContext.java | 16 +- 43 files changed, 1482 insertions(+), 1886 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java delete mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java create mode 100755 logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java delete mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala delete mode 100644 logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactoryV2.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/AbstractComponentConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ConfigReader.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/EngineConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/JobConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ProcessorConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ServiceConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SinkProviderConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SourceProviderConfig.java delete mode 100644 logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/StreamConfig.java diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/AbstractConfiguredComponent.java b/logisland-api/src/main/java/com/hurence/logisland/component/AbstractConfiguredComponent.java index b8bdd61db..45af34e02 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/AbstractConfiguredComponent.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/AbstractConfiguredComponent.java @@ -159,6 +159,8 @@ public Map getProperties() { } + + @Override public String getProperty(final PropertyDescriptor property) { return properties.get(property); diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java b/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java index 797d74ec0..41f63e98b 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -81,4 +81,6 @@ public interface ComponentContext extends ConfiguredComponent { * @return the configured name of this processor */ String getName(); + + } diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java index 08d6f4063..31912cdf4 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java @@ -20,10 +20,9 @@ import com.hurence.logisland.config.ControllerServiceConfiguration; import com.hurence.logisland.stream.StreamContext; -import java.io.Closeable; import java.util.Collection; -public interface EngineContext extends ComponentContext , Closeable { +public interface EngineContext extends ComponentContext { /** * @return retrieve the list of stream contexts @@ -38,7 +37,6 @@ public interface EngineContext extends ComponentContext , Closeable { void addStreamContext(StreamContext streamContext); - /** * @return the engine */ diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java b/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java index 284812f52..ae2e621fc 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -55,4 +55,13 @@ public void shutdown(EngineContext engineContext) { logger.info("engine shutdown"); } + @Override + public void awaitTermination(EngineContext engineContext) { + } + + @Override + public void reset(EngineContext engineContext) { + engineContext.getStreamContexts().clear(); + engineContext.getControllerServiceConfigurations().clear(); + } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java b/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java index 8cc91d9bd..5784809df 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,8 +31,21 @@ public interface ProcessingEngine extends ConfigurableComponent { /** * shutdown the engine with a context + * * @param engineContext */ void shutdown(EngineContext engineContext); + /** + * Await for termination. + * @param engineContext + */ + void awaitTermination(EngineContext engineContext); + + /** + * Reset the engine by stopping the streaming context. + * @param engineContext + */ + void reset(EngineContext engineContext); + } diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java index 450a511ee..729e4112a 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -23,7 +23,6 @@ import com.hurence.logisland.config.ControllerServiceConfiguration; import com.hurence.logisland.stream.StreamContext; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -93,10 +92,4 @@ public void addControllerServiceConfiguration(ControllerServiceConfiguration con controllerServiceConfigurations.add(config); } - @Override - public void close() throws IOException { - while (!streamContexts.isEmpty()) { - streamContexts.remove(0).close(); - } - } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java b/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java index 7f692e9b0..50b16516d 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/processor/ProcessContext.java @@ -22,7 +22,7 @@ import java.io.Closeable; -public interface ProcessContext extends ComponentContext, Closeable { +public interface ProcessContext extends ComponentContext { /** * Adds the given {@link ControllerServiceLookup} so that the diff --git a/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java b/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java index aab391c41..37d0064cc 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/processor/StandardProcessContext.java @@ -67,8 +67,4 @@ public void verifyModifiable() throws IllegalStateException { } - @Override - public void close() throws IOException { - controllerServiceLookup = null; - } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java b/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java index 8c6c91a5d..fe926c857 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java @@ -20,7 +20,7 @@ import com.hurence.logisland.controller.ControllerServiceLookup; import com.hurence.logisland.processor.ProcessContext; -import java.io.IOException; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -28,6 +28,7 @@ public class StandardStreamContext extends AbstractConfiguredComponent implements StreamContext { private final List processContexts = new ArrayList<>(); + private final Instant creationDate = Instant.now(); public StandardStreamContext(final RecordStream recordStream, final String id) { super(recordStream, id); @@ -89,11 +90,5 @@ public void verifyModifiable() throws IllegalStateException { } - @Override - public void close() throws IOException { - while (!processContexts.isEmpty()) { - processContexts.remove(0).close(); - } - controllerServiceLookup = null; - } + } diff --git a/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java b/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java index a68dfd057..237bb6c66 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,10 +20,9 @@ import com.hurence.logisland.controller.ControllerServiceLookup; import com.hurence.logisland.processor.ProcessContext; -import java.io.Closeable; import java.util.Collection; -public interface StreamContext extends ComponentContext, Closeable { +public interface StreamContext extends ComponentContext { /** * @return the Stream diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java new file mode 100644 index 000000000..05d8cdd11 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java @@ -0,0 +1,67 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote; + +import com.hurence.logisland.processor.ProcessContext; +import org.apache.spark.SparkContext; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.broadcast.Broadcast; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Map; + +/** + * A {@link Broadcast} wrapper for a Stream pipeline configuration. + * This class allow to magically synchronize data modified from the spark driver to every executor. + * + * @author amarziali + */ +public class PipelineConfigurationBroadcastWrapper { + private static final Logger logger = LoggerFactory.getLogger(PipelineConfigurationBroadcastWrapper.class); + + private Broadcast>> broadcastedPipelineMap; + + private static PipelineConfigurationBroadcastWrapper obj = new PipelineConfigurationBroadcastWrapper(); + + private PipelineConfigurationBroadcastWrapper() { + } + + public static PipelineConfigurationBroadcastWrapper getInstance() { + return obj; + } + + public JavaSparkContext getSparkContext(SparkContext sc) { + JavaSparkContext jsc = JavaSparkContext.fromSparkContext(sc); + return jsc; + } + + public void refresh(Map> pipelineMap, SparkContext sparkContext) { + logger.info("Refreshing dataflow pipelines!"); + + if (broadcastedPipelineMap != null) { + broadcastedPipelineMap.unpersist(); + } + broadcastedPipelineMap = getSparkContext(sparkContext).broadcast(pipelineMap); + } + + public Collection get(String streamName) { + return broadcastedPipelineMap.getValue().get(streamName); + } +} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index 76751a646..0c1e9df1e 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -20,11 +20,10 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.type.CollectionType; -import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.hurence.logisland.engine.spark.remote.model.Pipeline; +import com.hurence.logisland.engine.spark.remote.model.DataFlow; import okhttp3.*; +import okhttp3.internal.http.HttpDate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,23 +34,63 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import java.time.Duration; +import java.time.Instant; +import java.util.Date; import java.util.Iterator; -import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; /** - * Rest client wrapper for pipelines remote APIs. + * Rest client wrapper for logisland remote APIs. * * @author amarziali */ public class RemoteApiClient { + /** + * Conversation state. + */ + public static class State { + public Instant lastModified; + } + + /** + * Connection settings. + */ + public static class ConnectionSettings { + + private final String baseUrl; + private final Duration socketTimeout; + private final Duration connectTimeout; + private final String username; + private final String password; + + /** + * Constructs a new instance. + * If username and password are provided, the client will be configured to supply a basic authentication. + * + * @param baseUrl the base url + * @param socketTimeout the read/write socket timeout + * @param connectTimeout the connection socket timeout + * @param username the username if a basic authentication is needed. + * @param password the password if a basic authentication is needed. + */ + public ConnectionSettings(String baseUrl, Duration socketTimeout, Duration connectTimeout, String username, String password) { + this.baseUrl = baseUrl; + this.socketTimeout = socketTimeout; + this.connectTimeout = connectTimeout; + this.username = username; + this.password = password; + } + } + private static final Logger logger = LoggerFactory.getLogger(RemoteApiClient.class); - private static final String PIPELINES_RESOURCE_URI = "pipelines"; - private static final CollectionType pipelineType = TypeFactory.defaultInstance().constructCollectionType(List.class, Pipeline.class); + private static final String DATAFLOW_RESOURCE_URI = "dataflows"; + private static final String STREAM_RESOURCE_URI = "streams"; + + private static final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private final OkHttpClient client; @@ -60,18 +99,12 @@ public class RemoteApiClient { /** - * Constructs a new instance. - * If username and password are provided, the client will be configured to supply a basic authentication. + * Construct a new instance with provided connection settings. * - * @param baseUrl the base url - * @param socketTimeout the read/write socket timeout - * @param connectTimeout the connection socket timeout - * @param username the username if a basic authentication is needed. - * @param password the password if a basic authentication is needed. + * @param connectionSettings the {@link ConnectionSettings} */ - public RemoteApiClient(String baseUrl, Duration socketTimeout, Duration connectTimeout, - String username, String password) { - this.baseUrl = HttpUrl.parse(baseUrl); + public RemoteApiClient(ConnectionSettings connectionSettings) { + this.baseUrl = HttpUrl.parse(connectionSettings.baseUrl); this.mapper = new ObjectMapper(); mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); @@ -80,18 +113,18 @@ public RemoteApiClient(String baseUrl, Duration socketTimeout, Duration connectT OkHttpClient.Builder builder = new OkHttpClient() .newBuilder() - .readTimeout(socketTimeout.toMillis(), TimeUnit.MILLISECONDS) - .writeTimeout(socketTimeout.toMillis(), TimeUnit.MILLISECONDS) - .connectTimeout(connectTimeout.toMillis(), TimeUnit.MILLISECONDS) + .readTimeout(connectionSettings.socketTimeout.toMillis(), TimeUnit.MILLISECONDS) + .writeTimeout(connectionSettings.socketTimeout.toMillis(), TimeUnit.MILLISECONDS) + .connectTimeout(connectionSettings.connectTimeout.toMillis(), TimeUnit.MILLISECONDS) .followRedirects(true) .followSslRedirects(true); //add basic auth if needed. - if (username != null && password != null) { + if (connectionSettings.username != null && connectionSettings.password != null) { builder.addInterceptor(chain -> { Request originalRequest = chain.request(); Request requestWithBasicAuth = originalRequest .newBuilder() - .header(HttpHeaders.AUTHORIZATION, Credentials.basic(username, password)) + .header(HttpHeaders.AUTHORIZATION, Credentials.basic(connectionSettings.username, connectionSettings.password)) .build(); return chain.proceed(requestWithBasicAuth); }); @@ -99,33 +132,52 @@ public RemoteApiClient(String baseUrl, Duration socketTimeout, Duration connectT this.client = builder.build(); } + /** - * Fetches pipelines from a remote server. + * Generic method to fetch and validate a HTTP resource. * - * @return a list of {@link Pipeline} (never null). Empty in case of error or no results. + * @param url the resource Url. + * @param state the conversation state. + * @param resourceClass the bean model class. + * @param the type of the model data to return. + * @return an {@link Optional} bean containing requested validated data. */ - public Optional> fetchPipelines() { - Request request = new Request.Builder() - .url(baseUrl.newBuilder().addPathSegment(PIPELINES_RESOURCE_URI).build()) - .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) - .get() - .build(); - try (Response response = client.newCall(request).execute()) { - if (!response.isSuccessful()) { - logger.error("Error refreshing pipelines from remote server. Got code {}", response.code()); - } else { - List ret = mapper.readValue(response.body().byteStream(), pipelineType); - //validate against javax.validation annotations. - ret.forEach(RemoteApiClient::doValidate); - return Optional.of(ret); + private Optional doFetch(HttpUrl url, State state, Class resourceClass) { + Request.Builder request = new Request.Builder() + .url(url).addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); + + if (state.lastModified != null) { + request.addHeader(HttpHeaders.IF_MODIFIED_SINCE, HttpDate.format(new Date((state.lastModified.toEpochMilli())))); + } + + try (Response response = client.newCall(request.build()).execute()) { + if (response.code() != javax.ws.rs.core.Response.Status.NOT_MODIFIED.getStatusCode()) { + + if (!response.isSuccessful()) { + logger.error("Error refreshing {} from remote server. Got code {}", resourceClass.getCanonicalName(), response.code()); + } else { + String lm = response.header(HttpHeaders.LAST_MODIFIED); + if (lm != null) { + try { + Date tmp = HttpDate.parse(lm); + if (tmp != null) { + state.lastModified = tmp.toInstant(); + } + } catch (Exception e) { + logger.warn("Unable to correctly parse Last-Modified Header"); + } + } + T ret = mapper.readValue(response.body().byteStream(), resourceClass); + //validate against javax.validation annotations. + doValidate(ret); + return Optional.of(ret); + } } } catch (Exception e) { - logger.error("Unable to refresh pipelines from remote server", e); + logger.error("Unable to refresh dataflow from remote server", e); } return Optional.empty(); - - } /** @@ -134,7 +186,7 @@ public Optional> fetchPipelines() { * @param bean the instance to validate * @see javax.validation.Validator#validate */ - private static void doValidate(Object bean) { + private void doValidate(Object bean) { Set> result = validator.validate(bean); if (!result.isEmpty()) { StringBuilder sb = new StringBuilder("Bean validation failed: "); @@ -149,5 +201,47 @@ private static void doValidate(Object bean) { } } + /** + * Fetches dataflow from a remote server. + * + * @param dataflowName the name of the dataflow to fetch. + * @param state the conversation state (never null) + * @return a optional {@link DataFlow} (never null). Empty in case of error or no results. + */ + public Optional fetchDataflow(String dataflowName, State state) { + return doFetch(baseUrl.newBuilder().addPathSegment(DATAFLOW_RESOURCE_URI).addPathSegment(dataflowName).build(), + state, DataFlow.class); + } + + /** + * Push a dataflow configuration to a remote server. + * We do not care about http result code since the call is fire and forget. + * + * @param dataflowName the name of the dataflow to push + * @param dataFlow the item to push. + */ + public void pushDataFlow(String dataflowName, DataFlow dataFlow) { + try { + Request request = new Request.Builder() + .url(baseUrl.newBuilder() + .addPathSegment(DATAFLOW_RESOURCE_URI).addPathSegment(dataflowName) + .build()) + .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .post(RequestBody.create( + okhttp3.MediaType.parse(MediaType.APPLICATION_JSON), + mapper.writeValueAsBytes(dataFlow))) + .build(); + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + logger.warn("Expected application to answer with 200 OK. Got {}", response.code()); + } + } + + + } catch (Exception e) { + logger.warn("Unexpected exception trying to push latest dataflow configuration", e); + } + } + } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java index c6a755e3e..462bdedf9 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -19,16 +19,18 @@ import com.hurence.logisland.config.ControllerServiceConfiguration; import com.hurence.logisland.engine.EngineContext; -import com.hurence.logisland.engine.StandardEngineContext; import com.hurence.logisland.engine.spark.remote.model.*; import com.hurence.logisland.processor.ProcessContext; import com.hurence.logisland.processor.StandardProcessContext; import com.hurence.logisland.stream.RecordStream; import com.hurence.logisland.stream.StandardStreamContext; import com.hurence.logisland.stream.StreamContext; +import org.apache.spark.SparkContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -43,25 +45,6 @@ public class RemoteApiComponentFactory { private static final Logger logger = LoggerFactory.getLogger(RemoteApiComponentFactory.class); - /** - * Create a child isolated engine context for a pipeline sharing the engine processor but having separated streams, - * processor and services. - * - * @param engineContext the master engine context - * @param pipeline the pipeline. - * @return a child {@link EngineContext} - */ - public EngineContext createScopedEngineContext(EngineContext engineContext, Pipeline pipeline) { - EngineContext ret = new StandardEngineContext(engineContext.getEngine(), pipeline.getName()); - ret.getProperties().forEach((k, v) -> { - if (v != null) { - ret.setProperty(k.getName(), v); - } - }); - return ret; - } - - /** * Instantiates a stream from of configuration * @@ -76,12 +59,13 @@ public Optional getStreamContext(Stream stream) { new StandardStreamContext(recordStream, stream.getName()); // instantiate each related processor - stream.getProcessors().forEach(processor -> { + stream.getPipeline().getProcessors().forEach(processor -> { Optional processorContext = getProcessContext(processor); if (processorContext.isPresent()) instance.addProcessContext(processorContext.get()); }); + // set the config properties stream.getConfig().forEach(e -> instance.setProperty(e.getKey(), e.getValue())); @@ -145,4 +129,71 @@ public Optional getControllerServiceConfiguratio return Optional.empty(); } -} + + /** + * Updates the state of the engine if needed. + * + * @param sparkContext the spark context + * @param engineContext the engineContext + * @param dataflow the new dataflow (new state) + * @param oldDataflow latest dataflow dataflow. + */ + public void updateEngineContext(SparkContext sparkContext, EngineContext engineContext, DataFlow dataflow, DataFlow oldDataflow) { + if (oldDataflow == null || oldDataflow.getLastModified().isBefore(dataflow.getLastModified())) { + logger.info("We have a new configuration. Resetting current engine"); + engineContext.getEngine().reset(engineContext); + logger.info("Configuring dataflow. Last change at {} is {}", dataflow.getLastModified(), dataflow.getModificationReason()); + dataflow.getServices().stream() + .map(this::getControllerServiceConfiguration) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(engineContext::addControllerServiceConfiguration); + dataflow.getStreams().stream() + .map(this::getStreamContext) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(engineContext::addStreamContext); + logger.info("Restarting engine"); + try { + PipelineConfigurationBroadcastWrapper.getInstance().refresh( + engineContext.getStreamContexts().stream() + .collect(Collectors.toMap(StreamContext::getIdentifier, StreamContext::getProcessContexts)) + , sparkContext); + updatePipelines(sparkContext, dataflow); + engineContext.getEngine().start(engineContext); + } catch (Exception e) { + logger.error("Unable to start engine. Logisland state may be inconsistent. Trying to recover. Caused by", e); + engineContext.getEngine().reset(engineContext); + } + } else { + //need to update pipelines? + if (dataflow.getStreams().stream() + .anyMatch(s -> { + Optional old = oldDataflow.getStreams().stream() + .filter(t -> t.getName().equals(s.getName())).findFirst(); + return old.isPresent() && old.get() != null && + old.get().getPipeline().getLastModified().isBefore(s.getPipeline().getLastModified()); + })) { + updatePipelines(sparkContext, dataflow); + } + } + + } + + /** + * Update pipelines. + * + * @param sparkContext the spark context + * @param dataflow the dataflow + */ + public void updatePipelines(SparkContext sparkContext, DataFlow dataflow) { + Map> pipelineMap = dataflow.getStreams().stream() + .collect(Collectors.toMap(Stream::getName, + s -> s.getPipeline().getProcessors().stream().map(this::getProcessContext) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()))); + PipelineConfigurationBroadcastWrapper.getInstance().refresh(pipelineMap, sparkContext); + } + +} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java deleted file mode 100644 index 155507887..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistry.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.hurence.logisland.engine.spark.remote; - -import com.hurence.logisland.engine.EngineContext; -import com.hurence.logisland.engine.spark.remote.model.Pipeline; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -/** - * Stateful component registry. - * - * @author amarziali - */ -public class RemoteComponentRegistry { - - private static final Logger logger = LoggerFactory.getLogger(RemoteComponentRegistry.class); - - private final EngineContext engineContext; - private final Map childContextes = new HashMap<>(); - private final Map pipelineMap = new HashMap<>(); - - private final RemoteApiComponentFactory remoteApiComponentFactory = new RemoteApiComponentFactory(); - - /** - * Create an instance. - * - * @param engineContext the master engine (initial state). - */ - public RemoteComponentRegistry(EngineContext engineContext) { - this.engineContext = engineContext; - } - - private void stopPipeline(Pipeline pipeline) { - EngineContext ctx = childContextes.remove(pipeline.getName()); - pipelineMap.remove(pipeline.getName()); - logger.info("Stopping everything for pipeline {}", ctx.getName()); - ctx.getStreamContexts().forEach(streamContext -> { - logger.info("Pipeline {} : stopping stream {}", pipeline.getName(), streamContext.getName()); - try { - streamContext.getStream().stop(); - logger.info("Pipeline {} : successfully stopped stream {}", pipeline.getName(), streamContext.getName()); - } catch (Exception e) { - logger.error("Pipeline {} : unexpected error stopping stream {}: {}", pipeline.getName(), streamContext.getName(), e.getMessage()); - } - }); - try { - engineContext.close(); - } catch (IOException e) { - logger.warn("Unable to properly close engine " + engineContext.getName(), e); - } - logger.info("Pipeline {} stopped", ctx.getName()); - - } - - private void startPipeline(Pipeline pipeline) { - { - logger.info("Creating engine for pipeline {}", pipeline.getName()); - //create a new scoped engine context corresponding to the pipeline - EngineContext ec = remoteApiComponentFactory.createScopedEngineContext(engineContext, pipeline); - //add every service controller it needs - pipeline.getServices().stream() - .map(remoteApiComponentFactory::getControllerServiceConfiguration) - .filter(Optional::isPresent) - .map(Optional::get) - .forEach(ec::addControllerServiceConfiguration); - //now instantiate every stream - pipeline.getStreams().stream() - .map(remoteApiComponentFactory::getStreamContext) - .filter(Optional::isPresent) - .map(Optional::get) - .forEach(ec::addStreamContext); - //start it - try { - logger.info("Starting engine for pipeline {}", pipeline.getName()); - ec.getEngine().start(ec); - //now add the engine context to the registry - childContextes.put(pipeline.getName(), ec); - pipelineMap.put(pipeline.getName(), pipeline); - logger.info("Pipeline {} successfully started", pipeline.getName()); - } catch (Exception e) { - logger.error("Unable to properly start pipeline " + pipeline.getName(), e); - try { - logger.info("Shutting down pipeline {}", pipeline.getName()); - ec.getEngine().shutdown(ec); - logger.info("Pipeline {} successfully shut down", pipeline.getName()); - } catch (Exception e1) { - logger.warn("Unable to properly shut down pipeline " + pipeline.getName(), e1); - } - } - } - } - - - private Optional findInPipelines(Collection list, Pipeline item) { - return list.stream().filter(p -> p.getName().equals(item.getName())).findFirst(); - } - - - /** - * Updates the state of the engine by adding / removing pipelines according to the new provided config. - * - * @param pipelines the list of pipelines (new state) - */ - public void updateEngineContext(Collection pipelines) { - //remove missing or outdated items - pipelineMap.values().stream() - .filter(pipeline -> { - Optional found = findInPipelines(pipelines, pipeline); - return !found.isPresent() || pipeline.getLastModified().isBefore(found.get().getLastModified()); - }) - .collect(Collectors.toList()) - //remove active streams inside this pipeline - .forEach(this::stopPipeline); - - //add new items - pipelines.stream() - .filter(pipeline -> !pipelineMap.containsKey(pipeline.getName())) - .collect(Collectors.toList()) - .forEach(this::startPipeline); - - } -} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java index bc0022d04..323911106 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java @@ -29,15 +29,13 @@ /** * Component */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") public class Component { @JsonProperty("name") - @NotNull private String name = null; @JsonProperty("component") - @NotNull private String component = null; @JsonProperty("documentation") @@ -45,8 +43,7 @@ public class Component { @JsonProperty("config") @Valid - @NotNull - private List config = new ArrayList(); + private List config = new ArrayList<>(); public Component name(String name) { this.name = name; @@ -59,6 +56,9 @@ public Component name(String name) { * @return name **/ @ApiModelProperty(required = true, value = "") + @NotNull + + public String getName() { return name; } @@ -78,6 +78,9 @@ public Component component(String component) { * @return component **/ @ApiModelProperty(required = true, value = "") + @NotNull + + public String getComponent() { return component; } @@ -97,6 +100,8 @@ public Component documentation(String documentation) { * @return documentation **/ @ApiModelProperty(value = "") + + public String getDocumentation() { return documentation; } @@ -111,6 +116,9 @@ public Component config(List config) { } public Component addConfigItem(Property configItem) { + if (this.config == null) { + this.config = new ArrayList(); + } this.config.add(configItem); return this; } @@ -120,7 +128,10 @@ public Component addConfigItem(Property configItem) { * * @return config **/ - @ApiModelProperty(required = false, value = "") + @ApiModelProperty(value = "") + + @Valid + public List getConfig() { return config; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java new file mode 100755 index 000000000..2f0179f6f --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java @@ -0,0 +1,146 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * A streaming pipeline. + */ +@ApiModel(description = "A streaming pipeline.") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") + +public class DataFlow extends Versioned { + @JsonProperty("services") + @Valid + private List services = new ArrayList<>(); + + @JsonProperty("streams") + @Valid + private List streams = new ArrayList<>(); + + public DataFlow services(List services) { + this.services = services; + return this; + } + + public DataFlow addServicesItem(Service servicesItem) { + if (this.services == null) { + this.services = new ArrayList(); + } + this.services.add(servicesItem); + return this; + } + + /** + * The service controllers. + * + * @return services + **/ + @ApiModelProperty(value = "The service controllers.") + + @Valid + + public List getServices() { + return services; + } + + public void setServices(List services) { + this.services = services; + } + + public DataFlow streams(List streams) { + this.streams = streams; + return this; + } + + public DataFlow addStreamsItem(Stream streamsItem) { + if (this.streams == null) { + this.streams = new ArrayList(); + } + this.streams.add(streamsItem); + return this; + } + + /** + * The engine properties. + * + * @return streams + **/ + @ApiModelProperty(value = "The engine properties.") + + @Valid + + public List getStreams() { + return streams; + } + + public void setStreams(List streams) { + this.streams = streams; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DataFlow dataFlow = (DataFlow) o; + return Objects.equals(this.services, dataFlow.services) && + Objects.equals(this.streams, dataFlow.streams) && + super.equals(o); + } + + @Override + public int hashCode() { + return Objects.hash(services, streams, super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class DataFlow {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" services: ").append(toIndentedString(services)).append("\n"); + sb.append(" streams: ").append(toIndentedString(streams)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java index 655a88366..84ad7b704 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java @@ -22,132 +22,54 @@ import io.swagger.annotations.ApiModelProperty; import javax.validation.Valid; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; -import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** - * A streaming pipeline. + * Tracks stream processing pipeline configuration */ -@ApiModel(description = "A streaming pipeline.") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") -public class Pipeline { - @JsonProperty("name") - @NotNull - private String name = null; - - @JsonProperty("lastModified") - @NotNull - private OffsetDateTime lastModified = null; - - @JsonProperty("services") - @Valid - private List services = null; +@ApiModel(description = "Tracks stream processing pipeline configuration") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") - @JsonProperty("streams") +public class Pipeline extends Versioned { + @JsonProperty("processors") @Valid - @Size(min = 1) - @NotNull - private List streams = null; + private List processors = new ArrayList<>(); - public Pipeline name(String name) { - this.name = name; + public Pipeline processors(List processors) { + this.processors = processors; return this; } - /** - * The pipeline name - * - * @return name - **/ - @ApiModelProperty(required = true, value = "The pipeline name") - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Pipeline lastModified(OffsetDateTime lastModified) { - this.lastModified = lastModified; - return this; - } - - /** - * the last modified timestamp of this pipeline (used to trigger changes). - * - * @return lastModified - **/ - @ApiModelProperty(required = true, value = "the last modified timestamp of this pipeline (used to trigger changes).") - public OffsetDateTime getLastModified() { - return lastModified; - } - - public void setLastModified(OffsetDateTime lastModified) { - this.lastModified = lastModified; - } - - public Pipeline services(List services) { - this.services = services; - return this; - } - - public Pipeline addServicesItem(Service servicesItem) { - if (this.services == null) { - this.services = new ArrayList(); + public Pipeline addProcessorsItem(Processor processorsItem) { + if (this.processors == null) { + this.processors = new ArrayList(); } - this.services.add(servicesItem); + this.processors.add(processorsItem); return this; } /** - * The service controllers. + * Get processors * - * @return services + * @return processors **/ - @ApiModelProperty(value = "The service controllers.") - public List getServices() { - return services; - } - - public void setServices(List services) { - this.services = services; - } + @ApiModelProperty(value = "") - public Pipeline streams(List streams) { - this.streams = streams; - return this; - } - - public Pipeline addStreamsItem(Stream streamsItem) { - if (this.streams == null) { - this.streams = new ArrayList(); - } - this.streams.add(streamsItem); - return this; - } + @Valid - /** - * The engine properties. - * - * @return streams - **/ - @ApiModelProperty(value = "The engine properties.") - public List getStreams() { - return streams; + public List getProcessors() { + return processors; } - public void setStreams(List streams) { - this.streams = streams; + public void setProcessors(List processors) { + this.processors = processors; } @Override - public boolean equals(java.lang.Object o) { + public boolean equals(Object o) { if (this == o) { return true; } @@ -155,26 +77,21 @@ public boolean equals(java.lang.Object o) { return false; } Pipeline pipeline = (Pipeline) o; - return Objects.equals(this.name, pipeline.name) && - Objects.equals(this.lastModified, pipeline.lastModified) && - Objects.equals(this.services, pipeline.services) && - Objects.equals(this.streams, pipeline.streams); + return Objects.equals(this.processors, pipeline.processors) && + super.equals(o); } @Override public int hashCode() { - return Objects.hash(name, lastModified, services, streams); + return Objects.hash(processors, super.hashCode()); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Pipeline {\n"); - - sb.append(" name: ").append(toIndentedString(name)).append("\n"); - sb.append(" lastModified: ").append(toIndentedString(lastModified)).append("\n"); - sb.append(" services: ").append(toIndentedString(services)).append("\n"); - sb.append(" streams: ").append(toIndentedString(streams)).append("\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append(" processors: ").append(toIndentedString(processors)).append("\n"); sb.append("}"); return sb.toString(); } @@ -183,7 +100,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(java.lang.Object o) { + private String toIndentedString(Object o) { if (o == null) { return "null"; } @@ -191,5 +108,3 @@ private String toIndentedString(java.lang.Object o) { } } - - diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java index 73798ad92..a4452eaf3 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java @@ -22,10 +22,10 @@ import java.util.Objects; /** - * A logisland 'controller service'. + * A logisland 'processor'. */ -@ApiModel(description = "A logisland 'controller service'.") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") +@ApiModel(description = "A logisland 'processor'.") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") public class Processor extends Component { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java index 64aff47aa..12c88fb30 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java @@ -26,19 +26,16 @@ /** * Property */ - -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") public class Property { @JsonProperty("key") - @NotNull private String key = null; @JsonProperty("type") private String type = "string"; @JsonProperty("value") - @NotNull private String value = null; public Property key(String key) { @@ -52,6 +49,9 @@ public Property key(String key) { * @return key **/ @ApiModelProperty(required = true, value = "") + @NotNull + + public String getKey() { return key; } @@ -71,6 +71,8 @@ public Property type(String type) { * @return type **/ @ApiModelProperty(value = "") + + public String getType() { return type; } @@ -90,6 +92,9 @@ public Property value(String value) { * @return value **/ @ApiModelProperty(required = true, value = "") + @NotNull + + public String getValue() { return value; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java index 3795906e4..f98169d97 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java @@ -25,7 +25,7 @@ * A logisland 'controller service'. */ @ApiModel(description = "A logisland 'controller service'.") -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") public class Service extends Component { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java index 0e174fa35..c67ea3a77 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java @@ -21,50 +21,41 @@ import io.swagger.annotations.ApiModelProperty; import javax.validation.Valid; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; /** * Stream */ -@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-05-24T14:20:39.061Z") - +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") public class Stream extends Component { - @JsonProperty("processors") - @Valid - private List processors = null; + @JsonProperty("pipeline") + private Pipeline pipeline = null; - public Stream processors(List processors) { - this.processors = processors; - return this; - } - - public Stream addProcessorsItem(Processor processorsItem) { - if (this.processors == null) { - this.processors = new ArrayList(); - } - this.processors.add(processorsItem); + public Stream pipeline(Pipeline pipeline) { + this.pipeline = pipeline; return this; } /** - * Get processors + * Get pipeline * - * @return processors + * @return pipeline **/ @ApiModelProperty(value = "") - public List getProcessors() { - return processors; + + @Valid + + public Pipeline getPipeline() { + return pipeline; } - public void setProcessors(List processors) { - this.processors = processors; + public void setPipeline(Pipeline pipeline) { + this.pipeline = pipeline; } @Override - public boolean equals(Object o) { + public boolean equals(java.lang.Object o) { if (this == o) { return true; } @@ -72,13 +63,13 @@ public boolean equals(Object o) { return false; } Stream stream = (Stream) o; - return Objects.equals(this.processors, stream.processors) && + return Objects.equals(this.pipeline, stream.pipeline) && super.equals(o); } @Override public int hashCode() { - return Objects.hash(processors, super.hashCode()); + return Objects.hash(pipeline, super.hashCode()); } @Override @@ -86,7 +77,7 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class Stream {\n"); sb.append(" ").append(toIndentedString(super.toString())).append("\n"); - sb.append(" processors: ").append(toIndentedString(processors)).append("\n"); + sb.append(" pipeline: ").append(toIndentedString(pipeline)).append("\n"); sb.append("}"); return sb.toString(); } @@ -95,7 +86,7 @@ public String toString() { * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ - private String toIndentedString(Object o) { + private String toIndentedString(java.lang.Object o) { if (o == null) { return "null"; } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java new file mode 100755 index 000000000..67b9f24c8 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java @@ -0,0 +1,127 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.engine.spark.remote.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * a versioned component + */ +@ApiModel(description = "a versioned component") +@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2018-06-03T13:00:49.942Z") + +public class Versioned { + @JsonProperty("lastModified") + private OffsetDateTime lastModified = null; + + @JsonProperty("modificationReason") + private String modificationReason = null; + + public Versioned lastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; + return this; + } + + /** + * the last modified timestamp of this pipeline (used to trigger changes). + * + * @return lastModified + **/ + @ApiModelProperty(required = true, value = "the last modified timestamp of this pipeline (used to trigger changes).") + @NotNull + + @Valid + + public OffsetDateTime getLastModified() { + return lastModified; + } + + public void setLastModified(OffsetDateTime lastModified) { + this.lastModified = lastModified; + } + + public Versioned modificationReason(String modificationReason) { + this.modificationReason = modificationReason; + return this; + } + + /** + * Can be used to document latest changeset. + * + * @return modificationReason + **/ + @ApiModelProperty(value = "Can be used to document latest changeset.") + + + public String getModificationReason() { + return modificationReason; + } + + public void setModificationReason(String modificationReason) { + this.modificationReason = modificationReason; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Versioned versioned = (Versioned) o; + return Objects.equals(this.lastModified, versioned.lastModified) && + Objects.equals(this.modificationReason, versioned.modificationReason); + } + + @Override + public int hashCode() { + return Objects.hash(lastModified, modificationReason); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Versioned {\n"); + + sb.append(" lastModified: ").append(toIndentedString(lastModified)).append("\n"); + sb.append(" modificationReason: ").append(toIndentedString(modificationReason)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml index d3ca5841b..333a8d1d7 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml @@ -3,29 +3,46 @@ info: version: v1 title: Logisland standard API description: >- - The Logisland REST API for third party applications. + **The Logisland REST API for third party applications.** The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to: - Ask for configuration changes to be triggered. - - Report the latest configuration applied (to ease up resynchronisation and business continuity). + - Report the latest configuration applied (to ease up resynchronization and business continuity). - In terms of APIs, two degrees of freedom are possible: - - Dataflows (mandatory): + *As a general rule, the changes will be triggered if the **lastUpdated** field of the object you are going to modify is fresher than the one known by logisland.* + + + In terms of API, two degrees of freedom are possible: + + - **Dataflow**: A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). - Use the dataflow api if you want to keep a high level point of view on your data operations. In this case, every stream created in a dataflow box will be destroyed if any change occours inside the stream itself (e.g. A processor configuration change.) + Act at dataflow level if you want to: + + - Add/Remove any streaming endpoint + - Change any active stream configuration (e.g. kafka topic) + - Create/Remote/Modify any service + + + - **Pipeline**: + A pipeline is a processing chain acting on a data flowing point-to-point. - - Streams (optional): - Obviously, you can have a finer-grained control of what is going on inside your streams! - In case you marked your dataflow as mutable Logisland will poll as well on the streams API endpoint in order to know if any stream has been affected by some configuration change. + The api gives you the possibility to have a finer-grained control of what is going of any stream pipeline without perturbing the stream itself. + This means that the processor chain will be dynamically reconfigured without the need of stopping the stream and reconfigure the whole dataflow. - In this case, the processor chain will be dynamically reconfigured without the need of stopping the stream and restarting a new one. + Act at pipeline level if you want to: + + - Add/Remove processors in the pipeline + + - Change any processor configuration + + *Please note that if you need to add/remove controller services you must act your changes at dataflow level.* contact: name: Hurence @@ -39,41 +56,129 @@ consumes: produces: - application/json paths: - /{jobId}/dataflows: + /dataflows/{dataflowName}: + parameters: + - name: dataflowName + in: path + type: string + required: true + description: the dataflow name (aka the logisland job name) get: tags: - - dataflows - operationId: pollActiveDataflows - summary: Retrieves the data flow to run. + - dataflow + operationId: pollDataflowConfiguration + summary: Retrieves the configuration for a specified dataflow description: >- - A dataflow is a set of services and streams allowing a data flowing from one or more sources, - being transformed and reach one or more destinations (sinks). + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). + + Logisland will call this endpoint to know which configuration should be run. + + This endpoint also supports HTTP caching (Last-Updated, If-Modified-Since) as per RFC 7232, section 3.3 parameters: - - name: jobId - in: path + - name: If-Modified-Since + in: header type: string - required: true - description: logisland job id (aka the engine name) + description: Timestamp of last response + required: false + responses: "200": description: >- - should return every pipeline that should be running. - On server side, logisland will do the delta and apply the following: + Return the dataflow configuration. + + On logisland side, the following will happen: + + - At dataflow level: + + - Fully reconfigure a dataflow (stop and then start) if nothing is running (initial state) or if lastUpdated is fresher than the one of the already running dataflow. + + In this case be aware that old stream and services will be destroyed and + new ones will be created. + + - Do nothing otherwise (keep running the active dataflow) - - Add a new dataflow if any provided is not already running. - In this case streams and services will be created. + - At pipeline level: - - Fully reconfigure a dataflow (stop and then start) if another one with - the same name is already running but its lastUpdated is older than the one provided. + - The processor chain will be fully reconfigured if and only if the pipeline lastUpdated is fresher than the lastUpdated known by the system. - In this case be aware that old stream and services will be destroyed and - new ones will be created. + In any case the stream is never stopped. - - Remove a pipeline if running but no more present in returned ones. + headers: + Last-Updated: + type: string + description: Should be used for subsequent requests as If-Modified-Since request header. schema: - type: array - items: - $ref: '#/definitions/DataFlow' + $ref: '#/definitions/DataFlow' + examples: + A single stream dataflow: + lastModified: '1983-06-04T10:00.000Z' + modificationReason: Index Apache Logs again + services: + - component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService + documentation: elasticsearch service to sink records + name: elasticsearch_service + config: + - key: hosts + value: eshost:9300 + - key: cluster.name + value: escluster + streams: + - name: kafka_in + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + config: + - key: kafka.input.topics + value: logisland_raw + - key: kafka.output.topics + value: logisland_events + - key: kafka.error.topics + value: logisland_errors + - key: kafka.input.topics.serializer + value: none + - key: kafka.output.topics.serializer + value: com.hurence.logisland.serializer.KryoSerializer + - key: kafka.error.topics.serializer + value: com.hurence.logisland.serializer.JsonSerializer + - key: kafka.metadata.broker.list + value: sandbox:9092 + - key: kafka.zookeeper.quorum + value: sandbox:2181 + - key: kafka.topic.autoCreate + value: 'true' + - key: kafka.topic.default.partitions + value: '4' + - key: kafka.topic.default.replicationFactor + value: '1' + pipeline: + lastModified: '1983-06-04T10:00.000Z' + modificationReason: Initial configuration + processors: + - component: com.hurence.logisland.processor.SplitText + name: apache_parser + documentation: parse apache logs with a regexp + config: + - key: record.type + value: apache_log + - key: value.regex + value: (\S+)\s+(\S+)\s+(\S+)\s+\[([\w:\/]+\s[+\-]\d{4})\]\s+"(\S+)\s+(\S+)\s*(\S*)"\s+(\S+)\s+(\S+) + - key: value.fields + value: src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out + - component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch + documentation: a processor that indexes processed events in elasticsearch + name: es_publisher + config: + - key: elasticsearch.client.service + value: elasticsearch_service + - key: default.index + value: logisland + - key: default.type + value: event + - key: timebased.index + value: yesterday + - key: es.index.field + value: search_index + - key: es.type.field + value: record_type + "304": description: | Nothing has been modified since the last call. @@ -82,18 +187,25 @@ paths: (hence the server can answer with an empty body to save network and resources). "404": - description: Not found (the server probably does not handle this job) + description: Not found (the server probably does not handle this dataflow) default : description: Unexpected error post: tags: - - dataflows - operationId: updateCurrentConfiguration - summary: Communicates the configuration of running dataflows. + - dataflow + operationId: notifyDataflowConfiguration + summary: Push the configuration of running dataflows. description: >- - In order to ensure business continuity, Logisland will regularly contact the third party application in order to communicate a snapshot of the current configuration. + In order to ensure business continuity, Logisland will contact the third party application in order to push a snapshot of the current configuration. + + The endpoint will be called: + + - On a regular basis (according to logisland configuration). + + - Each time the a dataflow or a pipeline configuration change has been applied. - This service can be seen as well as a ping. + + This service can be seen as well as a liveness ping. parameters: - name: jobId in: path @@ -101,70 +213,17 @@ paths: required: true description: logisland job id (aka the engine name) - in: body - name: dataflows + name: dataflow required: true schema: - type: array - items: - $ref: '#/definitions/DataFlow' + $ref: '#/definitions/DataFlow' responses: default : description: | The server should return HTTP 200 OK. By the way, the response is ignored by Logisland since the operation - has a fire and forget nature. - - - /{jobId}/dataflows/{dataflowId}/streams/: - get: - tags: - - streams - operationId: pollStreamConfigurationChanges - summary: Retrieves the - description: Logisland will poll this API in order to start, reconfigure or stop the pipelines according to the received response. - parameters: - - name: jobId - in: path - type: string - required: true - description: logisland job id (aka the engine name) - - name: dataflowId - in: path - type: string - required: true - description: The name of the dataflow - - name: streamId - in: path - type: string - required: true - description: the name of the stream - - responses: - "200": - description: >- - should return every pipeline that should be running. - On server side, logisland will do the delta and apply the following: + has a *fire and forget* nature. - - Add a new pipeline if any provided is not already running. - - - Reconfigure a pipeline (stop and then start) if same pipeline already running and but its lastUpdated is older than the one provided - - - Remove a pipeline if running but no more present in returned ones. - schema: - type: array - items: - $ref: '#/definitions/StreamConfiguration' - "304": - description: | - Nothing has been modified since the last call. - - In this case the body content will be completely ignored - (hence the server can answer with an empty body to save network and resources). - - "404": - description: Not found (the server probably does not handle this job) - default : - description: Unexpected error definitions: @@ -212,43 +271,23 @@ definitions: allOf: - $ref: '#/definitions/Component' - Stream: - type: object - allOf: - - $ref: '#/definitions/Component' - - properties: - mutable: - type: boolean - description: >- - if the stream is mutable, logisland will poll for changes. - - See API /{jobId}/dataflows/{dataflowId}/stream/{streamId} for - more information. - - default: false - processors: - type: array - items: - $ref: '#/definitions/Processor' - Versioned: type: object description: a versioned component properties: - name: - type: string - description: The pipeline name lastModified: type: string format: date-time description: the last modified timestamp of this pipeline (used to trigger changes). + modificationReason: + type: string + description: Can be used to document latest changeset. required: - - name - lastModified - StreamConfiguration: + Pipeline: type: object - description: Tracks versioned stream configurations. + description: Tracks stream processing pipeline configuration allOf: - $ref: '#/definitions/Versioned' - properties: @@ -257,6 +296,18 @@ definitions: items: $ref: '#/definitions/Processor' + Stream: + type: object + allOf: + - $ref: '#/definitions/Component' + - properties: + pipeline: + $ref: '#/definitions/Pipeline' + required: + - pipeline + + + DataFlow: type: object @@ -272,7 +323,6 @@ definitions: streams: type: array description: The engine properties. - minItems: 1 items: $ref: '#/definitions/Stream' diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala deleted file mode 100644 index 1ffd744ee..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/BaseStreamProcessingEngine.scala +++ /dev/null @@ -1,494 +0,0 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.hurence.logisland.engine.spark - -import java.util -import java.util.Collections -import java.util.regex.Pattern - -import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} -import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} -import com.hurence.logisland.stream.spark.SparkRecordStream -import com.hurence.logisland.util.spark.SparkUtils -import com.hurence.logisland.validator.StandardValidators -import org.apache.spark.groupon.metrics.UserMetricsSystem -import org.apache.spark.streaming.{Milliseconds, StreamingContext} -import org.apache.spark.{SparkConf, SparkContext} -import org.slf4j.LoggerFactory - -import scala.collection.JavaConversions._ - - -object BaseStreamProcessingEngine { - - val SPARK_MASTER = new PropertyDescriptor.Builder() - .name("spark.master") - .description("The url to Spark Master") - .required(true) - // The regex allows "local[K]" with K as an integer, "local[*]", "yarn", "yarn-client", "yarn-cluster" and "spark://HOST[:PORT]" - // there is NO support for "mesos://HOST:PORT" - .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^(yarn(-(client|cluster))?|local\\[[0-9\\*]+\\]|spark:\\/\\/([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+|[a-z][a-z0-9\\.\\-]+)(:[0-9]+)?)$"))) - .defaultValue("local[2]") - .build - - val SPARK_APP_NAME = new PropertyDescriptor.Builder() - .name("spark.app.name") - .description("Tha application name") - .required(true) - .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^[a-zA-z0-9-_\\.]+$"))) - .defaultValue("logisland") - .build - - val SPARK_STREAMING_BATCH_DURATION = new PropertyDescriptor.Builder() - .name("spark.streaming.batchDuration") - .description("") - .required(true) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("2000") - .build - - val SPARK_YARN_DEPLOYMODE = new PropertyDescriptor.Builder() - .name("spark.yarn.deploy-mode") - .description("The yarn deploy mode") - .required(false) - // .allowableValues("client", "cluster") - .build - - val SPARK_YARN_QUEUE = new PropertyDescriptor.Builder() - .name("spark.yarn.queue") - .description("The name of the YARN queue") - .required(false) - // .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("default") - .build - - val memorySizePattern = Pattern.compile("^[0-9]+[mMgG]$"); - val SPARK_DRIVER_MEMORY = new PropertyDescriptor.Builder() - .name("spark.driver.memory") - .description("The memory size for Spark driver") - .required(false) - .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) - .defaultValue("512m") - .build - - val SPARK_EXECUTOR_MEMORY = new PropertyDescriptor.Builder() - .name("spark.executor.memory") - .description("The memory size for Spark executors") - .required(false) - .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) - .defaultValue("1g") - .build - - val SPARK_DRIVER_CORES = new PropertyDescriptor.Builder() - .name("spark.driver.cores") - .description("The number of cores for Spark driver") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("4") - .build - - val SPARK_EXECUTOR_CORES = new PropertyDescriptor.Builder() - .name("spark.executor.cores") - .description("The number of cores for Spark driver") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("1") - .build - - val SPARK_EXECUTOR_INSTANCES = new PropertyDescriptor.Builder() - .name("spark.executor.instances") - .description("The number of instances for Spark app") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .build - - val SPARK_SERIALIZER = new PropertyDescriptor.Builder() - .name("spark.serializer") - .description("Class to use for serializing objects that will be sent over the network " + - "or need to be cached in serialized form") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("org.apache.spark.serializer.KryoSerializer") - .build - - val SPARK_STREAMING_BLOCK_INTERVAL = new PropertyDescriptor.Builder() - .name("spark.streaming.blockInterval") - .description("Interval at which data received by Spark Streaming receivers is chunked into blocks " + - "of data before storing them in Spark. Minimum recommended - 50 ms") - .required(false) - .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) - .defaultValue("350") - .build - - - val SPARK_STREAMING_BACKPRESSURE_ENABLED = new PropertyDescriptor.Builder() - .name("spark.streaming.backpressure.enabled") - .description("This enables the Spark Streaming to control the receiving rate based on " + - "the current batch scheduling delays and processing times so that the system " + - "receives only as fast as the system can process.") - .required(false) - .addValidator(StandardValidators.BOOLEAN_VALIDATOR) - .defaultValue("false") - .build - - val SPARK_STREAMING_UNPERSIST = new PropertyDescriptor.Builder() - .name("spark.streaming.unpersist") - .description("Force RDDs generated and persisted by Spark Streaming to be automatically unpersisted " + - "from Spark's memory. The raw input data received by Spark Streaming is also automatically cleared." + - " Setting this to false will allow the raw data and persisted RDDs to be accessible outside " + - "the streaming application as they will not be cleared automatically. " + - "But it comes at the cost of higher memory usage in Spark.") - .required(false) - .addValidator(StandardValidators.BOOLEAN_VALIDATOR) - .defaultValue("false") - .build - - val SPARK_UI_PORT = new PropertyDescriptor.Builder() - .name("spark.ui.port") - .description("") - .required(false) - .addValidator(StandardValidators.PORT_VALIDATOR) - .defaultValue("4050") - .build - - val SPARK_STREAMING_TIMEOUT = new PropertyDescriptor.Builder() - .name("spark.streaming.timeout") - .description("") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("-1") - .build - - - val SPARK_STREAMING_UI_RETAINED_BATCHES = new PropertyDescriptor.Builder() - .name("spark.streaming.ui.retainedBatches") - .description("How many batches the Spark Streaming UI and status APIs remember before garbage collecting.") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("200") - .build - - val SPARK_STREAMING_RECEIVER_WAL_ENABLE = new PropertyDescriptor.Builder() - .name("spark.streaming.receiver.writeAheadLog.enable") - .description("Enable write ahead logs for receivers. " + - "All the input data received through receivers will be saved to write ahead logs " + - "that will allow it to be recovered after driver failures.") - .required(false) - .addValidator(StandardValidators.BOOLEAN_VALIDATOR) - .defaultValue("false") - .build - - - val SPARK_YARN_MAX_APP_ATTEMPTS = new PropertyDescriptor.Builder() - .name("spark.yarn.maxAppAttempts") - .description("Because Spark driver and Application Master share a single JVM," + - " any error in Spark driver stops our long-running job. " + - "Fortunately it is possible to configure maximum number of attempts " + - "that will be made to re-run the application. " + - "It is reasonable to set higher value than default 2 " + - "(derived from YARN cluster property yarn.resourcemanager.am.max-attempts). " + - "4 works quite well, higher value may cause unnecessary restarts" + - " even if the reason of the failure is permanent.") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("4") - .build - - - val SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() - .name("spark.yarn.am.attemptFailuresValidityInterval") - .description("If the application runs for days or weeks without restart " + - "or redeployment on highly utilized cluster, " + - "4 attempts could be exhausted in few hours. " + - "To avoid this situation, the attempt counter should be reset on every hour of so.") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("1h") - .build - - val SPARK_YARN_MAX_EXECUTOR_FAILURES = new PropertyDescriptor.Builder() - .name("spark.yarn.max.executor.failures") - .description("a maximum number of executor failures before the application fails. " + - "By default it is max(2 * num executors, 3), " + - "well suited for batch jobs but not for long-running jobs." + - " The property comes with corresponding validity interval which also should be set." + - "8 * num_executors") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("20") - .build - - - val SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() - .name("spark.yarn.executor.failuresValidityInterval") - .description("If the application runs for days or weeks without restart " + - "or redeployment on highly utilized cluster, " + - "x attempts could be exhausted in few hours. " + - "To avoid this situation, the attempt counter should be reset on every hour of so.") - .required(false) - .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .defaultValue("1h") - .build - - val SPARK_TASK_MAX_FAILURES = new PropertyDescriptor.Builder() - .name("spark.task.maxFailures") - .description("For long-running jobs you could also consider to boost maximum" + - " number of task failures before giving up the job. " + - "By default tasks will be retried 4 times and then job fails.") - .required(false) - .addValidator(StandardValidators.INTEGER_VALIDATOR) - .defaultValue("8") - .build - - val SPARK_MEMORY_STORAGE_FRACTION = new PropertyDescriptor.Builder() - .name("spark.memory.storageFraction") - .description("expresses the size of R as a fraction of M (default 0.5). " + - "R is the storage space within M where cached blocks immune to being evicted by execution.") - .required(false) - .addValidator(StandardValidators.FLOAT_VALIDATOR) - .defaultValue("0.5") - .build - - val SPARK_MEMORY_FRACTION = new PropertyDescriptor.Builder() - .name("spark.memory.fraction") - .description("expresses the size of M as a fraction of the (JVM heap space - 300MB) (default 0.75). " + - "The rest of the space (25%) is reserved for user data structures, internal metadata in Spark, " + - "and safeguarding against OOM errors in the case of sparse and unusually large records.") - .required(false) - .addValidator(StandardValidators.FLOAT_VALIDATOR) - .defaultValue("0.6") - .build - - val FAIR = new AllowableValue("FAIR", "FAIR", "fair sharing") - val FIFO = new AllowableValue("FIFO", "FIFO", "queueing jobs one after another") - - val SPARK_SCHEDULER_MODE = new PropertyDescriptor.Builder() - .name("spark.scheduler.mode") - .description("The scheduling mode between jobs submitted to the same SparkContext. " + - "Can be set to FAIR to use fair sharing instead of queueing jobs one after another. " + - "Useful for multi-user services.") - .required(false) - .allowableValues(FAIR, FIFO) - .defaultValue(FAIR.getValue) - .build -} - -abstract class BaseStreamProcessingEngine extends AbstractProcessingEngine { - - private lazy val conf = new SparkConf() - - - private val logger = LoggerFactory.getLogger(classOf[BaseStreamProcessingEngine]) - - - override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { - val descriptors: util.List[PropertyDescriptor] = new util.ArrayList[PropertyDescriptor] - descriptors.add(BaseStreamProcessingEngine.SPARK_APP_NAME) - descriptors.add(BaseStreamProcessingEngine.SPARK_MASTER) - descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_DEPLOYMODE) - descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_QUEUE) - descriptors.add(BaseStreamProcessingEngine.SPARK_DRIVER_MEMORY) - descriptors.add(BaseStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) - descriptors.add(BaseStreamProcessingEngine.SPARK_DRIVER_CORES) - descriptors.add(BaseStreamProcessingEngine.SPARK_EXECUTOR_CORES) - descriptors.add(BaseStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) - descriptors.add(BaseStreamProcessingEngine.SPARK_SERIALIZER) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) - descriptors.add(BaseStreamProcessingEngine.SPARK_UI_PORT) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) - descriptors.add(BaseStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) - descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) - descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) - descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) - descriptors.add(BaseStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) - descriptors.add(BaseStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) - descriptors.add(BaseStreamProcessingEngine.SPARK_MEMORY_FRACTION) - descriptors.add(BaseStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) - descriptors.add(BaseStreamProcessingEngine.SPARK_SCHEDULER_MODE) - - Collections.unmodifiableList(descriptors) - } - - - /** - * Called after the engine has been started. - * - * @param engineContext - */ - protected def onStart(engineContext: EngineContext) : Unit = {} - - /** - * Called before the engine is being stopped. - * - * @param engineContext - */ - protected def onStop(engineContext: EngineContext) : Unit = {} - - /** - * start the engine - * - * @param engineContext - */ - final override def start(engineContext: EngineContext) = { - logger.info("starting Spark Engine") - val timeout = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_STREAMING_TIMEOUT).asInteger().intValue() - val batchDuration = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION).asInteger().intValue() - - val streamingContext = createStreamingContext(engineContext) - - /** - * shutdown context gracefully - */ - sys.ShutdownHookThread { - logger.info("Gracefully stopping Spark Streaming Application") - streamingContext.stop(stopSparkContext = true, stopGracefully = true) - logger.info("Application stopped") - } - - streamingContext.start() - onStart(engineContext) - - if (timeout != -1) streamingContext.awaitTerminationOrTimeout(timeout) - else streamingContext.awaitTermination() - - logger.info("stream processing done") - } - - /** - * Hook to customize spark configuration before creating a spark context. - * - * @param sparkConf the preinitialized configuration. - * @param engineContext the engine context. - */ - protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit - - - final def createStreamingContext(engineContext: EngineContext): StreamingContext = { - val sparkMaster = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_MASTER).asString - val appName = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_APP_NAME).asString - val batchDuration = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION).asInteger().intValue() - - conf.setAppName(appName) - conf.setMaster(sparkMaster) - - - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_UI_PORT) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_SERIALIZER) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_DRIVER_MEMORY) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_DRIVER_CORES) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_EXECUTOR_CORES) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) - - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_MEMORY_FRACTION) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_SCHEDULER_MODE) - - conf.set("spark.kryo.registrator", "com.hurence.logisland.util.spark.ProtoBufRegistrator") - - - if (sparkMaster startsWith "yarn") { - // Note that SPARK_YARN_DEPLOYMODE is not used by spark itself but only by spark-submit CLI - // That's why we do not need to propagate it here - setConfProperty(conf, engineContext, BaseStreamProcessingEngine.SPARK_YARN_QUEUE) - } - - customizeSparkConfiguration(conf, engineContext) - - SparkUtils.customizeLogLevels - val sc = getCurrentSparkContext() - UserMetricsSystem.initialize(sc, "LogislandMetrics") - logger.info(s"spark context initialized with master:$sparkMaster, " + - s"appName:$appName, " + - s"batchDuration:$batchDuration ") - logger.info(s"conf : ${conf.toDebugString}") - - val ssc = getCurrentSparkStreamingContext() - setupStreamingContexts(engineContext, ssc) - - ssc - } - - protected final def getCurrentSparkContext(): SparkContext = { - SparkContext.getOrCreate(conf) - } - - protected final def getCurrentSparkStreamingContext(): StreamingContext = { - val batchDuration = conf.get(BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getName, - BaseStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getDefaultValue).toInt - StreamingContext.getActiveOrCreate(() => new StreamingContext(getCurrentSparkContext(), Milliseconds(batchDuration))) - } - - /** - * Override to setup streaming context before starting them. - * - * @param engineContext the engine context. - * @param scc the spark streaming context. - */ - protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit - - protected def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { - - // Need to check if the properties are set because those properties are not "requires" - if (engineContext.getPropertyValue(propertyDescriptor).isSet) { - conf.set(propertyDescriptor.getName, engineContext.getPropertyValue(propertyDescriptor).asString) - } - } - - - final override def shutdown(engineContext: EngineContext) = { - logger.info(s"shutting down Spark engine") - onStop(engineContext) - engineContext.getStreamContexts foreach (streamingContext => { - try { - - val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] - val sc = kafkaStream.getStreamContext(); - sc.stop(stopSparkContext = true, stopGracefully = true) - kafkaStream.stop() - } catch { - case ex: Exception => - logger.error("something bad happened, please check Kafka or cluster health : {}", ex.getMessage) - } - - }) - } - - override def onPropertyModified(descriptor: PropertyDescriptor, oldValue: String, newValue: String) = { - logger.info(s"property ${ - descriptor.getName - } value changed from $oldValue to $newValue") - } - -} - - diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index 8f841348b..42b3fb2df 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -19,19 +19,123 @@ package com.hurence.logisland.engine.spark import java.util import java.util.Collections +import java.util.regex.Pattern -import com.hurence.logisland.component.PropertyDescriptor -import com.hurence.logisland.engine.EngineContext +import com.hurence.logisland.component.{AllowableValue, ComponentContext, PropertyDescriptor} +import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} +import com.hurence.logisland.stream.StreamContext import com.hurence.logisland.stream.spark.SparkRecordStream +import com.hurence.logisland.util.spark.SparkUtils import com.hurence.logisland.validator.StandardValidators -import org.apache.spark.SparkConf -import org.apache.spark.streaming.StreamingContext +import org.apache.spark.groupon.metrics.UserMetricsSystem +import org.apache.spark.streaming.{Milliseconds, StreamingContext} +import org.apache.spark.{SparkConf, SparkContext} import org.slf4j.LoggerFactory -import scala.collection.JavaConverters._ +import scala.collection.JavaConversions._ + object KafkaStreamProcessingEngine { + val SPARK_MASTER = new PropertyDescriptor.Builder() + .name("spark.master") + .description("The url to Spark Master") + .required(true) + // The regex allows "local[K]" with K as an integer, "local[*]", "yarn", "yarn-client", "yarn-cluster" and "spark://HOST[:PORT]" + // there is NO support for "mesos://HOST:PORT" + .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^(yarn(-(client|cluster))?|local\\[[0-9\\*]+\\]|spark:\\/\\/([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+|[a-z][a-z0-9\\.\\-]+)(:[0-9]+)?)$"))) + .defaultValue("local[2]") + .build + + val SPARK_APP_NAME = new PropertyDescriptor.Builder() + .name("spark.app.name") + .description("Tha application name") + .required(true) + .addValidator(StandardValidators.createRegexMatchingValidator(Pattern.compile("^[a-zA-z0-9-_\\.]+$"))) + .defaultValue("logisland") + .build + + val SPARK_STREAMING_BATCH_DURATION = new PropertyDescriptor.Builder() + .name("spark.streaming.batchDuration") + .description("") + .required(true) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("2000") + .build + + val SPARK_YARN_DEPLOYMODE = new PropertyDescriptor.Builder() + .name("spark.yarn.deploy-mode") + .description("The yarn deploy mode") + .required(false) + // .allowableValues("client", "cluster") + .build + + val SPARK_YARN_QUEUE = new PropertyDescriptor.Builder() + .name("spark.yarn.queue") + .description("The name of the YARN queue") + .required(false) + // .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("default") + .build + + val memorySizePattern = Pattern.compile("^[0-9]+[mMgG]$"); + val SPARK_DRIVER_MEMORY = new PropertyDescriptor.Builder() + .name("spark.driver.memory") + .description("The memory size for Spark driver") + .required(false) + .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) + .defaultValue("512m") + .build + + val SPARK_EXECUTOR_MEMORY = new PropertyDescriptor.Builder() + .name("spark.executor.memory") + .description("The memory size for Spark executors") + .required(false) + .addValidator(StandardValidators.createRegexMatchingValidator(memorySizePattern)) + .defaultValue("1g") + .build + + val SPARK_DRIVER_CORES = new PropertyDescriptor.Builder() + .name("spark.driver.cores") + .description("The number of cores for Spark driver") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("4") + .build + + val SPARK_EXECUTOR_CORES = new PropertyDescriptor.Builder() + .name("spark.executor.cores") + .description("The number of cores for Spark driver") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("1") + .build + + val SPARK_EXECUTOR_INSTANCES = new PropertyDescriptor.Builder() + .name("spark.executor.instances") + .description("The number of instances for Spark app") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .build + + val SPARK_SERIALIZER = new PropertyDescriptor.Builder() + .name("spark.serializer") + .description("Class to use for serializing objects that will be sent over the network " + + "or need to be cached in serialized form") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("org.apache.spark.serializer.KryoSerializer") + .build + + val SPARK_STREAMING_BLOCK_INTERVAL = new PropertyDescriptor.Builder() + .name("spark.streaming.blockInterval") + .description("Interval at which data received by Spark Streaming receivers is chunked into blocks " + + "of data before storing them in Spark. Minimum recommended - 50 ms") + .required(false) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .defaultValue("350") + .build + val SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION = new PropertyDescriptor.Builder() .name("spark.streaming.kafka.maxRatePerPartition") .description("Maximum rate (number of records per second) at which data will be read from each Kafka partition") @@ -40,6 +144,44 @@ object KafkaStreamProcessingEngine { .defaultValue("5000") .build + val SPARK_STREAMING_BACKPRESSURE_ENABLED = new PropertyDescriptor.Builder() + .name("spark.streaming.backpressure.enabled") + .description("This enables the Spark Streaming to control the receiving rate based on " + + "the current batch scheduling delays and processing times so that the system " + + "receives only as fast as the system can process.") + .required(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("false") + .build + + val SPARK_STREAMING_UNPERSIST = new PropertyDescriptor.Builder() + .name("spark.streaming.unpersist") + .description("Force RDDs generated and persisted by Spark Streaming to be automatically unpersisted " + + "from Spark's memory. The raw input data received by Spark Streaming is also automatically cleared." + + " Setting this to false will allow the raw data and persisted RDDs to be accessible outside " + + "the streaming application as they will not be cleared automatically. " + + "But it comes at the cost of higher memory usage in Spark.") + .required(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("false") + .build + + val SPARK_UI_PORT = new PropertyDescriptor.Builder() + .name("spark.ui.port") + .description("") + .required(false) + .addValidator(StandardValidators.PORT_VALIDATOR) + .defaultValue("4050") + .build + + val SPARK_STREAMING_TIMEOUT = new PropertyDescriptor.Builder() + .name("spark.streaming.timeout") + .description("") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("-1") + .build + val SPARK_STREAMING_KAFKA_MAXRETRIES = new PropertyDescriptor.Builder() .name("spark.streaming.kafka.maxRetries") .description("Maximum rate (number of records per second) at which data will be read from each Kafka partition") @@ -47,43 +189,359 @@ object KafkaStreamProcessingEngine { .addValidator(StandardValidators.INTEGER_VALIDATOR) .defaultValue("3") .build + + val SPARK_STREAMING_UI_RETAINED_BATCHES = new PropertyDescriptor.Builder() + .name("spark.streaming.ui.retainedBatches") + .description("How many batches the Spark Streaming UI and status APIs remember before garbage collecting.") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("200") + .build + + val SPARK_STREAMING_RECEIVER_WAL_ENABLE = new PropertyDescriptor.Builder() + .name("spark.streaming.receiver.writeAheadLog.enable") + .description("Enable write ahead logs for receivers. " + + "All the input data received through receivers will be saved to write ahead logs " + + "that will allow it to be recovered after driver failures.") + .required(false) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("false") + .build + + + val SPARK_YARN_MAX_APP_ATTEMPTS = new PropertyDescriptor.Builder() + .name("spark.yarn.maxAppAttempts") + .description("Because Spark driver and Application Master share a single JVM," + + " any error in Spark driver stops our long-running job. " + + "Fortunately it is possible to configure maximum number of attempts " + + "that will be made to re-run the application. " + + "It is reasonable to set higher value than default 2 " + + "(derived from YARN cluster property yarn.resourcemanager.am.max-attempts). " + + "4 works quite well, higher value may cause unnecessary restarts" + + " even if the reason of the failure is permanent.") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("4") + .build + + + val SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() + .name("spark.yarn.am.attemptFailuresValidityInterval") + .description("If the application runs for days or weeks without restart " + + "or redeployment on highly utilized cluster, " + + "4 attempts could be exhausted in few hours. " + + "To avoid this situation, the attempt counter should be reset on every hour of so.") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("1h") + .build + + val SPARK_YARN_MAX_EXECUTOR_FAILURES = new PropertyDescriptor.Builder() + .name("spark.yarn.max.executor.failures") + .description("a maximum number of executor failures before the application fails. " + + "By default it is max(2 * num executors, 3), " + + "well suited for batch jobs but not for long-running jobs." + + " The property comes with corresponding validity interval which also should be set." + + "8 * num_executors") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("20") + .build + + + val SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL = new PropertyDescriptor.Builder() + .name("spark.yarn.executor.failuresValidityInterval") + .description("If the application runs for days or weeks without restart " + + "or redeployment on highly utilized cluster, " + + "x attempts could be exhausted in few hours. " + + "To avoid this situation, the attempt counter should be reset on every hour of so.") + .required(false) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .defaultValue("1h") + .build + + val SPARK_TASK_MAX_FAILURES = new PropertyDescriptor.Builder() + .name("spark.task.maxFailures") + .description("For long-running jobs you could also consider to boost maximum" + + " number of task failures before giving up the job. " + + "By default tasks will be retried 4 times and then job fails.") + .required(false) + .addValidator(StandardValidators.INTEGER_VALIDATOR) + .defaultValue("8") + .build + + val SPARK_MEMORY_STORAGE_FRACTION = new PropertyDescriptor.Builder() + .name("spark.memory.storageFraction") + .description("expresses the size of R as a fraction of M (default 0.5). " + + "R is the storage space within M where cached blocks immune to being evicted by execution.") + .required(false) + .addValidator(StandardValidators.FLOAT_VALIDATOR) + .defaultValue("0.5") + .build + + val SPARK_MEMORY_FRACTION = new PropertyDescriptor.Builder() + .name("spark.memory.fraction") + .description("expresses the size of M as a fraction of the (JVM heap space - 300MB) (default 0.75). " + + "The rest of the space (25%) is reserved for user data structures, internal metadata in Spark, " + + "and safeguarding against OOM errors in the case of sparse and unusually large records.") + .required(false) + .addValidator(StandardValidators.FLOAT_VALIDATOR) + .defaultValue("0.6") + .build + + val FAIR = new AllowableValue("FAIR", "FAIR", "fair sharing") + val FIFO = new AllowableValue("FIFO", "FIFO", "queueing jobs one after another") + + val SPARK_SCHEDULER_MODE = new PropertyDescriptor.Builder() + .name("spark.scheduler.mode") + .description("The scheduling mode between jobs submitted to the same SparkContext. " + + "Can be set to FAIR to use fair sharing instead of queueing jobs one after another. " + + "Useful for multi-user services.") + .required(false) + .allowableValues(FAIR, FIFO) + .defaultValue(FAIR.getValue) + .build } -class KafkaStreamProcessingEngine extends BaseStreamProcessingEngine { +class KafkaStreamProcessingEngine extends AbstractProcessingEngine { private val logger = LoggerFactory.getLogger(classOf[KafkaStreamProcessingEngine]) + private val conf = new SparkConf() + + /** + * Provides subclasses the ability to perform initialization logic + */ + override def init(context: ComponentContext): Unit = { + super.init(context) + val engineContext = context.asInstanceOf[EngineContext] + val sparkMaster = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_MASTER).asString + val appName = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_APP_NAME).asString + val batchDuration = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION).asInteger().intValue() + /** + * job configuration + */ + + + conf.setAppName(appName) + conf.setMaster(sparkMaster) + + def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { + + // Need to check if the properties are set because those properties are not "requires" + if (engineContext.getPropertyValue(propertyDescriptor).isSet) { + conf.set(propertyDescriptor.getName, engineContext.getPropertyValue(propertyDescriptor).asString) + } + } + + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_UI_PORT) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_SERIALIZER) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_DRIVER_MEMORY) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_DRIVER_CORES) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_EXECUTOR_CORES) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) + + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_MEMORY_FRACTION) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_SCHEDULER_MODE) + + conf.set("spark.kryo.registrator", "com.hurence.logisland.util.spark.ProtoBufRegistrator") + + if (sparkMaster startsWith "yarn") { + // Note that SPARK_YARN_DEPLOYMODE is not used by spark itself but only by spark-submit CLI + // That's why we do not need to propagate it here + setConfProperty(conf, engineContext, KafkaStreamProcessingEngine.SPARK_YARN_QUEUE) + } + + @transient val sparkContext = getCurrentSparkContext() + + SparkUtils.customizeLogLevels + UserMetricsSystem.initialize(sparkContext, "LogislandMetrics") + + /** + * shutdown context gracefully + */ + sys.ShutdownHookThread { + logger.info("Gracefully stopping Spark Streaming Application") + sparkContext.stop(); + logger.info("Application stopped") + } + + + logger.info(s"spark context initialized with master:$sparkMaster, " + + s"appName:$appName, " + + s"batchDuration:$batchDuration ") + logger.info(s"conf : ${conf.toDebugString}") + } override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { val descriptors: util.List[PropertyDescriptor] = new util.ArrayList[PropertyDescriptor] - descriptors.addAll(super.getSupportedPropertyDescriptors) + descriptors.add(KafkaStreamProcessingEngine.SPARK_APP_NAME) + descriptors.add(KafkaStreamProcessingEngine.SPARK_MASTER) + descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_DEPLOYMODE) + descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_QUEUE) + descriptors.add(KafkaStreamProcessingEngine.SPARK_DRIVER_MEMORY) + descriptors.add(KafkaStreamProcessingEngine.SPARK_EXECUTOR_MEMORY) + descriptors.add(KafkaStreamProcessingEngine.SPARK_DRIVER_CORES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_EXECUTOR_CORES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_EXECUTOR_INSTANCES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_SERIALIZER) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_BLOCK_INTERVAL) descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_BACKPRESSURE_ENABLED) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_UNPERSIST) + descriptors.add(KafkaStreamProcessingEngine.SPARK_UI_PORT) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_UI_RETAINED_BATCHES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_STREAMING_RECEIVER_WAL_ENABLE) + descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_MAX_APP_ATTEMPTS) + descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_AM_ATTEMPT_FAILURES_VALIDITY_INTERVAL) + descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_MAX_EXECUTOR_FAILURES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_YARN_EXECUTOR_FAILURES_VALIDITY_INTERVAL) + descriptors.add(KafkaStreamProcessingEngine.SPARK_TASK_MAX_FAILURES) + descriptors.add(KafkaStreamProcessingEngine.SPARK_MEMORY_FRACTION) + descriptors.add(KafkaStreamProcessingEngine.SPARK_MEMORY_STORAGE_FRACTION) + descriptors.add(KafkaStreamProcessingEngine.SPARK_SCHEDULER_MODE) + Collections.unmodifiableList(descriptors) } - override protected def customizeSparkConfiguration(sparkConf: SparkConf, engineContext: EngineContext): Unit = { - setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAXRETRIES) - setConfProperty(sparkConf, engineContext, KafkaStreamProcessingEngine.SPARK_STREAMING_KAFKA_MAX_RATE_PER_PARTITION) + /** + * start the engine + * + * @param engineContext + */ + override def start(engineContext: EngineContext) = { + logger.info("starting Spark Engine") + //val timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT).asInteger().intValue() + val streamingContext = createStreamingContext(engineContext) + streamingContext.start() + } + + protected def getCurrentSparkStreamingContext(sparkContext: SparkContext): StreamingContext = { + return StreamingContext.getActiveOrCreate(() => + return new StreamingContext(sparkContext, + Milliseconds(sparkContext.getConf.get(KafkaStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getName, + KafkaStreamProcessingEngine.SPARK_STREAMING_BATCH_DURATION.getDefaultValue).toInt)) + ) + } + protected def getCurrentSparkContext(): SparkContext = { + return SparkContext.getOrCreate(conf) } - override protected def setupStreamingContexts(engineContext: EngineContext, ssc: StreamingContext): Unit = { - val appName = engineContext.getPropertyValue(BaseStreamProcessingEngine.SPARK_APP_NAME).asString - engineContext.getStreamContexts.asScala.foreach(streamContext => { + + def createStreamingContext(engineContext: EngineContext): StreamingContext = { + + + @transient val sc = getCurrentSparkContext() + @transient val ssc = getCurrentSparkStreamingContext(sc) + val appName = sc.appName; + + + /** + * loop over processContext + */ + engineContext.getStreamContexts.foreach(streamingContext => { try { - val kafkaStream = streamContext.getStream.asInstanceOf[SparkRecordStream] - kafkaStream.setup(appName, ssc, streamContext, engineContext) + val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] + kafkaStream.setup(appName, ssc, streamingContext, engineContext) kafkaStream.start() } catch { case ex: Exception => logger.error("something bad happened, please check Kafka or cluster health : {}", ex.getMessage) } + }) + ssc } -} + override def shutdown(engineContext: EngineContext) = { + logger.info(s"shutting down Spark engine") + stop(engineContext, true) + + } + + private def stop(engineContext: EngineContext, doStopSparkContext: Boolean) = { + engineContext.getStreamContexts.foreach(streamingContext => { + try { + + val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] + kafkaStream.stop() + } catch { + case ex: Exception => + logger.error("something bad happened, please check Kafka or cluster health : {}", ex.getMessage) + } + + getCurrentSparkStreamingContext(getCurrentSparkContext()) + .stop(stopSparkContext = doStopSparkContext, stopGracefully = true) + + + }) + } + + override def onPropertyModified(descriptor: PropertyDescriptor, oldValue: String, newValue: String) = { + logger.info(s"property ${ + descriptor.getName + } value changed from $oldValue to $newValue") + } + + /** + * Await for termination. + * + */ + override def awaitTermination(engineContext: EngineContext): Unit = { + var timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) + .asInteger().toInt + val sc = getCurrentSparkContext() + + while (!sc.isStopped) { + try { + if (timeout < 0) { + Thread.sleep(200) + } else { + val toSleep = Math.min(200, timeout); + Thread.sleep(toSleep) + timeout -= toSleep + } + } catch { + case e: InterruptedException => return + case unknown: Throwable => throw unknown + } + } + } + + + /** + * Reset the engine by stopping the streaming context. + */ + override def reset(engineContext: EngineContext): Unit = { + logger.info(s"Resetting engine ${ + engineContext.getName + }") + stop(engineContext, false) + engineContext.getStreamContexts.clear() + engineContext.getControllerServiceConfigurations.clear() + } + + +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala index 4e74765f1..a9f33ee47 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -17,20 +17,23 @@ package com.hurence.logisland.engine.spark -import java.time.Duration +import java.time.{Duration, Instant} import java.util import java.util.Collections import java.util.concurrent.{Executors, TimeUnit} import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext -import com.hurence.logisland.engine.spark.remote.{RemoteApiClient, RemoteComponentRegistry} -import com.hurence.logisland.stream.StandardStreamContext +import com.hurence.logisland.engine.spark.remote.model.DataFlow +import com.hurence.logisland.engine.spark.remote.{PipelineConfigurationBroadcastWrapper, RemoteApiClient, RemoteApiComponentFactory} +import com.hurence.logisland.processor.ProcessContext +import com.hurence.logisland.stream.{StandardStreamContext, StreamContext} import com.hurence.logisland.stream.spark.DummyRecordStream import com.hurence.logisland.validator.StandardValidators -import org.apache.spark.streaming.StreamingContext import org.slf4j.LoggerFactory +import scala.collection.JavaConverters + object RemoteApiStreamProcessingEngine { val REMOTE_API_BASE_URL = new PropertyDescriptor.Builder() .name("remote.api.baseUrl") @@ -46,6 +49,13 @@ object RemoteApiStreamProcessingEngine { .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) .build + val REMOTE_API_CONFIG_PUSH_RATE = new PropertyDescriptor.Builder() + .name("remote.api.push.rate") + .description("Remote api configuration push rate in milliseconds") + .required(true) + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .build + val REMOTE_API_CONNECT_TIMEOUT = new PropertyDescriptor.Builder() .name("remote.api.timeouts.connect") .description("Remote api connection timeout in milliseconds") @@ -78,14 +88,14 @@ object RemoteApiStreamProcessingEngine { class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { private val logger = LoggerFactory.getLogger(classOf[RemoteApiStreamProcessingEngine]) - private val executor = Executors.newSingleThreadScheduledExecutor() - private var remoteApiRegistry: RemoteComponentRegistry = _ + private var initialized = false override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { val ret = new util.ArrayList(super.getSupportedPropertyDescriptors) ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_BASE_URL) ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE) + ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_CONFIG_PUSH_RATE) ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_CONNECT_TIMEOUT) ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_USER) ret.add(RemoteApiStreamProcessingEngine.REMOTE_API_PASSWORD) @@ -93,60 +103,75 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { return Collections.unmodifiableList(ret) } - override protected def setupStreamingContexts(engineContext: EngineContext, scc: StreamingContext): Unit = { - if (!engineContext.getControllerServiceConfigurations.isEmpty) { - logger.warn("This engine will not load service controllers from the configuration file!") - engineContext.getControllerServiceConfigurations.clear() - } - if (!engineContext.getStreamContexts.isEmpty()) { - logger.warn("This engine will not handle streams from the configuration file!") - engineContext.getStreamContexts.clear() - } - engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")); - super.setupStreamingContexts(engineContext, scc) - remoteApiRegistry = new RemoteComponentRegistry(engineContext); - - } /** - * Called after the engine has been started. + * start the engine * * @param engineContext */ - override protected def onStart(engineContext: EngineContext): Unit = { - super.onStart(engineContext) - val remoteApiClient = new RemoteApiClient( - engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_BASE_URL), - Duration.ofMillis(engineContext.getPropertyValue(RemoteApiStreamProcessingEngine.REMOTE_API_SOCKET_TIMEOUT).asLong()), - Duration.ofMillis(engineContext.getPropertyValue(RemoteApiStreamProcessingEngine.REMOTE_API_CONNECT_TIMEOUT).asLong()), - engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_USER), - engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_PASSWORD)) - - implicit def funToRunnable(fun: () => Unit) = new Runnable() { - def run() = fun() + override def start(engineContext: EngineContext): Unit = { + + if (engineContext.getStreamContexts.isEmpty) { + engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")) } - executor.scheduleWithFixedDelay(() => { - val pipelines = remoteApiClient.fetchPipelines() - if (pipelines.isPresent) { - remoteApiRegistry.updateEngineContext(pipelines.get()) - } - }, 0, engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE).toInt, - TimeUnit.MILLISECONDS) - } - /** - * Called before the engine is being stopped. - * - * @param engineContext - */ - override protected def onStop(engineContext: EngineContext): Unit = { - super.onStop(engineContext) - executor.shutdown() - //stop everything started from remote side. - if (remoteApiRegistry != null) { - remoteApiRegistry.updateEngineContext(Collections.emptyList()) + if (!initialized) { + initialized = true + val remoteApiClient = new RemoteApiClient(new RemoteApiClient.ConnectionSettings( + engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_BASE_URL), + Duration.ofMillis(engineContext.getPropertyValue(RemoteApiStreamProcessingEngine.REMOTE_API_SOCKET_TIMEOUT).asLong()), + Duration.ofMillis(engineContext.getPropertyValue(RemoteApiStreamProcessingEngine.REMOTE_API_CONNECT_TIMEOUT).asLong()), + engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_USER), + engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_PASSWORD))) + + + val appName = getCurrentSparkContext().appName + var currentDataflow: DataFlow = null + + //schedule dataflow refresh + @transient lazy val executor = Executors.newSingleThreadScheduledExecutor(); + @transient lazy val remoteApiComponentFactory = new RemoteApiComponentFactory + + + executor.scheduleWithFixedDelay(new Runnable { + val state = new RemoteApiClient.State + var i = 0 + + override def run(): Unit = { + try { + val dataflow = remoteApiClient.fetchDataflow(appName, state) + if (dataflow.isPresent) { + var lastUpdated: Instant = null + if (currentDataflow != null && currentDataflow.getLastModified != null) { + lastUpdated = currentDataflow.getLastModified.toInstant + } + remoteApiComponentFactory.updateEngineContext(getCurrentSparkContext(), engineContext, dataflow.get, currentDataflow) + + + + currentDataflow = dataflow.get() + } + } catch { + case default: Throwable => logger.warn("Unexpected exception while trying to poll for new dataflow configuration", default) + } + } + }, 0, engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE).toInt, TimeUnit.MILLISECONDS + ) + + } + + + + super.start(engineContext) + } + + + override def shutdown(engineContext: EngineContext): Unit = { + super.shutdown(engineContext) } + + } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala index be349fb7f..6231767da 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala @@ -22,6 +22,7 @@ import java.util.Collections import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext +import com.hurence.logisland.engine.spark.remote.PipelineConfigurationBroadcastWrapper import com.hurence.logisland.record.Record import com.hurence.logisland.serializer._ import com.hurence.logisland.stream.StreamProperties._ @@ -48,12 +49,9 @@ import org.slf4j.LoggerFactory import scala.collection.JavaConversions._ - - abstract class AbstractKafkaRecordStream extends AbstractRecordStream with SparkRecordStream { - val NONE_TOPIC: String = "none" private val logger = LoggerFactory.getLogger(this.getClass) protected var kafkaSink: Broadcast[KafkaSink] = null @@ -89,7 +87,7 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark } - override def setup(appName: String, ssc: StreamingContext, streamContext: StreamContext, engineContext: EngineContext) = { + override def setup(appName: String, ssc: StreamingContext, streamContext: StreamContext, engineContext: EngineContext) = { this.appName = appName this.ssc = ssc this.streamContext = streamContext @@ -186,57 +184,62 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark } else kafkaStream - stream.foreachRDD(rdd => { + stream + .foreachRDD(rdd => { + this.streamContext.getProcessContexts().clear(); + logger.info(s"Stream is ${this.streamContext.getIdentifier}") + this.streamContext.getProcessContexts().addAll( + PipelineConfigurationBroadcastWrapper.getInstance().get(this.streamContext.getIdentifier)) - if (!rdd.isEmpty()) { + if (!rdd.isEmpty()) { - val offsetRanges = process(rdd) - // some time later, after outputs have completed - if (offsetRanges.nonEmpty) { - // kafkaStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges.get) + val offsetRanges = process(rdd) + // some time later, after outputs have completed + if (offsetRanges.nonEmpty) { + // kafkaStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges.get) - kafkaStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges.get, new OffsetCommitCallback() { - def onComplete(m: java.util.Map[TopicPartition, OffsetAndMetadata], e: Exception) { - if (null != e) { - logger.error("error commiting offsets", e) + kafkaStream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges.get, new OffsetCommitCallback() { + def onComplete(m: java.util.Map[TopicPartition, OffsetAndMetadata], e: Exception) { + if (null != e) { + logger.error("error commiting offsets", e) + } } - } - }) + }) - needMetricsReset = true - } - else if (needMetricsReset) { - try { - - for (partitionId <- 0 to rdd.getNumPartitions) { - val pipelineMetricPrefix = streamContext.getIdentifier + "." + - "partition" + partitionId + "." - val pipelineTimerContext = UserMetricsSystem.timer(pipelineMetricPrefix + "Pipeline.processing_time_ms").time() - - streamContext.getProcessContexts.foreach(processorContext => { - UserMetricsSystem.timer(pipelineMetricPrefix + processorContext.getName + ".processing_time_ms") - .time() - .stop() - - ProcessorMetrics.resetMetrics(pipelineMetricPrefix + processorContext.getName + ".") - }) - pipelineTimerContext.stop() + needMetricsReset = true + } + else if (needMetricsReset) { + try { + + for (partitionId <- 0 to rdd.getNumPartitions) { + val pipelineMetricPrefix = streamContext.getIdentifier + "." + + "partition" + partitionId + "." + val pipelineTimerContext = UserMetricsSystem.timer(pipelineMetricPrefix + "Pipeline.processing_time_ms").time() + + streamContext.getProcessContexts.foreach(processorContext => { + UserMetricsSystem.timer(pipelineMetricPrefix + processorContext.getName + ".processing_time_ms") + .time() + .stop() + + ProcessorMetrics.resetMetrics(pipelineMetricPrefix + processorContext.getName + ".") + }) + pipelineTimerContext.stop() + } + } catch { + case ex: Throwable => + logger.error(s"exception : ${ex.toString}") + None + } finally { + needMetricsReset = false } - } catch { - case ex: Throwable => - logger.error(s"exception : ${ex.toString}") - None - } finally { - needMetricsReset = false } } - } - }) + }) } catch { case ex: Throwable => ex.printStackTrace() diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java index ca2e3ef39..12de07af4 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java @@ -42,10 +42,6 @@ public class RemoteApiEngineTest { @Ignore public void remoteTest() { - MockWebServer webServer = new MockWebServer(); - webServer.enqueue(new MockResponse().setBody("")); - - logger.info("starting StreamProcessingRunner"); Optional engineInstance = Optional.empty(); @@ -70,10 +66,11 @@ public void remoteTest() { try { // start the engine final EngineContext engineContext = engineInstance.get(); - Executors.newSingleThreadScheduledExecutor().schedule(()->engineContext.getEngine().shutdown(engineContext), - 10, TimeUnit.SECONDS); engineInstance.get().getEngine().start(engineContext); SparkUtils.customizeLogLevels(); + + engineContext.getEngine().awaitTermination(engineContext); + } catch (Exception e) { logger.error("something went bad while running the job : {}", e); System.exit(-1); diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java index b5b40b9e0..3db9f4619 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java @@ -30,23 +30,27 @@ public class RemoteApiClientTest { + private final String dataflowName = "dummy"; + private RemoteApiClient createInstance(MockWebServer server, String user, String password) { - return new RemoteApiClient(server.url("/").toString(), - Duration.ofSeconds(2), Duration.ofSeconds(2), user, password); + return new RemoteApiClient(new RemoteApiClient.ConnectionSettings( server.url("/").toString(), + Duration.ofSeconds(2), Duration.ofSeconds(2), user, password)); } @Test public void testAllUnsecured() throws Exception { + try (MockWebServer mockWebServer = new MockWebServer()) { mockWebServer.enqueue(new MockResponse().setResponseCode(404)); mockWebServer.enqueue(new MockResponse().setBodyDelay(3, TimeUnit.SECONDS)); final String dummy = "\"name\":\"myName\", \"component\":\"myComponent\""; - mockWebServer.enqueue(new MockResponse().setBody("[{" + dummy + ",\"lastModified\":\"1983-06-04T10:01:02Z\"," + - "\"streams\":[{" + dummy + "}]}]")); + mockWebServer.enqueue(new MockResponse().setBody("{" + dummy + ",\"lastModified\":\"1983-06-04T10:01:02Z\"," + + "\"streams\":[{" + dummy + "}]}")); RemoteApiClient client = createInstance(mockWebServer, null, null); - Assert.assertFalse(client.fetchPipelines().isPresent()); - Assert.assertFalse(client.fetchPipelines().isPresent()); - Assert.assertEquals(1, client.fetchPipelines().get().size()); + Assert.assertFalse(client.fetchDataflow(dataflowName, new RemoteApiClient.State()).isPresent()); + Assert.assertFalse(client.fetchDataflow(dataflowName, new RemoteApiClient.State()).isPresent()); + Assert.assertTrue(client.fetchDataflow(dataflowName, new RemoteApiClient.State()).isPresent()); + } @@ -55,9 +59,9 @@ public void testAllUnsecured() throws Exception { @Test public void testValidationFails() throws Exception { try (MockWebServer mockWebServer = new MockWebServer()) { - mockWebServer.enqueue(new MockResponse().setBody("[{\"name\":\"divPo\", \"lastModified\":\"1983-06-04T10:01:02Z\",\"services\":[{}],\"streams\":[{}]}]")); + mockWebServer.enqueue(new MockResponse().setBody("{\"name\":\"divPo\", \"lastModified\":\"1983-06-04T10:01:02Z\",\"services\":[{}],\"streams\":[{}]}")); RemoteApiClient client = createInstance(mockWebServer, null, null); - Assert.assertFalse(client.fetchPipelines().isPresent()); + Assert.assertFalse(client.fetchDataflow(dataflowName, new RemoteApiClient.State()).isPresent()); } @@ -67,11 +71,22 @@ public void testValidationFails() throws Exception { public void testAuthentication() throws Exception { try (MockWebServer mockWebServer = new MockWebServer()) { RemoteApiClient client = createInstance(mockWebServer, "test", "test"); - mockWebServer.enqueue(new MockResponse().setBody("[]")); - client.fetchPipelines(); + mockWebServer.enqueue(new MockResponse().setBody("{}")); + client.fetchDataflow(dataflowName, new RemoteApiClient.State()); RecordedRequest request = mockWebServer.takeRequest(); String auth = request.getHeader(HttpHeaders.AUTHORIZATION); Assert.assertEquals(Credentials.basic("test", "test"), auth); } } + + @Test + public void testUri() throws Exception { + try (MockWebServer mockWebServer = new MockWebServer()) { + RemoteApiClient client = createInstance(mockWebServer, null, null); + mockWebServer.enqueue(new MockResponse().setBody("{}")); + client.fetchDataflow(dataflowName, new RemoteApiClient.State()); + RecordedRequest request = mockWebServer.takeRequest(); + Assert.assertEquals("/dataflows/"+dataflowName, request.getPath()); + } + } } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java deleted file mode 100644 index d8b025b05..000000000 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteComponentRegistryTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.hurence.logisland.engine.spark.remote; - -import com.hurence.logisland.engine.EngineContext; -import com.hurence.logisland.engine.MockProcessingEngine; -import com.hurence.logisland.engine.StandardEngineContext; -import com.hurence.logisland.engine.spark.remote.mock.MockProcessor; -import com.hurence.logisland.engine.spark.remote.mock.MockServiceController; -import com.hurence.logisland.engine.spark.remote.mock.MockStream; -import com.hurence.logisland.engine.spark.remote.model.Pipeline; -import com.hurence.logisland.engine.spark.remote.model.Processor; -import com.hurence.logisland.engine.spark.remote.model.Service; -import com.hurence.logisland.engine.spark.remote.model.Stream; -import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.OffsetDateTime; -import java.util.Arrays; -import java.util.Collections; - -public class RemoteComponentRegistryTest { - - private static final Logger logger = LoggerFactory.getLogger(RemoteComponentRegistryTest.class); - - - @Test - public void updateEngineContext() { - MockProcessingEngine engine = new MockProcessingEngine(); - EngineContext masterContext = new StandardEngineContext(engine, "master"); - engine.start(masterContext); - RemoteComponentRegistry registry = new RemoteComponentRegistry(masterContext); - Pipeline pipeline1 = createPipeline("pipeline1"); - Pipeline pipeline2 = createPipeline("pipeline2"); - logger.info("should create two new pipelines"); - registry.updateEngineContext(Arrays.asList(pipeline1, pipeline2)); - logger.info("should do nothing"); - registry.updateEngineContext(Arrays.asList(pipeline1, pipeline2)); - logger.info("should remove a pipeline"); - registry.updateEngineContext(Arrays.asList(pipeline1)); - logger.info("should update the pipeline (since touch is fresher)"); - pipeline1 = createPipeline("pipeline1"); - registry.updateEngineContext(Arrays.asList(pipeline1)); - logger.info("should do nothing"); - registry.updateEngineContext(Arrays.asList(pipeline1)); - logger.info("should remove everything (Stop)"); - registry.updateEngineContext(Collections.emptyList()); - engine.shutdown(masterContext); - - - } - - private Service createService(String name) { - return (Service) new Service() - .name(name) - .component(MockServiceController.class.getCanonicalName()); - } - - private Processor createProcessor(String name) { - return (Processor) new Processor() - .name(name) - .component(MockProcessor.class.getCanonicalName()); - } - - private Stream createStream(String name) { - return (Stream) new Stream() - .addProcessorsItem(createProcessor("processor1")) - .addProcessorsItem(createProcessor("processor2")) - .name(name) - .component(MockStream.class.getCanonicalName()); - - } - - private Pipeline createPipeline(String name) { - return new Pipeline() - .addServicesItem(createService("service")) - .addStreamsItem(createStream("stream1")) - .lastModified(OffsetDateTime.now()) - .name(name); - } -} \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml index c27beae6f..1ececfa04 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml @@ -6,7 +6,7 @@ engine: type: engine documentation: Do some remote pipelines. configuration: - spark.app.name: FutureFactory + spark.app.name: RemoteConnect spark.master: local[2] spark.driver.memory: 512M spark.driver.cores: 1 @@ -30,50 +30,9 @@ engine: spark.streaming.ui.retainedBatches: 200 spark.streaming.receiver.writeAheadLog.enable: false spark.ui.port: 4040 - remote.api.baseUrl: http://localhost:1234/api + remote.api.baseUrl: http://localhost:3000 remote.api.polling.rate: 5000 + remote.api.push.rate: 10000 - controllerServiceConfigurations: - - controllerService: mqtt_service - component: com.hurence.logisland.stream.spark.structured.provider.MQTTStructuredStreamProviderService - configuration: - # mqtt.broker.url: tcp://51.15.164.141:1883 - mqtt.broker.url: tcp://localhost:1883 - mqtt.persistence: memory - mqtt.client.id: logisland - mqtt.qos: 0 - mqtt.topic: Account123/# - mqtt.username: User123 - mqtt.password: Kapu12345678+ - mqtt.clean.session: true - mqtt.connection.timeout: 30 - mqtt.keep.alive: 60 - mqtt.version: 3 - - controllerService: console_service - component: com.hurence.logisland.stream.spark.structured.provider.ConsoleStructuredStreamProviderService - - streamConfigurations: - - # indexing stream - - stream: indexing_stream - component: com.hurence.logisland.stream.spark.structured.StructuredStream - configuration: - read.topics: /a/in - read.topics.serializer: com.hurence.logisland.serializer.KuraProtobufSerializer - read.topics.client.service: mqtt_service - write.topics: /a/out - write.topics.serializer: none - write.topics.client.service: console_service - processorConfigurations: - - - processor: flatten - component: com.hurence.logisland.processor.FlatMap - type: processor - documentation: "extract metrics from root record" - configuration: - keep.root.record: false - copy.root.record.fields: true - leaf.record.type: record_metric - concat.fields: record_name diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java index 3e2542162..6d204b5ae 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java @@ -49,7 +49,7 @@ public static Optional getEngineContext(EngineConfiguration confi new StandardEngineContext(engine, Long.toString(currentId.incrementAndGet())); - // instanciate each related pipelineContext + // instantiate each related pipelineContext configuration.getStreamConfigurations().forEach(pipelineConfig -> { Optional pipelineContext = getStreamContext(pipelineConfig); pipelineContext.ifPresent(engineContext::addStreamContext); @@ -63,6 +63,7 @@ public static Optional getEngineContext(EngineConfiguration confi configuration.getControllerServiceConfigurations() .forEach(engineContext::addControllerServiceConfiguration); + ((AbstractConfigurableComponent)engine).init(engineContext); logger.info("created engine {}", configuration.getComponent()); @@ -70,7 +71,7 @@ public static Optional getEngineContext(EngineConfiguration confi return Optional.of(engineContext); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instanciate engine {} : {}", configuration.getComponent(), e.toString()); + logger.error("unable to instantiate engine {} : {}", configuration.getComponent(), e.toString()); } return Optional.empty(); } diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactoryV2.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactoryV2.java deleted file mode 100644 index 789a60b5c..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactoryV2.java +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.component; - -import com.hurence.logisland.config.v2.EngineConfig; -import com.hurence.logisland.config.v2.ProcessorConfig; -import com.hurence.logisland.config.v2.StreamConfig; -import com.hurence.logisland.engine.EngineContext; -import com.hurence.logisland.engine.ProcessingEngine; -import com.hurence.logisland.engine.StandardEngineContext; -import com.hurence.logisland.processor.ProcessContext; -import com.hurence.logisland.processor.Processor; -import com.hurence.logisland.processor.StandardProcessContext; -import com.hurence.logisland.stream.RecordStream; -import com.hurence.logisland.stream.StandardStreamContext; -import com.hurence.logisland.stream.StreamContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; - - -public final class ComponentFactoryV2 { - - private static Logger logger = LoggerFactory.getLogger(ComponentFactoryV2.class); - - private static final AtomicLong currentId = new AtomicLong(0); - - - public static Optional getEngineContext(EngineConfig configuration) { - try { - final ProcessingEngine engine = - (ProcessingEngine) Class.forName(configuration.getComponent()).newInstance(); - final EngineContext engineContext = - new StandardEngineContext(engine, Long.toString(currentId.incrementAndGet())); - -/* - // instanciate each related pipelineContext - configuration.getStreamConfig().forEach(pipelineConfig -> { - Optional pipelineContext = getStreamContext(pipelineConfig); - pipelineContext.ifPresent(engineContext::addStreamContext); - }); - - configuration.getConfiguration() - .forEach((key, value) -> engineContext.setProperty(key, value)); - - - // load all controller service initialization context - configuration.getControllerServiceConfigurations() - .forEach(engineContext::addControllerServiceConfiguration); - - - logger.info("created engine {}", configuration.getComponent()); -*/ - - return Optional.of(engineContext); - - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instanciate engine {} : {}", configuration.getComponent(), e.toString()); - } - return Optional.empty(); - } - - /** - * Instanciates a stream from of configuration - * - * @param configuration - * @return - */ - public static Optional getStreamContext(StreamConfig configuration) { - try { - final RecordStream recordStream = - (RecordStream) Class.forName(configuration.getComponent()).newInstance(); - final StreamContext instance = - new StandardStreamContext(recordStream, configuration.getStream()); - - // instanciate each related processor - /* configuration.getProcessorConfigurations().forEach(processConfig -> { - Optional processorContext = getProcessContext(processConfig); - processorContext.ifPresent(instance::addProcessContext); - });*/ - - // set the config properties - configuration.getConfiguration() - .entrySet().forEach(e -> instance.setProperty(e.getKey(), e.getValue())); - logger.info("created processor {}", configuration.getComponent()); - return Optional.of(instance); - - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instanciate processor {} : {}", configuration.getComponent(), e.toString()); - } - return Optional.empty(); - } - - public static Optional getProcessContext(ProcessorConfig configuration) { - try { - final Processor processor = (Processor) Class.forName(configuration.getComponent()).newInstance(); - final ProcessContext processContext = - new StandardProcessContext(processor, configuration.getProcessor()); - - // set all properties - configuration.getConfiguration() - .entrySet().forEach(e -> processContext.setProperty(e.getKey(), e.getValue())); - - logger.info("created processor {}", configuration.getComponent()); - return Optional.of(processContext); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instanciate processor {} : {}", configuration.getComponent(), e.toString()); - } - - return Optional.empty(); - } - - -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/AbstractComponentConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/AbstractComponentConfig.java deleted file mode 100644 index 020891329..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/AbstractComponentConfig.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; - - -/** - * Example of yaml configuration - *

- * component: com.hurence.logisland.processor.SplitText - * type: parser - * documentation: a parser that produce events from a REGEX - * configuration: - * key.regex: (\S*):(\S*) - * key.fields: c,d - * value.regex: (\S*):(\S*) - * value.fields: a,b - */ -public abstract class AbstractComponentConfig implements Serializable{ - - private String component = ""; - private String documentation = ""; - - private Map configuration = new HashMap<>(); - - public String getComponent() { - return component; - } - - public void setComponent(String component) { - this.component = component; - } - - public String getDocumentation() { - return documentation; - } - - public void setDocumentation(String documentation) { - this.documentation = documentation; - } - - public Map getConfiguration() { - return configuration; - } - - public void setConfiguration(Map configuration) { - this.configuration = configuration; - } - - @Override - public String toString() { - return "ComponentConfig{" + - "component='" + component + '\'' + - ", documentation='" + documentation + '\'' + - ", configuration=" + configuration + - '}'; - } -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ConfigReader.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ConfigReader.java deleted file mode 100644 index de5823402..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ConfigReader.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.hurence.logisland.config.v2.JobConfig; - -import java.io.File; -import java.io.FileNotFoundException; - - -public class ConfigReader { - - - /** - * Loads a YAML config file - * - * @param configFilePath the path of the config file - * @return a LogislandSessionConfiguration - * @throws Exception - */ - public static JobConfig loadConfig(String configFilePath) throws Exception { - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - File configFile = new File(configFilePath); - - if (!configFile.exists()) { - throw new FileNotFoundException("Error: File " + configFilePath + " not found!"); - } - - return mapper.readValue(configFile, JobConfig.class); - } - -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/EngineConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/EngineConfig.java deleted file mode 100644 index cc92c497e..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/EngineConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - -public class EngineConfig extends AbstractComponentConfig { - - -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/JobConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/JobConfig.java deleted file mode 100644 index d3d061638..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/JobConfig.java +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - - -import java.util.ArrayList; -import java.util.List; - -/** - * Yaml definition of the Logisland config - * - * - * version: 0.9.5 - * documentation: LogIsland analytics main config file. Put here every engine or component config - * engine: spark_engine - * component: com.hurence.logisland.engine.SparkStreamProcessingEngine - * .... - * - * - */ -public class JobConfig { - - private String documentation; - private String version; - private EngineConfig engine; - private List streams = new ArrayList<>(); - private List services = new ArrayList<>(); - - - public List getStreams() { - return streams; - } - - public void setStreams(List streams) { - this.streams = streams; - } - - public List getServices() { - return services; - } - - public void setServices(List services) { - this.services = services; - } - - public String getDocumentation() { - return documentation; - } - - public void setDocumentation(String documentation) { - this.documentation = documentation; - } - - public String getVersion() { - return version; - } - - public void setVersion(String version) { - this.version = version; - } - - public EngineConfig getEngine() { - return engine; - } - - public void setEngine(EngineConfig engine) { - this.engine = engine; - } - - @Override - public String toString() { - return "LogislandSessionConfiguration{" + - "documentation='" + documentation + '\'' + - ", version='" + version + '\'' + - '}'; - } -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ProcessorConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ProcessorConfig.java deleted file mode 100644 index 9e19bc431..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ProcessorConfig.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - - -public class ProcessorConfig extends AbstractComponentConfig { - - private String processor = ""; - - - public String getProcessor() { - return processor; - } - - public void setProcessor(String processor) { - this.processor = processor; - } - - - @Override - public String toString() { - return "ProcessorConfig{" + - "processor='" + processor + '\'' + - '}'; - } -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ServiceConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ServiceConfig.java deleted file mode 100644 index 9455dc03d..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/ServiceConfig.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - - -/** - * Yaml definition of the Logisland service config - * - * - * - controllerService: hbase_service - * component: com.hurence.logisland.service.hbase.HBase_1_1_2_ClientService - * type: service - * documentation: a processor that links - * configuration: - * hadoop.configuration.files: conf/ - * zookeeper.quorum: sandbox - * zookeeper.client.port: 2181 - * zookeeper.znode.parent: - * hbase.client.retries: 3 - * phoenix.client.jar.location: - * - * - */ -public class ServiceConfig extends AbstractComponentConfig { - - private String service = ""; - - public String getService() { - return service; - } - - public void setService(String service) { - this.service = service; - } - - - @Override - public String toString() { - return "ServiceConfig{" + - "service='" + service + '\'' + - '}'; - } -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SinkProviderConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SinkProviderConfig.java deleted file mode 100644 index d4e568fa8..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SinkProviderConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - -public class SinkProviderConfig extends AbstractComponentConfig{ -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SourceProviderConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SourceProviderConfig.java deleted file mode 100644 index 02722c14a..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/SourceProviderConfig.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - -public class SourceProviderConfig extends AbstractComponentConfig{ - - private String source = ""; - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public SourceProviderConfig(String source) { - this.source = source; - } - public SourceProviderConfig(){} -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/StreamConfig.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/StreamConfig.java deleted file mode 100644 index 7a590447b..000000000 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/v2/StreamConfig.java +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (C) 2016 Hurence (support@hurence.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.hurence.logisland.config.v2; - -import java.util.ArrayList; -import java.util.List; - - -/** - * A stream is a component + a set of processorConfigurations - */ -public class StreamConfig extends AbstractComponentConfig { - - private String stream = ""; - - private SourceProviderConfig source; - - private SinkProviderConfig sink = new SinkProviderConfig(); - - private List processors = new ArrayList<>(); - - public String getStream() { - return stream; - } - - public void setStream(String stream) { - this.stream = stream; - } - - public List getProcessors() { - return processors; - } - - public SourceProviderConfig getSource() { - return source; - } - - public void setSource(SourceProviderConfig source) { - this.source = source; - } - - public SinkProviderConfig getSink() { - return sink; - } - - public void setSink(SinkProviderConfig sink) { - this.sink = sink; - } - - public void setProcessors(List processors) { - this.processors = processors; - } - - @Override - public String toString() { - return "StreamConfig{" + - "stream='" + stream + '\'' + - ", processors=" + processors + - '}'; - } -} diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java index e4cf69cbc..d7ea87437 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - * + *

* Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,7 +24,6 @@ import com.hurence.logisland.registry.VariableRegistry; import com.hurence.logisland.validator.ValidationResult; -import java.io.IOException; import java.util.*; import static java.util.Objects.requireNonNull; @@ -56,7 +55,7 @@ public MockProcessContext(final Processor component) { this(component, VariableRegistry.EMPTY_REGISTRY); } - public MockProcessContext(final ControllerService component, final MockProcessContext context, final VariableRegistry variableRegistry) { + public MockProcessContext(final ControllerService component, final MockProcessContext context, final VariableRegistry variableRegistry) { this(component, variableRegistry); try { @@ -244,8 +243,5 @@ public void addControllerService(final String serviceIdentifier, final Controlle config.setAnnotationData(annotationData); } - @Override - public void close() throws IOException { - //do nothing - } + } From 7f9aeb83448e32222c47d156e57260a89d8027eb Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 4 Jun 2018 10:51:55 +0200 Subject: [PATCH 23/63] add rst documentation --- .../_static}/api.yaml | 0 .../_static/logisland_api_flows.png | Bin 0 -> 36603 bytes logisland-documentation/index.rst | 1 + logisland-documentation/rest-api.rst | 689 ++++++++++++++++++ .../engine/spark/remote/RemoteApiClient.java | 2 +- .../remote/RemoteApiComponentFactory.java | 22 +- .../RemoteApiStreamProcessingEngine.scala | 35 +- .../src/main/resources/docs/_static/api.yaml | 328 +++++++++ .../docs/_static/logisland_api_flows.png | Bin 0 -> 36603 bytes .../src/main/resources/docs/index.rst | 1 + .../src/main/resources/docs/rest-api.rst | 689 ++++++++++++++++++ 11 files changed, 1746 insertions(+), 21 deletions(-) rename {logisland-engines/logisland-spark_2_1-engine/src/main/resources => logisland-documentation/_static}/api.yaml (100%) create mode 100644 logisland-documentation/_static/logisland_api_flows.png create mode 100644 logisland-documentation/rest-api.rst create mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/_static/api.yaml create mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/_static/logisland_api_flows.png create mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/rest-api.rst diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml b/logisland-documentation/_static/api.yaml similarity index 100% rename from logisland-engines/logisland-spark_2_1-engine/src/main/resources/api.yaml rename to logisland-documentation/_static/api.yaml diff --git a/logisland-documentation/_static/logisland_api_flows.png b/logisland-documentation/_static/logisland_api_flows.png new file mode 100644 index 0000000000000000000000000000000000000000..713d5eb61e917d36e1f7b91532fdf1ed5236fbdb GIT binary patch literal 36603 zcmbsQbx<8`@GgoDPJm#+f@^ShcXxMBa0%`bG%VcRLkJ!`I0^3V?(XhqzTe*ak8`SS z-8%OUMJ;NWS<`P%_tV`^_Zy+2B>f%<9|-_}_p&k)Y5)L54gS6$LW5`KvKfuR4|p?q zX$j!%zn{Fe;zR%-1!N^eH9WJ9vpo#3qzU?R8Rw8a2-_%3y5Mm{MA2KMCUnfABBabS zB4jzF67dp$IdVi$Rrlce#I~8>wvc|uvL+Auer;5_c5?dEcL+x-CkT0ZIiF}*Sg_2= zOkW#4K0ZbfQ^jIoVZpz}$h*N4lU%F0T`Ha5 z;J5q9VrsW;t;PEjBNY{uWN}w5jo!b1$%KS*3Vz9_GHIeV*45dGH7=Zn6#*Qc|=WcrGV3U)TOpNg6A!0PJi#Kgo( ziw8oWWF$^#NC@84UIW}zmmOAmdOG+$3bL~Caz*BSG4D}O%6&Qb`F)OND`H86-QC@1 zk8aRNgg9JJGx{$KCPHw(*9rsv|l!Y!*n-zqH!lnjp|rm%*@oNl8FVq_g(pd31%o-e}p%IYXUs4 zMz~X%43U5j)JndxT0egNoP@Uyr-c+as?Sv!uZ<7;Gn#m(y_YD_P)cW$WJ*PVgCi33 zc3r6XV#npP)f-K%lm!57M#64~)02~Gj*e#)`i+%(^@F3Ml~VjrO5nLwP~MHMaPT5I)#=Ti$oJAM9%saagoHRa7M7MCZ?7+f z(y`EhC_0Jnxu~wLE@-&ij;pQSH#R;#PpNNA%|5(dm)o+5pP)LMT=q2RG^1JH1?e<8 z5CLiF=}mwBOjAwajSIhLl=!NES{kw|cqUovyq+t-bAUCJybm zs^bt5DfXpv+Ts8VMy+WDzt-1H0Q3k6_$_7<#etzAan+>0t$`h9NI|pP5j-FrLs*i4 zeR_Hdz8@|`&$&v&Fhm7?ZQQh{X844-H~;{>(zIH~^?PjW7)ii$d|DbJ02(;3esaQy z8DZqmx3)&o^?Q0+0~r|^4K4O>FhaoP0QL2Y5w`p3GH<}^^XBH};I|olNxcv%Dk=b- zfIyyKtF20(qA!MsLN=ktOfiiG3IHu0YP~<6;(BhdBm4D%i;D|%?trtlyF;_Azd&I# ze8G$jR9=mMr$7!_)~hxHPRC?SX6DktRB^}4?b7`G{P+Jk81);vdwO*B^|fPgh>3+< zb`>B6@F;jJ-q%*?3bzx|Br{g%qUc?g%`Q4G?Uh{n{?=Yz5247oDBmncvnH%K2vPgc zFK_0IL6b_)%=9>(uXc7uaSLgxvL!`DMFq~!H^jwdoSoT#@jFM?vm@~FRdwR3)1cty zW@a@tHK@b_Rt^qE8XBMRTDU>vfP)l-p%Qt69`4xjl0q!zOXW(!$<5u!6dwtJR2sH~ zna)34?Ntf&4GfeN6cp4TLglF!E48<`XY;w4QO>d8&CFo88C6ww$B~JvU`0nnEHu~? zh@#8N%3hEE`=we4bPf$6i^iRwpM!vsoRpL$=ySW;=EIrs0xlvrD5#*Y(B94tgoCMk zaRkrmwt{@0(x#>+FE6i!g$4N(hLXs!*Vk779R~-8gai$U1)cGHUDQ5aIg2NM?x?M` zH4Ke}_?+;OSNf(55HGaacp7XFxq0{TKUdEJ^){LNzR{hC-<<;`H;P2K{ZTi}6uwJl z0D?`wFAn`Ju)*BiJTW&{xs4|yAtgmh7^zGQ6g_z&30$;zktLpE>jnHteqv&x=rCu@ z&nP@rXds1AUyJvAxk{<&=~ZP&bMR4=0#ED{Of{*fD5y zFa)sz9hQ}wJD1aTb|jPgeHS&q`?2TS%blWvf~SCyhzKBSla9lD#~`jpPk_dHus~UNivkM|U$byLIXMX$Yli??s91bfR=HLg6e%+^GZj@4(`j1{ zDOT;K_Z2!JAw#m9pn$-=ufK%Ee{XZ!*-K9^BQa53zNL5kAB%a~NYCy_=2#Xl48Rj} zbOWN`-HOM@0ak!LW6~FeSYUo-#gG)86mb%Sry}`Ohoy#!qHHrOY!*QUczE-!y8mCp z_5b(ap7~g@E>iVXLL-P$Hi7a$d}Pa%KGet&SyB~CvRV$5s6i{@^I)AR$4}^KaW~fm z5Y3b2vXhJ+Y2+(4tPjKrmA|gxMDY9D9Gv$tLT|Ed8#}4h(=rHgd=`a4} z`reaX+E1a-<3sm7g+3VWDWB9zrn|dC#`Y)> zgG_bgpR}mc*z5`4mjgE{M55?)$;~O7UVgM{HOkAHQ3tOft4o2Wj(IeTpECG=DiA2c z(22Zgq$U{+Vkx@~x&{4_wgx&aYQZ?0Ir6jRp7CU^%yh6$_Ln2vuRgp_qUfnw=7eE~ z`#0XvQs(^^TtE202Za3hfDuJ&`s9K8ILeZ_BYI&o2YQw-iY=T05m zpq`x0W-oSV1eNBMrYNqTL@542tqQ60SIoD+^DL#dY;?vact+yj|Nkeo%iLWoyUEUX zw=g>v^Heem!;~b*M3RiB%+C}q`k?al(A~l&21nH9k5OF6X<;=HtbyxUXx%d#W{*y^ z0xlq_y0PM4r7$H}l4@A8@vtxXhoX<@FVgW?vitRelgr`$ zZRIZl<+_K(dS7UP8!-h4<-6=w%34Ru55i~!Uv3S(WIxI7zsm3T)a8VB+#`r+It7p0 zCKvG%4PW~c&k88o=o%sAPjRTn)cebb&9%Vn&JEe*6<1VcequaKYo!UM%o*A6%~yCE zvOio96&LEz!SRl+akMPS4R^EFlM(g)>?N9EtV}PAW?L#yUBHq(e)P)!@(}Tc?Wbv* zq@~+%zY-I`c+D<7wJrIPPt!}+2x83q-_>UYf8% znueD(^n`Jt5k`dW1j=jg3b`uHak$_OJ1lbyJx;@6?`#P9b%>EXCSRMBD7qdjI+po{ z9r5TcOb{ktvQZi3JHZnyQkpOq9YUzyO>#uB1lWAdqdKHEsS>&|D%VOtWS^5Vzi|Dg z&7R^~Z_TLkdN1&s(D@d8a%&uQg9K|9<-0b$$|)L|1QGN+8WHqP9RVEa?;Car;|J5u zZAy}64M3pGX_z)$=Icsc@_`p1oc*LLyYK z@mi^p-;4@rE{{7;4?mB7DO7erstOgR)A&sT;rJdz`HtJRHIZmdgLLC}+;hQX+|oO1 z<$CgTn9)WzN7N^^> z(~_j^L5ZZF|8j7cwWLXqp}TnBiULW2?=qb`-@n*Y zpqi3lmCTjWs~B#mDAQZ=46EGB*{eY$xzFLC)m_7De&1^8 z^>W(%*9SHG$z{x*R)ryE+^9ge-Xut-zsgrn!!AM3NJbTXydyKcWFY^Fl?oeM-ooeM z)}6IGoj(zu+g8%O`nIFtJGpS~`rA!ShgIN5qJzJ$XC$TVh(Qui@{HO?mhR zJND|l0`h4_H<3;hILdI_s4&k<@7L-UCK^cJwWYSeipJ4%pHd3Q7_!WHO{;X+#y{1U zB5@fP%#}Vr@gT!3Z@dzRsrSa7d+$ZwASIAqJDh692qJ0AV**8_7b*akW*$%n8YOt)EIT%@!s{aetoUtH!COYF5_(cgraH2wee z2VQ@SbT?;U`eeqvA((%mA+3rfN7tT9)W7G%6tc|cMOI#$IYiCg+s^N`ee(T@HJ0S6 z$08C}3H{xJYITHkkRwF$-$7!yu6}xANXoda^#>3FI9B*|o^IV~8R?B%?HD62{T^?< zZI?;8!eNq{ogrS5khJ^h^Ogzh%gcNi!C)!);iu1o)!ago=us&uo54vlnW!{w^NYE{ zr^^#10cb-lwCM)lKL6D=mxNw@2?-BZUR%HVGewP7Tiw8-CsnT`Beu6x1_Qtv|4GkL zuQp+VeS9JvYOqS`5vYcfJv?BDj%x0()@Fe^KHF0|-QQ&Kk{COoJ~ihF7xHHQWO|k3 zV#C;UAKA#)#HU2KY;AcSjLT#>c2=u z-*>aFrR=MTFc3gKOjjZ%laRK)u zkC_pN>>HA@u{A)R-jI@R+9gz8UETBL&AIKyK)`7e>+4puq#SA{eI*vQZVm7Cm|&wz zq^4N@Kw;kkVDiN}CPA*EIV*-W;36Z9Lo-GVhd2`zs6P&0{C%`_ z8f4!Hts~%!$!HfsT#$$zZLq1|olMS*&D|~j@#WHM8Oo23Zs_WU4_n~O|LynYSvLcv zymj@94lEFba;>F_weLpvxL;F`kZ|=!7payJp%Um+2~e&t_|s6yUov;=im{|KK(`GU zOF8~NY7$zsc{OYBbR;wEal}-$;>B1jLxm1eC$@wN1cNj(l^T0wJs&2Jv#a$v<;o?A zEx!bY6cHpx78=W%rX}Jsjqe{CZ40Nask`K2js742jxof)7)B;+v0R&t-~I>*1rMsG zuS9sm0z}6nbv8{KHItNh^%X9^F`-553@}HNkrYq7$gjfFk$|r*PSZ4U2FH{DI#H%X zwPNT`R7z1aLh%{y4Ga#ZqBu%fYU!3gzt!K#mTah^#TZ7-?uQ2E!AB&7aE(u0ZVePF zN-$q*t0oonTb?zn+d;*C8U3JNS_c5+7{{3EMf1fayeTMpe#tHx+AV5|tW{YyE@ggU-iK&HK z2u%k}Y`;hyPS;Z+QkI;VP(ua&dP0*)DN(VBbP2`EA7fCB!k^zsq+QjN!x4uP%{Ba} zn$@!jSrDiO0W(|$>%ht$8mM+c`&ubMTE8V7N-awDMS6{5ffTUpkUFo zkp<8r?`Px|*7J`-w)1NQ!rvt3?Z;Twp)}=(gpsEDLE+t2{nfnusXVgLFp7H>h@~cE5^> zVJMUN^>05K6e?)QaRpPSJ`w#G_HNNIu!0K^rR8ePwwJt;&8Df2W^9t~2) zHHO%gQIBk!Qn8k`<_--co-$thJ5*p~ZLpSRWhD(PV5?3K^pB2MI)%Vd$pOEs*{0~^ zIWNh@s>lwcO7zfRCF6Y1kQ|tn&HoiG8!C1@`9g-LDf6m^;*nX$%v3Zw92yXVmj4hv z+?H1r&1R2O9Pd*gk}@CA<hP8Ws*b zQ0U^y(#7gS^@m8CUiuIq!MGe2z_s+rZZ=?KmSA1hlhf{i&KeQ7$sW2WZ&1OS(V$o1 zJPXa5rcIKJrE~r;=<*?1vc&W*K?dS{wu!^hUCY65Bw?Y1w+aJPuEcE0m#gQ{(LgTu zCyD~l%iaSjB7!Z4qyT^g_W_~h^;$K+hn<6(`AtgVa_U$B$fLWH#E>YN zg$fL9DoG!fpfd|#u?GA?8gBo=eMH&CK-pVXrUig8Bf|>C@0-W47;5{UE#a@>su&rb zW(cp3%pj&#*Du}uA;;Hhstt>u^vww{a^C}mVeeJ6u@0x5n;rOc()*D~(n)#Q*RJ6M zBelDl$X&8->^SCUqttU!DW#l+eSKJfkmaZiM1|bcMP#o?Qbb5?Aqf^b<-*?qhr?f| zJG@TVO->X7a3l)1aDa=8I(vx>UAKkeHCtAwnWt(5A;34UpM)#GwwFR9@3LRU6=W74 z(d2qVz+I7`uKP}|8 zR3N;6s(GNl_9pCuB4rf3>JQba_A2(Jx?9Zd78N=TsCaCs!4OlFM*cYwjtwz4)9AY*Ii=s$fZ8BFjmT`_GL!Of<%5 zx?`F}s=NA=y5r6is`6@ui^>=WxJUCIjNi4G33vV>v-uXVkcA^RT)X-R2TAw~$ zuHs$v_r>3PY~-#u)n2ZIm7Uktw6+&v1hsu0CnkJfW%X-44z9PQx`y64?E9VgA&Dh90GjhPB`?;@jck>OUFT}`eOzM@XbeY*Vk z0SfnQ;x8;f{=p^W=Pg4_U=DlIy+r=h2eWvV&bcQCH6bEip^I<9ZA38}jpUoJ^G@%u zd9NgPDMe!?#lJV(t%eKV-n0`EB>#tp7vGdU`qob_wi@U?dY9TJJ?;}^{2&Rm^u2}8*xPi-5 zOYoD&?iJ5KY);Js<|1i0v*}Uj6Z02roS{Bg*P6x@?9`<2<<`5o_A&uo%Wu;$lsB#w z>yY6jZzf1J3l1)zV?dTk?Xc)9eJHe3@avbA^jBvcxooT|&17s#D)eNB8djWG>Le%F zpFon7ZmDWYLI|q4Qxi08xFe*R$Gf5?lPLLR+Z5zzCB0-Dl~55Ht1!>K3Rm@i?zPnX z5sNMep{yhPMw@9Vwm*$88KGsUM6hvAMc57tOhe4tqy{z-|A>7I+SGGvYA#R#b4Wx; z9-ay%qeAM@B!3n$T#~z(R`$THU*sU3MQ7 zz)g}2ki>jk@38m|+q&WCpHz%~PBqI=`JJ9+2k{4DP|8kG*6MwZOg{cjZM|VC9RT6m(z?=~on(zz=&4TKl1x z-;#PTem9Xr3RF;}SqUw#<^@<~$2QA8!;1KEC* zCB@X1xa{y`K$r0;NBnV}h7)8)1Az(p?P-?NpT(ro{22pFQk76t6=m=1JthQ9Ghs#dP3^II z4!H5=1u#Z;FBHpZ5`HM>cVXZRa#wFgpvY1X=128>pa>+zTfM)tpqsK2RKPNK3aKaV z8Mkr#u0qobQ5vl0*jdt%(Pemck{V5q>lepOl(ZlMX&gv(-XtwzkOsHdP&tI4UXk{f zufvTMGw;Ps$f-jcVo1ahp^>?+4)zQ*KT_8?0- z{n98dOCq)32tCLcgUQFCYArnmxg*U0(RZTbbg~5ghKW;pId9}($n&zKfW#Vl{nAX1 z4BgWA|h)j6;L7i&Y>1FPaB9qHGjyX zdWvW!$FprkwKHRzwF9+&@ZDD>S1tCLSnX(>t$!&noX6Egvl`x9$Ef z5*0BhPr%zTHi;PfTZmI=5a^03_j*AoR-rq*0_>skzq#M`?iy<=`WW2 z$8aaAtRbnO&6wi#?p;aUk#{pl7QWaCTMbQ49Gigs-gJdeIFVig#4%g;UyPmq7hhcu zWhn4}W&uP5a|liTyZ3}avDY=gS(xg^`dg(~(cGbox22qCw$_9gdI-52Vt%hHoLKq4 z$<@iffBy~+%F!i*y^jCIHnZUCLLCZ@2t$Ig?EY-#k_XGSIC$F zN4`j{M1u||5+vcnOznSF*b08pBVg2=<(#C%&Lb`rM-S;SyLj-$k~}B?FT76qoqQ+6 zt@Q4|wHfT~f=wHTmFACRyfDeLaG>w?IAx7kTzLuz<&N!LH5oV%*zD&e|KCN#VP$ms zCXzRIbm0H};ta~!*_jeEA~`u3Y(h<7a-JWbIAayHeB@xjiS##^(!%Gn`tqMV`k*}h zNGcs(X|5M}G!RofLl(xFcNh^80>sLcnCTfA`0R!-$v*h5QjLslAFcjp4MyNSX-MO7 z3uCUlqLC>`FB%~u7A{ToOl)5`77OM}$MFa2xb!qZ<@wQz6t41G{ap;xC@Au^O0E0% zY!~ol#p3koBi`css6#xwsP~0@|FwDkH>GcG!osE4;4ldGsWS&P48h(M5ZK)<@dzW% z|FdY&d-8|u$mb?vTj7onMSf*$fe43IBKWXbaj>_WDeI{?HQOW1G^Zk%*{xyz8~vAe zvmBKgZSV2be2izt>$&x#xKDA+tx|913!yh74=ZbRdK9>?{}{av1~gY zn30)u5!z}=?tKdhCyPztDl|4?wMTQWV4D^Yp~MWhKX{hXqi3XDZP*zzBKAW-^C_fl zg=uQtJc};#zCIRo?VcaZ&Fucey6`xsht*}_&%;f|wEMiUQrErP1tO zZ0+q7KD#{5vw^KRw|`}Cogi6@5F1`sN8wiVizNX3=CNGfRUAJ&Asd7KlMqJ!SdMV* z3aG7>H`gPRO$%y`U)QQ=@<>B<^>C1T&$uc@?@CP3&cUS2l!`y824)=8NBwQz$G`J) z!--u09imJQ*-(K6zm~YG1bpN2RQeeiD#D@>6EX%-E+|fyTX8W4cP@PHZsKJWpaMxF zBKMSpywj#DEYH3{iI>e6`{WxoyHm^3Q3%eowS)((e+8#5eiPi8llOeWd=u>|FF&!l z9$)@9=&`=;zv>Tfd%EJSigm+tW4eRoXw*SS;HOQ_s!2nq!*Dx{wl}*HO_wereL7Lg z9Yh-O?rzQJEpbq&wfjV$Tg3G^4e5LDQ?mjZ7#}RKikQ*8hm0bm;jz@xAq+6QT^#wfG~gX0!;W(9dhvJR2`Zp*53H}^ zgqcY=-%{UOXTa~iQ2lm2aAg`Z%EbS+prRA!arL-U z_;yPC(3=_mZlP~%rT6P|cxdj&hOz+OTU4cF^(aWI%YEnQtAB9iir3c-?{WnTqos(M zm0ye>fX?OR!@2r5LO!pN-q}@?e5poSNHc-I?OG+{%L}~WJH^(ojJs4qb?i|SvrYVw zQ0vmqd+HhZh*P<7Z@OG(b%x;RBC(WH$FQQ@Qm=Dfd93E4#&oo zpPE0BIQ#MXDs-01o%t?5+Z1KyqKOCiRR{Z`SwseO8vH9Erdi*#AyR!Xh>n2;uFGpFconEBRb3mw*#%dl}bz?D&h4jUlFk+)(0zQy2O z5t7=ua{;=otd|!rfEY5?u=PV&BWdo;TcY?^+z=F~q&7BxH*TAukVU2PiIdAqwR$-? z0MnY@TYQ-ZuP{7a{TDRA-C~)3YD52n=DW9!o*+xHj^@sCWsDglXgH{JgE2HXm>noL zxbl!N`1Im&d~DB^`N54F1<a-CWguV zHU@}$CM5T@aj$OSQ9_u4KHu+^^tj zGiA#v{K?Hs+*#MTl977vFrDwdACnLc-;xf1_C*n-;e*wFjK=uX{MqMuv*~yognOSaw3`T74lrX*qp+ z+(-VKZ0wQZ)K%~C*)Em#qhDIyN@$ofxn3xMwCmYx+OuIfMK5&sBfa%F)v$ZB4+E6} zW4Ct%Sk4E-Gedju_i*lqYI8d~GzqP~8?Fi(-SgbV&#jxv086|~<1ycCll&cs2}*uR z0!LfSxUc8(UH;!e#Wi=_W~A$83A>1O;-14n3w7-JZI`vE>VKM+vUkv5ny+~C$(6A2 zX54O#t_U6;E->vV8kyjz%@uxeawOiE^yCLjsJ0xQ2e&p?T04#yxbVFmvC%AD1C8?v zYlq~$`QLZemT3w*c8n}V->FGTc`u#^8aXOvKTI(Pa2V6k6(#h6gMw=L=NDfuk@1>M zhmfyiqPC>$1!P)<{gcju&Qk7<=D5i|4Y#E#HyQ~^$-k^++XCx-Bms7)TPNH3pL@23 zvKePqyzkTXbtbwZ6y=rNcmMh=-TRu}Ks7fE<-HnudtcXkd~L?X_&Ho9Zfuf>MK|N?N=9P^z-U46c$WQ$4D~C$7BPCq16+8e1r$px_N_5S!9a zCsX?zo$PvYV#zV{Ax5WKo3ue?6xA|8eeT zZt6 zefikM66D#C^IRv})u-%h`A`203P>HY{j^2{kyDb>X?k=-4YxHjnvd zYJ}V8TKBHeYDcB&x|-^m1_CgGf~g%|N+Nnhx0?FL7iz9}JfT9o-Yw+)vFn1Q4in z$r+g3aoG~Lhk=tiSlpF{HP!WZ#ONyeA?Z`Sw?_`rZx#A&txQ}lE;T~F0soRbEDYbt z8`StRj+#LMVWzph?d!`kzn1PMpJAkZJJ!{Kd7pg&AWHxjCpnGZ?z4(L1}b3-R&tvB zStbndQJFY^&GzzGc{fx_FsE^U&ntJ#GY)HSWlfmw5*5JO-uo#i*Y5g?1tcY>d=`+s zy8Jl*N5SkD(tn!lkqo_7(g4tXFV81?# zykGliVpokyXwihc1IPu8>j%K3cXl-2i0kUoximKB2PL&qfRAhADe)={wB7MBI74iN zHA7E<*FP{&T=1GEdef^u9A{) zRbyQ7n&8lJbV0{i-g4G3e`6ICU`+bE-sSi2I-4Jg>@Gt=&#iRd!cmXUkGE>S%vGn8 zj1qBpZr7^7LIK`}4H*>EA<4;#J3g&h#|q6&x0e?!PB&_Lhg~_ue&Nw5&7Q9sb{Y~_ zZp<)#&Hf$hQ&H5hnFIc|VXB6O`x9kjX-T5#ph6UPV+r5zG#)da9~MY6cQ- z6I0l)( z$(uP+LYyxJCdGt)%+ChrtkF__FN2{3+RQPT=Um|I;oVx}6Ig6?p~GGrZzX2maBw|3 zkW@jvAI{LupRrLQ=4Y|c=|Hno_})RXqEJ$<ye}u3PLaP(UCV3acu6muiY;Z-3iW_d(d3IcoGc zU4#Kz5nySsA^8-&001fYCN3=~6JM6~|5Bj;s6QTB z^5qy2LJ)p?W(Qh1&5gDcXX0^mpK|iw9c*g2*+np;%l-J78*sBiAeu<$I6VD&%L|$S zN(EezSWb5OMF$Bk%JGuZQ11*j_73BTs>{T#Pg>Ys6Uqr#=RyNq&lf|F^&PIKi*2!U zffI#mR@TKCX>qRrXl()TRAONo}g$B3zg|@{6;LZQ;%mAMmG&V0Mxzkc-@?R2?OJk|e(k9GUL*TT6Zz7A<-27?$ObKUWYM%|yf`p-c0(KVk~Rq#)p zu>!Uwu9wm&YbX&?tTz55dEk4ii{Pt2W%$TP?gu|&(0oa(l(cyyeaZk5DTo79YX|H(Mg17Ck{D--OhM z`tc{?%-4#Xl@l+Y=FnljemY44l*Gamo8@y5lcfiZR z(E8TXej)U?{ZwIhuiLA_Cr`Yv4_UTmv$#;2QOp;cznzP z&a2y!6=&!TfqVOd)+y;byv?OW{x(6s(ML^T!o}M3S8JY8y{kLZSfP`z-@>#fFO^YC z3n}_XsHb+zBXS;H?spTd&4f<3n92w@!UHe?Qt}ed!^v|m0yF*o5${^lK3ZUZOYnR$ zn|h5=pTz%3?)<{!H_3Ew0<-=->0laKhz6 zc)oXPp1naNTB*r6#x1THLZ8j|?w7(~T4I*bAqp`hi%Fv56fm&>HE&HZOgc2UG)eolw-=BklLia(T&*LL3VTd zvS6L%Ri3Fib663H5N^-T#hYGB-Gg%r=M?@DUr-*fUdwHmWe-b;0}rUNwiIRCc!O2~ z)0yk~ACWsg<@E(RIBz#)#0E4OmA;vvx4<004HOEavz@V7;qp z&C)`kazdkRvd!y;5Kzb!99#Z}5AYBY10mr?P{4C38Cqqi6m$e{vzEYOHqU%T^(iepc`lCoGHR$|M_r3=*PKpHePfHef=|S>apvhlT`T%HfS79rM-^XcwuPs-8bNPk40JGe@h9EW`t5iRE}*sn{P) z-}5`~%VGj5b+kQTzb*)$llz#+5Fn)$g1kPGvcgNM@`r1;>i7xs5d!`O4|h)_%;)&% z_k$WzF@DIv?8~hn5Jk*gKd8#@V+nk3!j%gc8TD05%bmJ+Wh}RcTE8g#41hc~={o>S zVW0!gQXP2$_>*6koc0fGX?U%&?N<^F;DBM`tt{>Gi+!E0FFJ)($nu;k{Au91;ogbp zK6Qdz8iI)nPhYGae8fH%7EGOhcBjP1{&@b-oC>tPKj#)Ug8J<6HyCvXNED( z;ymwa6ZYkxMzj0b$@+Fi{Ohk;pDNT!!%{*b_Kg(~c+mo*DMtADNB8t_8uGsg#UEu{ zM6M_M#J;{~iFHXOX8}sFB<)$Oj!FdF)>7U!&fD2X?NAhl2`ZiAtSai@lE|w+zceKJ zwZ1qS3I;qqhPJ;TOdQ&LZsu~28Jat0NlrIz^Mv%_jtBf?y9&59#$Oi}pxfN74ql?C z8~oJ%B5L^T7*O||BXrb|J4|7Kek2deEA?-{+ldy)7^t zra5G>GF8lZ%MvYXeMm)JQeb)ZiaP2b7XQbZN;)w$(`)lwNyu1bEA6w(73GPk>-Gs5 z#Tv&?&{F@M8=VSzFU|AzNQEBMEpqpEsOt|$T}!KaIAU+tM~7D`{BbD{dCEPB~EMo!RBa~kB%8*5ot;g8?e7_F{xBW>L?(4O5GBr-J z?DuGp2mHqBXX~VAd_ zcXJb_=Vqs3X78C?92p#aovAMSmPZrYmztI_Fw782?!+z|^DSRWPtyKFWRIXg+u@Ca zeskJTmzxq%3%}dp%5Mo(tS%>9xf@OjBz@eSXd+2Y!4{v8t zP$~#_*~b3=-#hz%it;ul1^f+bor)$XO6uOxv~804zT09)i>Dh7W_tU_kzpeeBlVe* zB&3dDJ-RG)tYV~EEzL0m;$V*YP zPlXvQmHpE8ad{F(Hohy`7}8|q$FP)^ecSGyA6q419~?V5VKs8CVqo8@Hdel3r@mDt z5H{I~HJ-*RX48wd0MiwUM-RNovDe_n@b11<`#=o7*H@w=YLqk1PkC2GwbIHYD<3%K z{S-v@A}wQp&hRsowT1(2z8bv>{M{|A4rkz3*B5 zkblD|cEv$qKl3w~yDCe+`txa=s$=ELlgxcZ9Ov89r>~yl{nd^yEqhg9vIHBbuRWGN zIP7zK%bKMVm)d4BniRXsgi0vHB+MPTiclui$92jhI63Fmw{hf-I_lkq+NY~YZa42l z*E#iACAhcD=f=*EpL)aLx3PET+u>{e_N}YnboJ5n^(o*)Xs#uf#7~ljZrCT`R;5B~ z^m#k;vL<0Ts*l*uxKFXxX1m_u2-5A5l_cE9gIJk_@+yoB-Yb7+_X9#6 z-z=r#7rWO<5VfJhW7TQs2IV3VV)1fwGZnd|amNThVsq4AzViI-yKDO6mVJJUpr&4{ zWatCtlBxW+a${v*50)n!+8*Ihh`M&aw)!FXJfI4BJmy&nuZ7FrapK>MlWnurdwyR7B28EdDmL322?Q&mJD0{LP=W@0Hy@;O{NW=R1k0{8*+}alH3IWv5%a zSJo$q!(;Z(=xLj`&-=Q%>OyeVqjYwqneuaB*9N#HScN;X2OdZu?Y5YFeeD9ny3N@> z=0|_Ozi4H2xoUB_=C;OeRSDXBTDW7@moBx#j1}*V87u6NFKe<5`68(%5iTYgD|0e0 zCgwnbKuYGfT+#LMT}F(?O#e|qbHb*W_t`&fGt)0y3v(Vj&27mrWl?7{pwkq`9G`9+ zC|0rN+|pPNHNVZU=0CDIA17hU?=JGXo`aCTJo)pIEPa@~eUkJx*(!TuQNtWDy%iupgY099w%`ee$)#eeSh#rJPAXI~kxAk+$eij5q<;&0cw zhP^Y|%?6eux_o9>U&rG=n30Q*rEJ{hi(fM0^Q$y{&2zt%;<4~RX`-)OiFW5kR` zEnJKact_&mKWv&)R2m4B%vk+%FhXi^i&({P@DT1OL}>JWLKQ{-(GsrhFgg|T6rb$K z^Kr5EkeIUD_iADK;KMVR4S_x5L(i1t=$FHha3)xi44!2QtKUx>e)@lwOI!5Y*hWK2 zNObg;6baS>E?V!((Bh^A^R_lxsr1d z0eo<%{N_F0#(ciHpSF?R+J8a%dY3l1PkUuxXQ$OSH}z>^Lx1+|%+#!0s*no>emRQK zD$u7Q7h8CA=K_|oro7q7`_-heZ2(jMm?|x{qO+*=PEu-eNEUy)^;~+2 zdq_TU-G`YH%|Yb9;U2GTa`|y(NtY?^eV$o@UY)B0J?9ef`HJJanvK>Y$`MYgU|mN8 zne1>4!0Zx;a1UF{Q$4vUo`8jgt*b*;RLIm``DRFPgy+(?yOufgAq%TaiyDkn1gme)^d$P_=b=FPiw1qz&(B zYj7lH)WP@L@5?9&_%JO=epRCPkOG+J@H0V-$EV(4Fiv_22-Q+hiqrq_(;6fuHX>-q z)oVQYF&Frk5EZvim+=GxrYoRMd1k`Z{po$3AwzU;%=C)_o zCzEc{Ox<9M9p(Sx>@9%eh`u+`#w7#^5C{?of#4E?J0TDxXmAY>JcGLo8r!%P4JLu}O!#2rNTJ^u&v zL49B``k`2{)98gp9L!Ux10SCXNYAI~8<8xLa{bVJ=u4bmmZB^*P&7Q)k;@{;%e3D7 z?q^r_$R1VW&Y8r28Zix*GT*cvd{Gik-6@MSBALTITpsgyE8#8)V zNIy8@OHd}u^H^2aR-{=?=LhO0Jp69>hyo#o5~+u2Y#D4k++`0-8)B7&9GkmNm&+k{ zN9`tc;zJuG6kmH}3G6+rdz`;`~!o~!B!nE*9*kB zW}QK7V*BJae}$9um&w;e!rYQiZ*A*J{%4qggCwNUh5qAFsyM^xDJdxUUI1Vw1%gL! z*xO%EHVMtm7yxQyJ(T;#3Wm_x!1`$b`iSCmL3(1nu~t_`xfsS45l@G2o!ss0;{4KK zY%HQqKkLY{p&8sydbkrrQCa^RsG=H90t6odLYo<}{qOE>cUcXXJO9*%l}lYLnoWU`g7DCmfpI#07(b0f$7-MBz_ z!s*SfXNizJV0qQ`w5snypC+|yhm)bg$NN~CDj{l3NzC>i?`E{wq4)*s*VEHMpjGqhV)$>s?GZKYW zNMFVO7cd~MT`JNaKmF){bvVaoJUbhtyXQ9|YUT9MHqC_O5&w4-?C;ej8ke=eUHhK@ z6~9J`$$qdj8M8ZC+dE)&bNnL}Ehg~4_wtiy@d9<6j_BVnHalYixg)7Y=DIHd63ntY zwF}>{|G{scXev;w6Kf-D*6Z8BBI_zpk_2cN{+6hWuVttJ?O!N(t1UhO;?hHrxF9D+ zqun7N@a@^MHK#@k0<9$`oT?$@8m6Fq1>oUt0qr|4_ALMT?9Gj^!nFH-RJz|F6p)?E z98mlpSG?42U*Ee%$|&rq^;fHR)c(T64mnLDWTPK8;-ZskYb7rLX!6+*3a4IHIl6jk z=S}1*fiAlf<5j)Bb39{MOV$g3vis|p4jE-nI*sYh>NidXL}9j``rkE-03S3O1Hco- z`T27C;2;H#e0#YHQc?LS4R3pA()bW+%LP2tpB-NdYOk`Bm%~Xc%yYxFUk`95x1~rv zA9C5obQ@}bFsTr%^9w+Z`Zz$K_G_O7qBZbI??9vJNv&!-?l-6a<`VY6#p;!YrVVT$ zcz|(wJ^Vr51N znZPNN8qUwdgNar7VrKudPd@IE{WUnH(tXD_CrIFmj8%++U0No1Xt4V$eLs6Ve4xc|gxI{-rO`azE>Tge<7kH0PXaYGy+cZiKYZkSfv3#&m885Iue@5AZ z_v)7w{zabYr7DD^h*%Iv<7FGZ>~*m$+`#YWYCuxaWwa@-ZapkcE`;LXxqC2|p7v@w|@iQg#m<+xAnIlb40S4c77a^AzKJ006x6ZtAb@&p4jtWkZA-T3Mj9~bi$p-Yy32b$a@ z6*?l3(irZxv&dpCSgDNJ2zeZ^94&e@0~Dcmv#nqs=6Q|%#VJ#-=N7VhWVKt?9>0IR zcKc-3-x`&%$e9`x^g=LA0IqlJM=yN(=Q<2)da7%`4IjBLRtvcy1lKkdi0~O-=q#3M zH|VpL9SL!iKl~Znpl+(KsaDAMq39o8R-=67_gkBpj_ziqDNWKqd1Kizj2YW9EtH+` z!bZ@oX~bh~`@tHg0}-|lOBoR$^xHtJgRtx-C-a*o{BTwu-_yV6x!!$T&**gFd~UV& z3@Pa>BgQ;M^4iR^sCgs83ZPO9Kb%_KF$@z4HxBn^e6IAR5^C7 z+I3()#$3Fr5OLSxQAs$S>QCSY>j8H!=ij~mFc|;%7zF1VoIE>JcKnB**hrSR&1r=y zMfp88y7bpuS5GmmX6PkFip>2|q@dX^R!%?oyU+%7<1Jx18Khj#_ z&i8dt%=8H})!7K9_y>l};X_eN@aS@C4yC-`TsS9kL?Z63PqQ+zkEPF?-?#$e62WBr zP-~Nsci<2QhK9e`#2e=BjpSxiQZYQ4Z`c1xVStD~TJIfRl8(JV?|2c{IB9}+{YEUh1{x(yqY436I8Y-gAh9zC^a?7M5= z;o_H0n7qklP0PIu^nay;HbU33QzJpv%(3S^?ovzItYE&#dav`X5JMZn6YXL#U8-Nl zc+aGSXSPicL??kOFUv+d^%28nqd%dh=(V9iuE}J#-8v$(W7u_rsdP&iz;>B{P*jb zBLBJnOKRi`(aHQ%3oGds1leGT~xpTbmGh@vfFGVh07A^put3y{&qq;m$OI3i3Y=fQ}8}>&LbulsxLRi^7^OF;& z?8?&DE_J^#Ry4yj7h6o_*d)+Cf*+K1JjK%wB^aj(iO zL4CPuF=XHUaC4#LIo;VdQ8!gH@l!0BZYwGkdC!FvY*Afz_}fcl??1-*=S_=4?j_3= zw<8;0uhIlj(RRSUlU$u9gAlcobhp_^86D2Fj`?Ve>oIw z&lCf}C5MHn=H@8N=rudIjJ}#!4NjsA5w|E8khy+hKsC0-e#>>R^Y8jPg$D1vu!tCK zG~M@`@FW%RQi8yIrlp7C;%GqV)5RX{pD_K*1-pZBYU3XX7Y(PnNwtS>e5JoRu1_p; zmaqH({`P-csda)}r^3N`A^<<`w*`aH&+~>JD#-Ze&D~HiV){zzZTmO% z^vzb9WS@`t5x%M2RPT3H{(6XtA2-x&u;qAIhS1X+SXsRb>vi9GOt{oe4+$4bZ`{*Y zzdF_%fSB1{jJOLrW5?+n$yE50O!OAmE{eo`w@;Q~tLw(E+Vm@{Gk-`SmzX!ElvwT| z^=rDu7|owLZej1i+n(7HB;m8~UjFk2*dBTOc|1InE287pL2qZmjeJ-tn2W0F(RUj)X`{twJx{th@jZGC;ETRU+e409`%WNw8H(76D z##ETos%$J-4gQGILV6r@Mi%SLf@pPAII*%Dtxa~{e_K&jer%f6b`Srw=NCR_b~J&( z>-KG6SbOWBt2~lMA_0b!lxg_*IyE+nbnn=hYEA6|<7sPoNkfD4N#N&fd{ab$QJI)O zjcND#G2p?x%KWf5^F6NPSk#CC{L83;J1-Okp}mAS?)qr)V5aG~X0(3b-N$3uG)6EY z**@0|S=xU5Lv0~$aHhq}5uFs5r;;?p`-~>w)H#&3727ub)5sv2_+U1|t)XE~kWq*D zZB&ihalo=L!Z$zd#Gs%empvfE!Te~UCPzhRC5V$OwGppWA>(*XQC#GB-<9ttn1scy zl$C`$9wd@!b8lDs^5EzBx{O(aN6@Rs>Jwp2(3!y;NgN&No(k=!@2^{>jjwmUB>`yg z3L!~P01(svft4cIKrIr?ZC>P_$b8?)Zpse)G|Yx;2lHKlA9sVy(UNAs%|Xy@0GbuB zy0cOzyCMv%&h_*1oIx9$?Dk+YRVw5_FyZAq7}x6+2&CgTW!Kd=!vfq()n1>oRGM9D zrw!d+C!9&y(BiH>BT^;6Zu|h*+qvV0wJl}P*YnrU{)1Pdy)2a zq&k8YPw8+wm^P+A%CgFn{NB%g!?6$BJF^Sf`{^R!{rL`beJpc|_jstw%Js5ebn&lr z2@^7yYgU+vC=uDq@@*>tT@()(#YZH*gzcNqO59)nTB+0>ihXHzhegzC=r*U;AGh6s zM8iY}5B$y~fq+tR^lQAy91&o=#jpl-=8;f+{aOD^dodxlh!(f5Bq`Y79e2?IRB8ch{U*~**~ zO?74Zgwx|yHL4T{9$pW801M%TsT~gRMbt;u$zuCAu{?qK7bmCtWL`JLya~{OBuP$& zBj@-)1WF9Iy{&ZaiP8JU7uS_Nmv&GKpRe>r0VxWz^GeQilpC8R7dTIt2KKNEs;R}6 zkjVCrW)10Cc@MD1g9AP-CDx9Q7`?D-A~xqmV&a)+o}DNMl$WF?IdY<6J~@>9Etc=+ zi$bsf*XKh90+s6lf|2-+Fn71J?KY^!S?A(k6kX?*ql1DhvXMlU?q)E<(KI{6q(EY( zOw-YziwoteQDQFS*I^jRyjFPHHTi?Nq;+P@XD6`!;CnCDH7ScC<*S?=${OQ6D(p$i zpIo*#vs3g0LuvPYx1V}5DUS2*1HL~&6 z@pA*Ow7eyTbG1^feT*jhz}+XV(!e)ps9FA*sLA;Py{-kFr=n=VD51lzm1F~e(48Bh`Mxk0>U41*O-=2xT0G-PPyhpjPIXCXxc>(Jp3>}6Xf^zf5ChAroI3*sNlPiD1hjUr&WP;gnAX+3# zIfw=wnC$L`wjRL=Bj_|s6~ooIVpYL0@fw4I?vos$`}Bg$LaB&HNC`h@ifUbU&WGbC zm-nI)Vxb9gI{}5P5{0hN4oIr(b3o(lc*c_|=^y za#$x0^hjTwvskY+kASagQZtOl9!B(<$a??IfPgcM%v~FI(%nCY(KH>jM00kGd!aU2 zu(hC9!!8RZO;@{1l`(ZOn!@3BIz$=rdv>XYTlI}~{xUQyHn}AGwIt=@ts$5LMKZx# z5w{u62>L8Ug>R+GCSZ2ztwMjx#*&Fc*Id8t?0eSjW3P^l7o}4A2}K8OB_$#bSdpjJ z9KMocoTBRCmkbWGvpcnijAbGxtP?V2(!})l^h|5h#jwBZ1GPIkwSQdeoiWSqvu4#| z`i%S8Z5^U~m`!br%?9ju`6za)5LkZdKwB}ZV5sVYJU0K6Rc*%{I+Z9L6NtoB1es_R zNxE7xi>39%r3|B#&~{rKm_l2m^NdX``5NYyMYrZ~I4cynDWwGFsFHD%-d}dUv0MVJMdcav9SXhL9YE_}Z zQM@e+{xwh-9`ZMCuDg#8WMlx-NUQ_crah#jb_c zt)jj%uAI=9+x@COu1=OXEi^ZUIrm_9SD`18wUE9Fb{zK7oQ-4UpFO*=b70#haOjry<^6yTSIVJryG2 z*XB&kdN~?khA08O-@=GBuvlrlyPm#mfxk;_dus>No0W>I?(Xg0<{#Knu2D|47L65h zDtgLhRioOAR`W#xc&}0s0m0|=(fo^U`ln)V3iDs-t)iSZy8yJ7ZJX(l|O&oNCGr8;4;o;&D!H=!p`e_6&WP-00ZmsZN zj{<&lkDSv>=Ft_wB@v6;ZF_6LVk1(~w@NCDDD~up;Q)UsDrQ_8Vk9Id^$=u0yLqn= zwM6*^@T%a!fL5gzwSG~_T}k3Ux1$;Rm z)F)|384+*-ju$NOfN>VcV5@8nP`A)$=BtQ>-rIGDowq%0PM)sr z{yiB4+l;FQ35@j#UlS8+IrT^9SLPWpm)>c#I_tk2HyQqKlUS0eWFJFbr%=4&JlZ6E zkKj_v@AO~{^Gk!?9?Al%tGf>ijcp}({b1TVxUPl8I%lg=8H%^n$F@%wACGJ%W3n~T zz*f)C%vHQ?LGxZmr}x%9p2bPIMsLb-D2BUYRiQ4BtK8w9Q^)n<5)3k44zo!uC@3W^ z-|+@9+z8fX3{7U* zR&M2wR^xT0zta%n`#EjDX#S39RBZABgb)3@HZv467IPLRO^HaxK5EB`-Edl> zOb4c`v`5`BE^>c}nnPQcd@+b^9?}p#8MzO%X!^2XiQ8RD;ggOkZFZD4rmpvEHK_Cv zHREJaCv!QUfJT=^%cm9r88Ue$u)#3JrT;X@Rk%xAs`=~~Q}*Y4pA->ruu$2s@%5kg zzH4;OhGFi~S?l)7i^^JU-SzT1+z~1^Bg@j@(Z)o)eTLw&ibrg61+C#W=ao<1LQ}?#?#wo-BS( zo;c5g3^SS2&-|j*7x~flC->MgHn*JI{rc)L<}>eThvhZ8$< z^Q-3HQfQIEVL>f+8`fg#HIJ$9O#ub1tn91Q^{eA$XUmn)cbz@Wta>CM*Ss%7^0gYj zY9I|tcB7cYA%AyB(#=(UfJG8C^v(5lyp_c?tMnIq;VRny-(al#5FyAZ_FrcN{2wvg zkbtKrfxJ+%QmYHlq|5TVfQ|M$PY2UN77a$MZD6AuU#5gX7EIXsI>%8Z#}O;Yx(m0Y z?)H56t3(U|5X)M-)_IME37cI0K9$!rbGMH%9+`$lofmP5s#e)Jq9Zh7V7juMcV4##SJUAVLT z=1TNDL_p@oe(w#2ncBS>r7mO1R$u zTQg-(JU0scD(a(!6|ys)zy>`5r!+L>qRrUDUu*`2Kt3%iUdrPr8aL|E&Ab zy=r~!y`_?rsIt1HdK|Zo&mzDNa16i?kS-Fm^unI2p1T2yU{Y|0_lBSzEKUnBvd=|7 z4eiJEgzQdq_v&7RYbWdbzBz6?JrA>K$ifx7);J%~Gry04YRRObB7Z3Xmy5B0R^pjt zd8tZe^|FXSbBWJ&-hq`gvVXJ1`MSmGQ19!Cxsy}FK%6T98G%Mg*TjUvavH1sdeZli z75}@VUM0#f6wu23cJDYG@FHX8s0ZnanAKv~7MQKLrUVev>T!r-1dMm{1K|&N96zwp zrJS?D0|BxddD3o&Ehbf^emx_1rfbDv)Plj#(JGZ6yD3RPcud0E>zlEJsILHsLa41z z)W3zg!Lmmaxy9|+cUtOtY%PxYsx5Xn1W0$7R1>cmX6AcJzY+4f_%$1Gr)onu4pBqh z2)XSYJ=}L*uefq|+P0~1qg?$uH$6PxUN|mN7DluS74DJ9ffAmR5Gv4K@DdvSmZ?g zq3gHblF8KkSM%S#xpqKfSrNBj*C`T`H4R(EF_TrJhzu7`hCWf<8QW+ww5h`H}l$rZl$R&1cv5h80Hst_jgnHdLcJ zUaD!ZQE9u>z-m2z8f<;^i#_OO0ahO@V#w0@z&=Q=Y~DzYT};n>fP6L(X|@JiFQ;LeGR zN)-qVSUxcV&9(x?4-w+Wi>=zKs$kSs8$*t%^J|MeXn5d$#e&(+gDiGht(MLgvomn% zMrG}7btU__Y^a^(!&;9D-y=JYmyYH;+opdj!X_W)hm$_>pPIOm=FX{ts!2-uvBIt^ zK0t8zxw%YdAxzM*N@>3qH23fVzkoiVf}-llvPRjRgULq-%*^c$^A5x}lqAZvP*#}R zZcR?Fb=o*sY1pYEY@WDG&iq1Um^x8xlWx~2jb_}J^oF#`ZMPzCJ^7n?Y+>r=9t2i) z7N@ zPX1`UN49f!Kb!TxzlO1F`C=)T%F5YPTCRNt{>#b{{U4#yyNVa`ZPiwBR>Xw9XrdG< zb+6L~pE&ZLu`xRPeTuELj^`?0f%sb4IMQ%(dMl=`u4?R5{#01zdx7VlFb|I_utXCi z(U0XQgn~goHoVDh(Sp8!pzV>pi6OJcV|w%#FSOt0S&i?ek;E@23$eWn3`B?fF~ltR z4|+YWJ#4x;T}pA%B34+ayUUkl9w2{DiX|9UbaD?z-1(m$tTc{&;(DlDD3bmQR1SlOy z#PVjK*Vogx;RkMw$kbE?;*YKZHn58K_n<241`(d3=S7@z8f#Dua2Yv&0HyfTZEu4+ zgjaF?9IeBPd`c}i%kNqy`QoVk-TPcI!8%&s7%fv-s3~&iI5bkb0)4M< zgj01FaaDAuO9tpRhyAz5t*N&=xo)I9(}mwfC*O(N#lB-?=ib)cZ2$AVC)jP{Mi7N7 zjn|~0@B1^S>ECjdvZauP^2c)jS4kbrw`=?HemKPTh&E*GtNC3H1lRu_tK)ihlz7SO z)^x!#5%8`M_!m$mYIax+kN6ys|Fujgl2XK66WQa@rMBkGlo!;GpEL4BrONt_hgadB zYfZ6WiDmI zZ|Bq!Kwb6%m{WUW&U`Zj?*Z!eqM#t1hic)i)+XS`RANaB0?N0vJ9X%Cl`|LGmzO4Y zg;K6Wf)>LUP$+~E^0%#vFL+Q~7NnCN&e)j2ByGF8_gylQc{{dOEi+sy5rD?*yW+A5 z((d26Zu1%>3o5TS1>G0O6~1PE{>~8L#(L;pJ*FC#=aY{qt!9UFeeA|%@R0w9Ka6BK zYi6GTGTion+ssLB>Cxg{)#4r1;{E+t;PKY=4=5UaFq9X{Mr_X?>UPuVkd|~wiGx4ErsQya74j@icsJbz zo@Bw3HDwy?i@aIlWiY4z=iW<_+op?!p?qZ(oGm-MbJaQn_me+MElstqpzs*yPYNk{ zWNx!qsy~{E>cxzaDWO==%ITct&bNHpEvSQ?yk1h>sqQ&UcjkNeQ zJ^{*r*AB0gnFxJT+TJP|?qnLcTN3j>+#PVcwq9iH`b2(30-YP@6Rx~(U*3iZ+Dtbm zRBgx1$p&=i`U7B{_w`AeJJ%NH01i0DWqD2@d~qSMpKrIO@&*_@{!>epuYBLBX`%Wg#Om!2V;lKY)vf2341f)0nJpD62G~W*YCLSArF3omKNHIikpitxci%I@hh!KPU z`R%WU8;9%ljySSFKWw6KCjKLtI6ldF^i_EVDH$HO9bU7LdC+WF2M44k_Y+&H9 z)cZIs=^81mk=yzLQ9$R1i4C^@tHeqe7idzwX1w2c2&pXdCdZD=+$##r;&uraW@7_) zFt)!h!vC5)F}NNy`kG18SPcPNIte`8L}T8kQnj_r%;UG0F)Q2Pw83p_si9`GWZ(>TkIc(NG&hfy3JUfJXQ)PMiqTM2>iIdDQaPGUYi?0_ zSAjHgRH+3eD!G*DR3fk=!7L_;)5g{L&i&z8+hna;e3_5mTePu&MMs3*Q?aq744x8x zc4J`byO!^L9eo5gg*F8Nrs6xZP42AFt^cYD1SH%A z2M0Gd^VO(B*ocV#l8#9U+o`K47QrdX+WqdnQ%{;p+2~8TA7=C%}Ufgwbe6CS1nb{|n*YSf@m*y)Ywd%64xJ`#oSqzo&r zhltzd%k2A@vU|W}vm75>e-tvrHpRIT1x-Li{T3fu8by;~)o9iuuqw&p+l2s9zk*6zfVVJ4P%xqhWWE8ZHi#f)?^qw-%;El9yte@oZ ziZ<0GKkeVZtdODW1lEC3Eq*6W#+wELffo1TtKYvSi<+GJ9u|tuHkf}>FVhdotd1L< zzel%8WwpK3R{FFX*b2?+hRO2G;RL`QY_qhr!tr~l#7)<0M7e9s1MNiIF6|cg_NJVw zJrQGnD=>Sp21k4(B2*t?#qh;N?CX^f@U^-#dNVIhY=;3sj70)4K)Q&ZZxT6RoQK>4 zgqNoQdkp*Sl?)=pgju1w#z4E(D;5^xpnBJu-`rwZ9vLqgN{oV?OAUT-c}BT)kmo4)P)(db@ zQoGwCb|e{iN#Qe5hJjHrQvJy)h9J0dfKVly56*WD{0^B^ig@+5`&B1P@+NM!M@EQd zOu_lMZMQO|PV?a^dkTn=3j8iIh&3Rb|4Vp@0Rcy1^@>`&-7@wgYqh6=F%WDfN+`3- zj)YNy;?Z$%lc@lm858XUSQr|t&Gqx`!m8a!oCg4s=9@YLbeSgD{psY>2eNjczFqN8PL4^7bx zj5CP1@SM%p`$xa}*4|lZ{epQpFhq(|&f3Lmb%bz7RIlf~(Xs7?eMMDlFSWt54fkt; zc~mO8C&et^a<+`+= zlF>|vvO>hwWTCcOy1p9(M>S@LdU=t#KQ|w3_9}ekPG}qC%Mw0L2Ynr-X7Z^sJN%g! znPZ4g(iWTFiJ#l07OSg<+-X_x<)?o4r_nJ&31EP*TGaJJTU%L3QdVKUD)Bknwc*b~ z2Pu@a*Vbll!iwgk-pQ_iG7+FkdyteO&cOqg6S&CB{(9hgIniH9 zWA%KOAJidC`>*1IQ%a3 z)o977PE)gYxS;bH4f*1XgOqghn0;&i(8DRpwO=$thhrZXzRuIt>*1 z@#rWSrqyXJ)=bUj|8;fcp?T}q-_E1MJAYan-~u-G1~B%${dp7!eL5_OQFf8_*)C!i zIEwsDc7-2f3Uw#rYBHRGG-%Ps<{DAGe7}?X{8V70g7vm#biQb-_qg?{r|7YC1&V#e<{3%T_T(C`$k0gqx?$%;t>N3* zF!6hm1ISCB8u>CqwOAx%z}P!CrOCUk%tafz#wl3L%9^cnEmIao>R%x?pHE*B9DK0u zf~9BSyGYCMmke)G|DwG0Q(h>Kqit<4ho$~NzF&ouiUs3%WgFvDaZR3C7>9Bl8Ff(C z+1w{T@ve@0oG^Pb9+3dV@5>~6n?HSj`bZx42T2APlt=;V9`Wv-=poka7JY z2pt8jYd8KK2R!^!`M>n;xhV(qV$%u zX|_>mAZ&&7d3zRa=JVS{*>9U@ISl0h_9csb_i+>Y18zCrlOeP)mI&+HryfsOIpbB*gn8wEmzlZ;8V+#UFBVvUTzFC8esC> zA#F$`dK0DiD#ieqjU0R5ISaD$hsVXaUyTn9IBY0CyFn)J+qUt}9pU@huEPkQmKyil z%-d2EjH1=%_?!|kfvei-^pS}osShgxkIoXt-=L-u58Exh!0KA|N1xn#{#IBGm*328OYQbSN(Bx|7FMkH-Sxe9?SME7;cOnlGs zIaC$*bTV8sd$D{69l*eZWzzm-flV!rAq~`h$$KU7`>bX`7QWqMnY3DzdkgcOYAidE zEh%4DsC|D&<~mmE-J=YN%^}X?Y1r542e*xVC2D0zOMHY;bYF=yyOhV3ghJ`EID(M1 zn69-amvl)q-BLX_l5a+nb5Z>^r`zy2Z&)Cfw#E&N@SOFq#l zi~(e%zVYphdRB{XCqoGDF~Ize1Kaj}%EGi!VMarSk>Ht`Kb!xtExeB;m8ui-tiyi4R& z#82ugcu>0}3ua^R{3p)Ff%dWG#A$qCB!>Y8$cbe%oRIO22==_%oJ#~eniR1@P&J4b zrgCI7HBXk~0+zvltRl?-L|Mwn!Y{16S=yb4x@yxy!svi5*4wi$%B%~q(Tf5cN-uyC z^?KN(6Gee^AFZ6SLPC;I@gyM&5H9Ict^NJ}_T)KU-Ooe`bD)wk0M0`ukNTD()q~=e z;=Kp}r1fTn@W30vrRS@5;`%$mqy52+<&EsIWOyI;q^EY$*(<>pc`#c#U^pq`RSjUDCsv5z1EzM`(_Y@k5cn%0h&O>fUa zV~0^)Bhqfqw1WkM`eQN;`)6H=%bML!bXz88niZglkNn8%&i9jdFl^X|LT&4ne&Pa;)p8Yq@9oH~V^rj%0yGiDRU5BR6p~^(SZ{ z4iHNB8EoUh2}xlQWNGV95)faSel;t-HCRJDZi{m)o_vlpu{u)wT+)BaeYr66Q7ojC zTrG+?uPT>LyuW zx)F^)GTZ8Hge0X84x-?LJP%-dG}-efknwL|)H(WMa!H0$+r+;NB=QrNHTicq5~m|w zwCcThB^yI10RT&NTN)@S(&@K9H*(XlIZ2>X+#il<0jrY@#PAi7t7!FEG8~!f$%yo< z_e*GQ!0KQQFMLFEW^OR<1FyUZTW7VULn>D`L|!?Vhu1K?>?B2{3Be}sr!DrpWP`=PqiF%`xbW)xv?SIfjs#J! z^BJ;>>*JIA$Pvlj%n&wT-CP_*U${;}X9ttHrh#75kkAqEp1*$w1>9~uJu27fu&MtR zU8h1%8Z$yvg38VOr1E^>d^fUb3B~k1bQif0do8`FzYTxXZhV6?Teb8j^5_kjFw&WPmGSk zQn+J!SV(#+f{Qg16c?wPrdsIlC5Z#p5QWjre^cA;&%zeC$p%w~0HLP#0!^deYd>Y) z9q_$kHBk)9S#iHg+a_?8)|&~Xj*gvVl8IHcm1w~w3>8V4hPoA$!sE_`_;pmCPnKcA z4q!i%&KaH$a>7?Q$Oz~(BubV(2CLTYNkX-s+67Xa_*mH(#+R2He>a(FXZWt7fgVv0 zz~_06G!LS`tGrUlz-{q`#5Xe zA@|)B_4gasw^Vh#G)OAx@cn_4@>FHKKsbDbTqdZt+~+MB>F?*#+lM9T_b$WlS<$AJ z^YCdwX%^dr@JreM_fNmq)<8Y}lVYarTF@p}UoQ&-PeuEWYy(z_$gl>FANrB`D7#ppb$r9SuCR#Z3o_mwbZOS!q?9iDep@7k`?|lkgF-`s9V=m zIn(#)#a_(d>5y8ac%z<~K3Mu`dR%?%XsO~Ioc*}Hk=qWZ(=h3;zv-9* z#;4e_U-@sEVy_}fV7mh3pt2Ftth>|>DI5KF0Jy;2uGp^_yfVgqiLe%BRt;l zJxO!s==pMt657G>*IO*rsha^`ST!OBLz$z;#k6<{#NF;_E`|>~yo>~vOI1kt=51MW4Fe) zl|kFDyp!|h!W8solYglBZU!K}!Y?n@jtYx%t%!NALpfviBG5XD&^xr!8e0Nlk|mRp zR!TdV`H~30U;){rGrmyC*Q0g?TCZTyL7jUvuITQ(uRCUzi zP^j|a*vd+zWPZkmpk%-9Z@nDwEe<4lj|$PXx6-Eb)s0^Cq{cQwXPrVyaEKUq5J0>F(EYx&^x6aeZ7-^K913-=Y<#U zio^Lx6FDbu?UGDx2jjDYHrCQ4aqObD%28J2ZeqGd1arWG(sGQ0DsQ7T3vIid;H8CQ zdBE;VRCGwtRFpnAY1I%3fl!ER#9*yK*!X~T8hA1iria8AVa4+jE}2(+@BZNG5q`j? zH}(!SFis{%OARMS^3WA)asKinNxM}9XcwQL6b9Ne^a;*vKgW~=Qb(U4y^1xcKjF*x zB1CJl(H5{uNv0aR8T5vpU*-fY73f6sqQ;Xbel!1Mrl{0`mTHPj#*Nb{{IR;-6 zQa|2<7D*k6;~F7uDl&l#+9q#@aaXx$qi6j~NYs3{O??dJA`LyFD0L{KRjV{}HW14h zfS@S&Na`+2O`4i*G^$rQF9t?6U%YdKC!q#KuzZ7@HE;4Njw_Du8b!iO9hxm7G+_>3 z&JIg2BWFM_bMp!Vpcapmjalxi#Q1Tu^cPR#_vn~R-%iA->~}9&trfd<&%s)D9U3Y} z_*lI74t*i;&8wUYqOD>uxKqpQu^yyJAYCKsI=@bRmiF6xA#DTXP;h+jg9BtKy%n~= zWL4j&Mf~$y3jn+CS50w?oT)eNPYLM?j6Pf|QR>L~z~I`?6_G9j$w5YvJ}*XUUVaw? zCqCPQ{B~r2ejx&J*9&3oQ+I2^!KgUat zftD(gndtJgbmzSYw@i5M384)5_UTS=fYwPymwGXFT&(8a7rGm@vEhEYU4xn4z;)&B zNS^j$3qck1o|lY{pV8`&Q94n*sL35W2{RZRrh`oy@@ryAoM*IWlfNN5u0HZ zIkJSSK04_u)G1H;gHR^i)oU~1gCnfjs7$4NM|B?iMf=MU$uNuol1(%;<6rpdT7}C1 zQ8+Ah;v2E~MjnSq>oyWyP-Di09i|q0;ydsNf+;ytUtKO!S^PhhA@YtO*cuFD^MGC2 zQNY27={?3GDf=2;bP-Q|;#bVG6R`uqYe|w`tFK6f8E5Za&VdJ)kL*NI&iQXEg4i+}^Bde0>JSDh=3qWB8j~6+OBwH~q;|BiB z{BW+ffFPVKzp0T?o^!8>+8%>Mo|EK;h}82Y5zUX7b_Y`+30@vU^I} z0lEB-u$LG+39k|-(Ah9LReLYy34i69U(UC;uL-NZ+?9@M*&H;sa zJQ3pAax@pt{J?A!+Wy#B3!v}d>04bAE49yVxF6SkiUGRA+)U?ZlDfK+S?e+MD%J~GVj*Xjms_>h$1BHR zf0MFu89?V^wqm1Dsiq9#2k)}j;Oyo;1G~0si|$K#!UtAik`m+KSUuid%B1iNC38o)Esug7Vb*STJe+5j zl^rpe$gN;-d46)MP<1`q?7O+SaXR0Y$QcJWSKuo!#8p~a8bvKD^X=PWt6P&Q1GE4} z_c7OKBvnE}qM@N7g~u*Yz>R01QY@=NtH~bhBqkP?puQ8@CFDBoZnAShZNZ2ItdgwC?ozVcg%)bqO_U zF9pTh^;{bQne%>aWofkX=tK?Lw28nki9vfa6qQFaFx6)sc-z|9>87WrHJY@U|9({# z7jZ9@>hq}dfiL6y#Om4q44vGJh#_?9cEM#U#vw@PD$ z1!jANE{$W;7#>ofNN^6_f9OzWs&V;|v;+;mQO#wpFBhmJr2A}Hr-$MQ$&S8Z{vZV?&m7li!y4UwZZf>_+V7_R=uQ z^|;+Ov%}7BXujF;2zizD`N7ULu(_tKUpV*ckESd7*+F5VsP0^sxzv9QFdVwS1m^aV zwD7}ixkFNEV<^W`ZZt8W@PIGJXZ1i+Ns1;Eg)%xdhHs9dD)=}c!ougGL_gvxvr*DN;d zFL@-zp`EiG|MF3=5)MwiMaWis9EVx;o!h2ucw)YR1AVCV=;eM*=TNcFV9Egb$l zAVA_pmT^h#iI0T~uWV#8hv@gawtZYrMe<Ak0HvH$ zR0P7pj0dAYcC7YYUT&@%utGnhxG?aO+*?f3VxVwRUmvQ8bCK4@79om)@c4;V@@N_7 z`Sa)B5a4}(eV4Yk&}rv3P=t|NzV6YzHa0drka6mBaa`T`{q34SM>dJe6q~%sbck+s zqLGuKew1}7B8I(Vlnjqx>xDmmZ0WFNqcU&mo%FJgqpPbRq64HnY2c^7SRYRLW3oFV zFE39-b7$e#Fa1$mJrEUVsXM zEO&K9_2$q45ikqyjBmQeVu2=;WfcvM*8vv4|MuG-n$pg#rl!u{8~WPcude3k$cc$A z9axcv`$JIs+S=NR3LBl4ZRvafQ$fJm&nw#ti;7&K1XIj47N~^hcA93mR7BJEE(|*Z zw~0p$Z{M;woN#~bsXnEr?QQe1!UV4M!^7}W{4T$6?Js~>-sl4X1o*xoD z{jwIXj%rmZ6)j+XZVs|OIGm=n&2f0GY@eFFIwlfBC?t;^z?iK1U$yuFwFwCcu<)MtGkA$d#?Ot$$2L_Ij9+b4?Q z{!_h-m9SP=BGTF2U2N^^=H~~xpbxDpQALY9>i-_moh|@a-o$8Sm7+y=V&{1>xdVU% z8akaC6&3Yju$Z~{O0Jvib}$e5Wu;59u^!=Q0Y*OT>zgrFe+r^0DY|(AT(U$z9zlev zFg;@NW9?E}OG`@y&Jyh&(<6~=fTt79-!HUUSy{n{^Xr^j)LuS_?N6ROfj)BB?5><; z;hj6$+K`|5T+lgFMT^UKR^Gk`+~0CCzXc! z&=+ELx{5|ooV2VxY4Th{_~q5NgZ1=WsJ2MY%34^?0Z&n6?R&Oln^bt3hqU#}yo>tc z6&SiLek_busvYGetL_OA>w{08EQGM(sU%>D2Fckl`bX#swQ&Oe!SeeI+nOm@#Z<6;w-6lSozpyU#v}kO zT)(3Yt4ELV1_Hf5Q>)d0e#3xSKo}1>+221^=-!*Dt;}$thBK}NaW~ho| ZJu<=f-?)@L2#I+{A!tls)3Nip{{uE_<7WT> literal 0 HcmV?d00001 diff --git a/logisland-documentation/index.rst b/logisland-documentation/index.rst index 790ad2863..738798892 100644 --- a/logisland-documentation/index.rst +++ b/logisland-documentation/index.rst @@ -28,6 +28,7 @@ Contents: developer tutorials/index api + rest-api components changes faq diff --git a/logisland-documentation/rest-api.rst b/logisland-documentation/rest-api.rst new file mode 100644 index 000000000..0f389f32b --- /dev/null +++ b/logisland-documentation/rest-api.rst @@ -0,0 +1,689 @@ +Logisland REST API +================== + +**The Logisland REST API for third party applications.** + + +.. toctree:: +:maxdepth: 3 + +------------ +Introduction +------------ +Logisland makes available a standard RESTful API definition to interoperate with any third party application implementing it. + +The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to: + +- Ask for configuration changes to be triggered. +- Report the latest configuration applied (to ease up resynchronization and business continuity). + + +Both flows can hence be resumed by the following sequence diagram: + +.. image:: /_static/logisland_api_flows.png + + + +------------ +Usage +------------ + +In terms of API, two degrees of freedom are possible: + +- **Dataflow**: + + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). + + Act at dataflow level if you want to: + + - Add/Remove any streaming endpoint + - Change any active stream configuration (e.g. kafka topic) + - Create/Remote/Modify any service + + +- **Pipeline**: + + A pipeline is a processing chain acting on a data flowing point-to-point. + + The api gives you the possibility to have a finer-grained control of what is going of any stream pipeline without perturbing the stream itself. + This means that the processor chain will be dynamically reconfigured without the need of stopping the stream and reconfigure the whole dataflow. + + Act at pipeline level if you want to: + + - Add/Remove processors in the pipeline + + - Change any processor configuration + +.. hint:: As a general rule, the changes will be triggered if the *lastUpdated* field of the object you are going to modify is fresher than the one known by logisland. + + +----------------- +API Specification +----------------- + +This section resumes the Rest API specification. More details are available on the `swagger spec `_. + +========== +Operations +========== + +GET ``/dataflows/{dataflowName}`` +--------------------------------- + + +Summary ++++++++ + +Retrieves the configuration for a specified dataflow + +Description ++++++++++++ + +.. raw:: html + + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed by a pipeline and reach one or more destinations (sinks). +Logisland will call this endpoint to know which configuration should be run. + + This endpoint also supports HTTP caching (Last-Updated, If-Modified-Since) as per RFC 7232, section 3.3 + +Parameters +++++++++++ + +.. csv-table:: +:delim: | + :header: "Name", "Located in", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 15, 10, 10, 10, 20, 30 + + dataflowName | path | Yes | string | | | the dataflow name (aka the logisland job name) + + +Request ++++++++ + + +Headers +^^^^^^^ + +.. code-block:: javascript + + If-Modified-Since: Timestamp of last response + + +Responses ++++++++++ + +**200** +^^^^^^^ + +Return the dataflow configuration. +On logisland side, the following will happen: +- At dataflow level: + + - Fully reconfigure a dataflow (stop and then start) if nothing is running (initial state) or if lastUpdated is fresher than the one of the already running dataflow. + + In this case be aware that old stream and services will be destroyed and + new ones will be created. + + - Do nothing otherwise (keep running the active dataflow) + +- At pipeline level: + + - The processor chain will be fully reconfigured if and only if the pipeline lastUpdated is fresher than the lastUpdated known by the system. + + In any case the stream is never stopped. + + +Type: :ref:`Versioned ` extended :ref:`inline ` + +**Example:** + +.. code-block:: javascript + + { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "services": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ], + "streams": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + } + ] + } + +**304** +^^^^^^^ + +Nothing has been modified since the last call. + +In this case the body content will be completely ignored +(hence the server can answer with an empty body to save network and resources). + + + +**404** +^^^^^^^ + +Not found (the server probably does not handle this dataflow) + + +**default** +^^^^^^^^^^^ + +Unexpected error + + + +POST ``/dataflows/{dataflowName}`` +---------------------------------- + + +Summary ++++++++ + +Push the configuration of running dataflows. + +Description ++++++++++++ + +.. raw:: html + + In order to ensure business continuity, Logisland will contact the third party application in order to push a snapshot of the current configuration. +The endpoint will be called: +- On a regular basis (according to logisland configuration). +- Each time the a dataflow or a pipeline configuration change has been applied. + +This service can be seen as well as a liveness ping. + +Parameters +++++++++++ + +.. csv-table:: +:delim: | + :header: "Name", "Located in", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 15, 10, 10, 10, 20, 30 + + jobId | path | Yes | string | | | logisland job id (aka the engine name) + dataflowName | path | Yes | string | | | the dataflow name (aka the logisland job name) + + +Request ++++++++ + + + +.. _d_68b618b2088b15f9f9f912df4be811df: + +Body +^^^^ + +A streaming pipeline. + +:ref:`Versioned ` extended :ref:`inline ` + +.. _i_ae1816015667b75409cb3251ba13c032: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + services | No | array of :ref:`Component ` | | | The service controllers. + streams | No | array of :ref:`Component ` extended :ref:`inline ` | | | The engine properties. + +.. code-block:: javascript + + { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "services": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ], + "streams": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + } + ] + } + +Responses ++++++++++ + +**default** +^^^^^^^^^^^ + +The server should return HTTP 200 OK. +By the way, the response is ignored by Logisland since the operation +has a *fire and forget* nature. + + +=============== +Data Structures +=============== + +.. _d_3c2b4cd64485b5f73be7a1facba6ed8c: + +Component Model Structure +------------------------- + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + component | Yes | string | | | + config | No | array of :ref:`Property ` | | | + documentation | No | string | | | + name | Yes | string | | | + +.. _d_68b618b2088b15f9f9f912df4be811df: + +DataFlow Model Structure +------------------------ + +A streaming pipeline. + +:ref:`Versioned ` extended :ref:`inline ` + +.. _i_ae1816015667b75409cb3251ba13c032: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + services | No | array of :ref:`Component ` | | | The service controllers. + streams | No | array of :ref:`Component ` extended :ref:`inline ` | | | The engine properties. + +.. _d_0752e439d11d3f0d4f6b437e63ea7248: + +Pipeline Model Structure +------------------------ + +Tracks stream processing pipeline configuration + +:ref:`Versioned ` extended :ref:`inline ` + +.. _i_f3879f767282c180c5b651f138c40b05: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + processors | No | array of :ref:`Component ` | | | + +.. _d_865032b24aeb47b5fd3a07f7e49d88fd: + +Processor Model Structure +------------------------- + +A logisland 'processor'. + +:ref:`Component ` + +.. _d_28dca67a05e18e9f96317b5bef61d056: + +Property Model Structure +------------------------ + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + key | Yes | string | | | + type | No | string | | {'default': 'string'} | + value | Yes | string | | | + +.. _d_35858dd5b9e97d51acc7f109ceb3deb0: + +Service Model Structure +----------------------- + +A logisland 'controller service'. + +:ref:`Component ` + +.. _d_ab44feb101835c4602a49f15a25615a8: + +Stream Model Structure +---------------------- + +:ref:`Component ` extended :ref:`inline ` + +.. _i_09545770fbf157c057309e15e402b2f4: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + component | Yes | string | | | + config | No | array of :ref:`Property ` | | | + documentation | No | string | | | + name | Yes | string | | | + pipeline | No | :ref:`Versioned ` extended :ref:`inline ` | | | + +.. _d_bcefda54d79a3bedfa83231aed8d38b1: + +Versioned Model Structure +------------------------- + +a versioned component + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index 0c1e9df1e..5eab152d3 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -229,7 +229,7 @@ public void pushDataFlow(String dataflowName, DataFlow dataFlow) { .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) .post(RequestBody.create( okhttp3.MediaType.parse(MediaType.APPLICATION_JSON), - mapper.writeValueAsBytes(dataFlow))) + mapper.writeValueAsString(dataFlow))) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java index 462bdedf9..2c0a19b05 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -138,7 +138,8 @@ public Optional getControllerServiceConfiguratio * @param dataflow the new dataflow (new state) * @param oldDataflow latest dataflow dataflow. */ - public void updateEngineContext(SparkContext sparkContext, EngineContext engineContext, DataFlow dataflow, DataFlow oldDataflow) { + public boolean updateEngineContext(SparkContext sparkContext, EngineContext engineContext, DataFlow dataflow, DataFlow oldDataflow) { + boolean changed = false; if (oldDataflow == null || oldDataflow.getLastModified().isBefore(dataflow.getLastModified())) { logger.info("We have a new configuration. Resetting current engine"); engineContext.getEngine().reset(engineContext); @@ -159,12 +160,13 @@ public void updateEngineContext(SparkContext sparkContext, EngineContext engineC engineContext.getStreamContexts().stream() .collect(Collectors.toMap(StreamContext::getIdentifier, StreamContext::getProcessContexts)) , sparkContext); - updatePipelines(sparkContext, dataflow); + updatePipelines(sparkContext, engineContext, dataflow); engineContext.getEngine().start(engineContext); } catch (Exception e) { logger.error("Unable to start engine. Logisland state may be inconsistent. Trying to recover. Caused by", e); engineContext.getEngine().reset(engineContext); } + changed = true; } else { //need to update pipelines? if (dataflow.getStreams().stream() @@ -174,25 +176,31 @@ public void updateEngineContext(SparkContext sparkContext, EngineContext engineC return old.isPresent() && old.get() != null && old.get().getPipeline().getLastModified().isBefore(s.getPipeline().getLastModified()); })) { - updatePipelines(sparkContext, dataflow); + updatePipelines(sparkContext, engineContext, dataflow); + changed = true; } } - + return changed; } /** * Update pipelines. * - * @param sparkContext the spark context - * @param dataflow the dataflow + * @param sparkContext the spark context + * @param engineContext the engine context. + * @param dataflow the dataflow */ - public void updatePipelines(SparkContext sparkContext, DataFlow dataflow) { + public void updatePipelines(SparkContext sparkContext, EngineContext engineContext, DataFlow dataflow) { Map> pipelineMap = dataflow.getStreams().stream() .collect(Collectors.toMap(Stream::getName, s -> s.getPipeline().getProcessors().stream().map(this::getProcessContext) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toList()))); + engineContext.getStreamContexts().forEach(streamContext -> { + streamContext.getProcessContexts().clear(); + streamContext.getProcessContexts().addAll(pipelineMap.get(streamContext.getIdentifier())); + }); PipelineConfigurationBroadcastWrapper.getInstance().refresh(pipelineMap, sparkContext); } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala index a9f33ee47..c8df54977 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -25,15 +25,12 @@ import java.util.concurrent.{Executors, TimeUnit} import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext import com.hurence.logisland.engine.spark.remote.model.DataFlow -import com.hurence.logisland.engine.spark.remote.{PipelineConfigurationBroadcastWrapper, RemoteApiClient, RemoteApiComponentFactory} -import com.hurence.logisland.processor.ProcessContext -import com.hurence.logisland.stream.{StandardStreamContext, StreamContext} +import com.hurence.logisland.engine.spark.remote.{RemoteApiClient, RemoteApiComponentFactory} +import com.hurence.logisland.stream.StandardStreamContext import com.hurence.logisland.stream.spark.DummyRecordStream import com.hurence.logisland.validator.StandardValidators import org.slf4j.LoggerFactory -import scala.collection.JavaConverters - object RemoteApiStreamProcessingEngine { val REMOTE_API_BASE_URL = new PropertyDescriptor.Builder() .name("remote.api.baseUrl") @@ -116,7 +113,6 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { } - if (!initialized) { initialized = true val remoteApiClient = new RemoteApiClient(new RemoteApiClient.ConnectionSettings( @@ -137,7 +133,6 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { executor.scheduleWithFixedDelay(new Runnable { val state = new RemoteApiClient.State - var i = 0 override def run(): Unit = { try { @@ -147,11 +142,14 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { if (currentDataflow != null && currentDataflow.getLastModified != null) { lastUpdated = currentDataflow.getLastModified.toInstant } - remoteApiComponentFactory.updateEngineContext(getCurrentSparkContext(), engineContext, dataflow.get, currentDataflow) - - - - currentDataflow = dataflow.get() + if (remoteApiComponentFactory.updateEngineContext(getCurrentSparkContext(), engineContext, dataflow.get, currentDataflow)) { + currentDataflow = dataflow.get() + try { + remoteApiClient.pushDataFlow(appName, currentDataflow); + } catch { + case default: Throwable => logger.warn("Unexpected exception while trying to push configuration to remote server", default) + } + } } } catch { case default: Throwable => logger.warn("Unexpected exception while trying to poll for new dataflow configuration", default) @@ -160,10 +158,21 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { }, 0, engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE).toInt, TimeUnit.MILLISECONDS ) + executor.scheduleWithFixedDelay(new Runnable { - } + override def run(): Unit = { + try { + remoteApiClient.pushDataFlow(appName, currentDataflow) + } catch { + case default: Throwable => logger.warn("Unexpected exception while trying to push configuration to remote server", default) + } + } + }, 0, engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_CONFIG_PUSH_RATE).toInt, TimeUnit.MILLISECONDS + ) + } + super.start(engineContext) } diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/_static/api.yaml b/logisland-framework/logisland-resources/src/main/resources/docs/_static/api.yaml new file mode 100644 index 000000000..333a8d1d7 --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/docs/_static/api.yaml @@ -0,0 +1,328 @@ +swagger: '2.0' +info: + version: v1 + title: Logisland standard API + description: >- + **The Logisland REST API for third party applications.** + + + The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to: + + - Ask for configuration changes to be triggered. + + - Report the latest configuration applied (to ease up resynchronization and business continuity). + + + *As a general rule, the changes will be triggered if the **lastUpdated** field of the object you are going to modify is fresher than the one known by logisland.* + + + In terms of API, two degrees of freedom are possible: + + - **Dataflow**: + + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). + + Act at dataflow level if you want to: + + - Add/Remove any streaming endpoint + - Change any active stream configuration (e.g. kafka topic) + - Create/Remote/Modify any service + + + - **Pipeline**: + + A pipeline is a processing chain acting on a data flowing point-to-point. + + The api gives you the possibility to have a finer-grained control of what is going of any stream pipeline without perturbing the stream itself. + This means that the processor chain will be dynamically reconfigured without the need of stopping the stream and reconfigure the whole dataflow. + + Act at pipeline level if you want to: + + - Add/Remove processors in the pipeline + + - Change any processor configuration + + *Please note that if you need to add/remove controller services you must act your changes at dataflow level.* + + contact: + name: Hurence + email: support@hurence.com +host: localhost:8081 +schemes: + - http + - https +consumes: + - application/json +produces: + - application/json +paths: + /dataflows/{dataflowName}: + parameters: + - name: dataflowName + in: path + type: string + required: true + description: the dataflow name (aka the logisland job name) + get: + tags: + - dataflow + operationId: pollDataflowConfiguration + summary: Retrieves the configuration for a specified dataflow + description: >- + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). + + Logisland will call this endpoint to know which configuration should be run. + + This endpoint also supports HTTP caching (Last-Updated, If-Modified-Since) as per RFC 7232, section 3.3 + parameters: + - name: If-Modified-Since + in: header + type: string + description: Timestamp of last response + required: false + + responses: + "200": + description: >- + Return the dataflow configuration. + + On logisland side, the following will happen: + + - At dataflow level: + + - Fully reconfigure a dataflow (stop and then start) if nothing is running (initial state) or if lastUpdated is fresher than the one of the already running dataflow. + + In this case be aware that old stream and services will be destroyed and + new ones will be created. + + - Do nothing otherwise (keep running the active dataflow) + + - At pipeline level: + + - The processor chain will be fully reconfigured if and only if the pipeline lastUpdated is fresher than the lastUpdated known by the system. + + In any case the stream is never stopped. + + headers: + Last-Updated: + type: string + description: Should be used for subsequent requests as If-Modified-Since request header. + schema: + $ref: '#/definitions/DataFlow' + examples: + A single stream dataflow: + lastModified: '1983-06-04T10:00.000Z' + modificationReason: Index Apache Logs again + services: + - component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService + documentation: elasticsearch service to sink records + name: elasticsearch_service + config: + - key: hosts + value: eshost:9300 + - key: cluster.name + value: escluster + streams: + - name: kafka_in + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + config: + - key: kafka.input.topics + value: logisland_raw + - key: kafka.output.topics + value: logisland_events + - key: kafka.error.topics + value: logisland_errors + - key: kafka.input.topics.serializer + value: none + - key: kafka.output.topics.serializer + value: com.hurence.logisland.serializer.KryoSerializer + - key: kafka.error.topics.serializer + value: com.hurence.logisland.serializer.JsonSerializer + - key: kafka.metadata.broker.list + value: sandbox:9092 + - key: kafka.zookeeper.quorum + value: sandbox:2181 + - key: kafka.topic.autoCreate + value: 'true' + - key: kafka.topic.default.partitions + value: '4' + - key: kafka.topic.default.replicationFactor + value: '1' + pipeline: + lastModified: '1983-06-04T10:00.000Z' + modificationReason: Initial configuration + processors: + - component: com.hurence.logisland.processor.SplitText + name: apache_parser + documentation: parse apache logs with a regexp + config: + - key: record.type + value: apache_log + - key: value.regex + value: (\S+)\s+(\S+)\s+(\S+)\s+\[([\w:\/]+\s[+\-]\d{4})\]\s+"(\S+)\s+(\S+)\s*(\S*)"\s+(\S+)\s+(\S+) + - key: value.fields + value: src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out + - component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch + documentation: a processor that indexes processed events in elasticsearch + name: es_publisher + config: + - key: elasticsearch.client.service + value: elasticsearch_service + - key: default.index + value: logisland + - key: default.type + value: event + - key: timebased.index + value: yesterday + - key: es.index.field + value: search_index + - key: es.type.field + value: record_type + + "304": + description: | + Nothing has been modified since the last call. + + In this case the body content will be completely ignored + (hence the server can answer with an empty body to save network and resources). + + "404": + description: Not found (the server probably does not handle this dataflow) + default : + description: Unexpected error + post: + tags: + - dataflow + operationId: notifyDataflowConfiguration + summary: Push the configuration of running dataflows. + description: >- + In order to ensure business continuity, Logisland will contact the third party application in order to push a snapshot of the current configuration. + + The endpoint will be called: + + - On a regular basis (according to logisland configuration). + + - Each time the a dataflow or a pipeline configuration change has been applied. + + + This service can be seen as well as a liveness ping. + parameters: + - name: jobId + in: path + type: string + required: true + description: logisland job id (aka the engine name) + - in: body + name: dataflow + required: true + schema: + $ref: '#/definitions/DataFlow' + responses: + default : + description: | + The server should return HTTP 200 OK. + By the way, the response is ignored by Logisland since the operation + has a *fire and forget* nature. + + +definitions: + + Property: + type: object + required: + - key + - value + properties: + key: + type: string + type: + type: string + default: "string" + value: + type: string + + Component: + type: object + required: + - component + - name + properties: + name: + type: string + component: + type: string + documentation: + type: string + config: + type: array + items: + $ref: '#/definitions/Property' + + Service: + type: object + description: A logisland 'controller service'. + allOf: + - $ref: '#/definitions/Component' + + + Processor: + type: object + description: A logisland 'processor'. + allOf: + - $ref: '#/definitions/Component' + + Versioned: + type: object + description: a versioned component + properties: + lastModified: + type: string + format: date-time + description: the last modified timestamp of this pipeline (used to trigger changes). + modificationReason: + type: string + description: Can be used to document latest changeset. + required: + - lastModified + + Pipeline: + type: object + description: Tracks stream processing pipeline configuration + allOf: + - $ref: '#/definitions/Versioned' + - properties: + processors: + type: array + items: + $ref: '#/definitions/Processor' + + Stream: + type: object + allOf: + - $ref: '#/definitions/Component' + - properties: + pipeline: + $ref: '#/definitions/Pipeline' + required: + - pipeline + + + + + DataFlow: + type: object + description: A streaming pipeline. + allOf: + - $ref: "#/definitions/Versioned" + - properties: + services: + type: array + description: The service controllers. + items: + $ref: '#/definitions/Service' + streams: + type: array + description: The engine properties. + items: + $ref: '#/definitions/Stream' + diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/_static/logisland_api_flows.png b/logisland-framework/logisland-resources/src/main/resources/docs/_static/logisland_api_flows.png new file mode 100644 index 0000000000000000000000000000000000000000..713d5eb61e917d36e1f7b91532fdf1ed5236fbdb GIT binary patch literal 36603 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 ZJu<=f-?)@L2#I+{A!tls)3Nip{{uE_<7WT> literal 0 HcmV?d00001 diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/index.rst b/logisland-framework/logisland-resources/src/main/resources/docs/index.rst index 790ad2863..738798892 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/index.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/index.rst @@ -28,6 +28,7 @@ Contents: developer tutorials/index api + rest-api components changes faq diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/rest-api.rst b/logisland-framework/logisland-resources/src/main/resources/docs/rest-api.rst new file mode 100644 index 000000000..0f389f32b --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/docs/rest-api.rst @@ -0,0 +1,689 @@ +Logisland REST API +================== + +**The Logisland REST API for third party applications.** + + +.. toctree:: +:maxdepth: 3 + +------------ +Introduction +------------ +Logisland makes available a standard RESTful API definition to interoperate with any third party application implementing it. + +The API should be implemented by a third party application and logisland will regularly poll this endpoint in order to: + +- Ask for configuration changes to be triggered. +- Report the latest configuration applied (to ease up resynchronization and business continuity). + + +Both flows can hence be resumed by the following sequence diagram: + +.. image:: /_static/logisland_api_flows.png + + + +------------ +Usage +------------ + +In terms of API, two degrees of freedom are possible: + +- **Dataflow**: + + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed and reach one or more destinations (sinks). + + Act at dataflow level if you want to: + + - Add/Remove any streaming endpoint + - Change any active stream configuration (e.g. kafka topic) + - Create/Remote/Modify any service + + +- **Pipeline**: + + A pipeline is a processing chain acting on a data flowing point-to-point. + + The api gives you the possibility to have a finer-grained control of what is going of any stream pipeline without perturbing the stream itself. + This means that the processor chain will be dynamically reconfigured without the need of stopping the stream and reconfigure the whole dataflow. + + Act at pipeline level if you want to: + + - Add/Remove processors in the pipeline + + - Change any processor configuration + +.. hint:: As a general rule, the changes will be triggered if the *lastUpdated* field of the object you are going to modify is fresher than the one known by logisland. + + +----------------- +API Specification +----------------- + +This section resumes the Rest API specification. More details are available on the `swagger spec `_. + +========== +Operations +========== + +GET ``/dataflows/{dataflowName}`` +--------------------------------- + + +Summary ++++++++ + +Retrieves the configuration for a specified dataflow + +Description ++++++++++++ + +.. raw:: html + + A dataflow is a set of services and streams allowing a data flowing from one or more sources, being transformed by a pipeline and reach one or more destinations (sinks). +Logisland will call this endpoint to know which configuration should be run. + + This endpoint also supports HTTP caching (Last-Updated, If-Modified-Since) as per RFC 7232, section 3.3 + +Parameters +++++++++++ + +.. csv-table:: +:delim: | + :header: "Name", "Located in", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 15, 10, 10, 10, 20, 30 + + dataflowName | path | Yes | string | | | the dataflow name (aka the logisland job name) + + +Request ++++++++ + + +Headers +^^^^^^^ + +.. code-block:: javascript + + If-Modified-Since: Timestamp of last response + + +Responses ++++++++++ + +**200** +^^^^^^^ + +Return the dataflow configuration. +On logisland side, the following will happen: +- At dataflow level: + + - Fully reconfigure a dataflow (stop and then start) if nothing is running (initial state) or if lastUpdated is fresher than the one of the already running dataflow. + + In this case be aware that old stream and services will be destroyed and + new ones will be created. + + - Do nothing otherwise (keep running the active dataflow) + +- At pipeline level: + + - The processor chain will be fully reconfigured if and only if the pipeline lastUpdated is fresher than the lastUpdated known by the system. + + In any case the stream is never stopped. + + +Type: :ref:`Versioned ` extended :ref:`inline ` + +**Example:** + +.. code-block:: javascript + + { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "services": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ], + "streams": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + } + ] + } + +**304** +^^^^^^^ + +Nothing has been modified since the last call. + +In this case the body content will be completely ignored +(hence the server can answer with an empty body to save network and resources). + + + +**404** +^^^^^^^ + +Not found (the server probably does not handle this dataflow) + + +**default** +^^^^^^^^^^^ + +Unexpected error + + + +POST ``/dataflows/{dataflowName}`` +---------------------------------- + + +Summary ++++++++ + +Push the configuration of running dataflows. + +Description ++++++++++++ + +.. raw:: html + + In order to ensure business continuity, Logisland will contact the third party application in order to push a snapshot of the current configuration. +The endpoint will be called: +- On a regular basis (according to logisland configuration). +- Each time the a dataflow or a pipeline configuration change has been applied. + +This service can be seen as well as a liveness ping. + +Parameters +++++++++++ + +.. csv-table:: +:delim: | + :header: "Name", "Located in", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 15, 10, 10, 10, 20, 30 + + jobId | path | Yes | string | | | logisland job id (aka the engine name) + dataflowName | path | Yes | string | | | the dataflow name (aka the logisland job name) + + +Request ++++++++ + + + +.. _d_68b618b2088b15f9f9f912df4be811df: + +Body +^^^^ + +A streaming pipeline. + +:ref:`Versioned ` extended :ref:`inline ` + +.. _i_ae1816015667b75409cb3251ba13c032: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + services | No | array of :ref:`Component ` | | | The service controllers. + streams | No | array of :ref:`Component ` extended :ref:`inline ` | | | The engine properties. + +.. code-block:: javascript + + { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "services": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ], + "streams": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring", + "pipeline": { + "lastModified": "2015-01-01T15:00:00.000Z", + "modificationReason": "somestring", + "processors": [ + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + }, + { + "component": "somestring", + "config": [ + { + "key": "somestring", + "type": "string", + "value": "somestring" + }, + { + "key": "somestring", + "type": "string", + "value": "somestring" + } + ], + "documentation": "somestring", + "name": "somestring" + } + ] + } + } + ] + } + +Responses ++++++++++ + +**default** +^^^^^^^^^^^ + +The server should return HTTP 200 OK. +By the way, the response is ignored by Logisland since the operation +has a *fire and forget* nature. + + +=============== +Data Structures +=============== + +.. _d_3c2b4cd64485b5f73be7a1facba6ed8c: + +Component Model Structure +------------------------- + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + component | Yes | string | | | + config | No | array of :ref:`Property ` | | | + documentation | No | string | | | + name | Yes | string | | | + +.. _d_68b618b2088b15f9f9f912df4be811df: + +DataFlow Model Structure +------------------------ + +A streaming pipeline. + +:ref:`Versioned ` extended :ref:`inline ` + +.. _i_ae1816015667b75409cb3251ba13c032: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + services | No | array of :ref:`Component ` | | | The service controllers. + streams | No | array of :ref:`Component ` extended :ref:`inline ` | | | The engine properties. + +.. _d_0752e439d11d3f0d4f6b437e63ea7248: + +Pipeline Model Structure +------------------------ + +Tracks stream processing pipeline configuration + +:ref:`Versioned ` extended :ref:`inline ` + +.. _i_f3879f767282c180c5b651f138c40b05: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + processors | No | array of :ref:`Component ` | | | + +.. _d_865032b24aeb47b5fd3a07f7e49d88fd: + +Processor Model Structure +------------------------- + +A logisland 'processor'. + +:ref:`Component ` + +.. _d_28dca67a05e18e9f96317b5bef61d056: + +Property Model Structure +------------------------ + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + key | Yes | string | | | + type | No | string | | {'default': 'string'} | + value | Yes | string | | | + +.. _d_35858dd5b9e97d51acc7f109ceb3deb0: + +Service Model Structure +----------------------- + +A logisland 'controller service'. + +:ref:`Component ` + +.. _d_ab44feb101835c4602a49f15a25615a8: + +Stream Model Structure +---------------------- + +:ref:`Component ` extended :ref:`inline ` + +.. _i_09545770fbf157c057309e15e402b2f4: + +**Inline schema:** + + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + component | Yes | string | | | + config | No | array of :ref:`Property ` | | | + documentation | No | string | | | + name | Yes | string | | | + pipeline | No | :ref:`Versioned ` extended :ref:`inline ` | | | + +.. _d_bcefda54d79a3bedfa83231aed8d38b1: + +Versioned Model Structure +------------------------- + +a versioned component + +.. csv-table:: +:delim: | + :header: "Name", "Required", "Type", "Format", "Properties", "Description" + :widths: 20, 10, 15, 15, 30, 25 + + lastModified | Yes | string | date-time | | the last modified timestamp of this pipeline (used to trigger changes). + modificationReason | No | string | | | Can be used to document latest changeset. + From b92799f6f9b7869b1fbe0bb8d3793830f09b2009 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 4 Jun 2018 12:08:38 +0200 Subject: [PATCH 24/63] Only update modified pipelines --- .../remote/RemoteApiComponentFactory.java | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java index 2c0a19b05..9d75985cb 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -29,9 +29,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -160,7 +159,7 @@ public boolean updateEngineContext(SparkContext sparkContext, EngineContext engi engineContext.getStreamContexts().stream() .collect(Collectors.toMap(StreamContext::getIdentifier, StreamContext::getProcessContexts)) , sparkContext); - updatePipelines(sparkContext, engineContext, dataflow); + updatePipelines(sparkContext, engineContext, dataflow.getStreams()); engineContext.getEngine().start(engineContext); } catch (Exception e) { logger.error("Unable to start engine. Logisland state may be inconsistent. Trying to recover. Caused by", e); @@ -169,29 +168,39 @@ public boolean updateEngineContext(SparkContext sparkContext, EngineContext engi changed = true; } else { //need to update pipelines? - if (dataflow.getStreams().stream() - .anyMatch(s -> { - Optional old = oldDataflow.getStreams().stream() - .filter(t -> t.getName().equals(s.getName())).findFirst(); - return old.isPresent() && old.get() != null && - old.get().getPipeline().getLastModified().isBefore(s.getPipeline().getLastModified()); - })) { - updatePipelines(sparkContext, engineContext, dataflow); - changed = true; + + + Map streamMap = dataflow.getStreams().stream().collect(Collectors.toMap(Stream::getName, Function.identity())); + + List mergedStreamList = new ArrayList<>(); + for (Stream oldStream : oldDataflow.getStreams()) { + Stream newStream = streamMap.get(oldStream.getName()); + if (newStream != null && oldStream.getPipeline().getLastModified().isBefore(newStream.getPipeline().getLastModified())) { + changed = true; + logger.info("Detected change for pipeline {}", newStream.getName()); + mergedStreamList.add(newStream); + } else { + mergedStreamList.add(oldStream); + } + } + if (changed) { + updatePipelines(sparkContext, engineContext, mergedStreamList); } + } return changed; } + /** * Update pipelines. * * @param sparkContext the spark context * @param engineContext the engine context. - * @param dataflow the dataflow + * @param streams the list of streams */ - public void updatePipelines(SparkContext sparkContext, EngineContext engineContext, DataFlow dataflow) { - Map> pipelineMap = dataflow.getStreams().stream() + public void updatePipelines(SparkContext sparkContext, EngineContext engineContext, Collection streams) { + Map> pipelineMap = streams.stream() .collect(Collectors.toMap(Stream::getName, s -> s.getPipeline().getProcessors().stream().map(this::getProcessContext) .filter(Optional::isPresent) @@ -204,4 +213,5 @@ public void updatePipelines(SparkContext sparkContext, EngineContext engineConte PipelineConfigurationBroadcastWrapper.getInstance().refresh(pipelineMap, sparkContext); } + } \ No newline at end of file From d65fc699e4f5b171bb7baf3879e4853f6467cab3 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 4 Jun 2018 13:50:37 +0200 Subject: [PATCH 25/63] Fix ES jackson deps --- .../logisland-elasticsearch_2_4_0-client-service/pom.xml | 1 - .../logisland-elasticsearch_5_4_0-client-service/pom.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml b/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml index 2f45d6e60..7898911a5 100644 --- a/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml +++ b/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml @@ -67,7 +67,6 @@ com.fasterxml.jackson.core jackson-core - 2.6.6 diff --git a/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml b/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml index 9e60bb364..3c220864e 100644 --- a/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml +++ b/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml @@ -82,7 +82,6 @@ com.fasterxml.jackson.core jackson-core - 2.6.6 From e401f386bbb9a842a617ec57485523c49140fe45 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 5 Jun 2018 11:33:40 +0200 Subject: [PATCH 26/63] Correct dependcies to cope with spark 2.1.x Ensure dataflow is cleaned in case of bad request Enforce logisland component validations before starting the dataflow. --- logisland-documentation/pom.xml | 6 +- .../logisland-spark_2_1-engine/pom.xml | 4 +- .../engine/spark/remote/RemoteApiClient.java | 8 +- .../remote/RemoteApiComponentFactory.java | 129 +++++++++++------- .../util/spark/ProcessorMetrics.java | 2 +- .../spark/KafkaStreamProcessingEngine.scala | 4 +- .../RemoteApiStreamProcessingEngine.scala | 39 +++--- .../spark/AbstractKafkaRecordStream.scala | 1 - ...KafkaStructuredStreamProviderService.scala | 1 + .../StructuredStreamProviderService.scala | 4 + pom.xml | 46 ++++++- 11 files changed, 164 insertions(+), 80 deletions(-) diff --git a/logisland-documentation/pom.xml b/logisland-documentation/pom.xml index b03a9286c..acba32626 100644 --- a/logisland-documentation/pom.xml +++ b/logisland-documentation/pom.xml @@ -117,6 +117,11 @@ com.hurence.logisland logisland-redis_4-client-service + + com.hurence.logisland + logisland-connector-opcda + + @@ -136,7 +141,6 @@ also adding the project build directory --> com.hurence.logisland.documentation.DocGenerator - diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index 22f1c9187..c1ae2761e 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -149,9 +149,9 @@ http://www.w3.org/2001/XMLSchema-instance "> - org.hibernate.validator + org.hibernate hibernate-validator - 6.0.10.Final + 5.1.3.Final diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index 5eab152d3..63fdcd1c9 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -227,9 +227,11 @@ public void pushDataFlow(String dataflowName, DataFlow dataFlow) { .addPathSegment(DATAFLOW_RESOURCE_URI).addPathSegment(dataflowName) .build()) .addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) - .post(RequestBody.create( - okhttp3.MediaType.parse(MediaType.APPLICATION_JSON), - mapper.writeValueAsString(dataFlow))) + .post(dataFlow != null ? + RequestBody.create(okhttp3.MediaType.parse(MediaType.APPLICATION_JSON), + + mapper.writeValueAsString(dataFlow)) : + RequestBody.create(null, new byte[0])) .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java index 9d75985cb..c40e52b65 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -17,7 +17,12 @@ package com.hurence.logisland.engine.spark.remote; +import com.hurence.logisland.component.ConfigurableComponent; +import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.config.ControllerServiceConfiguration; +import com.hurence.logisland.controller.ControllerService; +import com.hurence.logisland.controller.ControllerServiceInitializationContext; +import com.hurence.logisland.controller.StandardControllerServiceContext; import com.hurence.logisland.engine.EngineContext; import com.hurence.logisland.engine.spark.remote.model.*; import com.hurence.logisland.processor.ProcessContext; @@ -29,7 +34,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -50,7 +58,7 @@ public class RemoteApiComponentFactory { * @param stream * @return */ - public Optional getStreamContext(Stream stream) { + public StreamContext getStreamContext(Stream stream) { try { final RecordStream recordStream = (RecordStream) Class.forName(stream.getComponent()).newInstance(); @@ -58,24 +66,24 @@ public Optional getStreamContext(Stream stream) { new StandardStreamContext(recordStream, stream.getName()); // instantiate each related processor - stream.getPipeline().getProcessors().forEach(processor -> { - Optional processorContext = getProcessContext(processor); - if (processorContext.isPresent()) - instance.addProcessContext(processorContext.get()); - }); + stream.getPipeline().getProcessors().stream() + .map(this::getProcessContext) + .forEach(instance::addProcessContext); // set the config properties - stream.getConfig().forEach(e -> instance.setProperty(e.getKey(), e.getValue())); - + configureComponent(recordStream, stream.getConfig()) + .forEach((k, s) -> instance.setProperty(k, s)); + if (!instance.isValid()) { + throw new IllegalArgumentException("Stream is not valid"); + } logger.info("created stream {}", stream.getName()); - return Optional.of(instance); + return instance; } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instantiate stream " + stream.getName(), e); + throw new RuntimeException("unable to instantiate stream " + stream.getName(), e); } - return Optional.empty(); } /** @@ -84,7 +92,7 @@ public Optional getStreamContext(Stream stream) { * @param processor the processor bean. * @return optionally the constructed processor context or nothing in case of error. */ - public Optional getProcessContext(Processor processor) { + public ProcessContext getProcessContext(Processor processor) { try { final com.hurence.logisland.processor.Processor processorInstance = (com.hurence.logisland.processor.Processor) Class.forName(processor.getComponent()).newInstance(); @@ -92,15 +100,21 @@ public Optional getProcessContext(Processor processor) { new StandardProcessContext(processorInstance, processor.getName()); // set all properties - processor.getConfig().forEach(e -> processContext.setProperty(e.getKey(), e.getValue())); + configureComponent(processorInstance, processor.getConfig()) + .forEach((k, s) -> processContext.setProperty(k, s)); + ; + + if (!processContext.isValid()) { + throw new IllegalArgumentException("Processor is not valid"); + } + logger.info("created processor {}", processor); - return Optional.of(processContext); + return processContext; } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - logger.error("unable to instantiate processor " + processor.getComponent(), e); + throw new RuntimeException("unable to instantiate processor " + processor.getName(), e); } - return Optional.empty(); } @@ -110,23 +124,27 @@ public Optional getProcessContext(Processor processor) { * @param service the service bean. * @return optionally the constructed service configuration or nothing in case of error. */ - public Optional getControllerServiceConfiguration(Service service) { + public ControllerServiceConfiguration getControllerServiceConfiguration(Service service) { try { + ControllerService cs = (ControllerService) Class.forName(service.getComponent()).newInstance(); ControllerServiceConfiguration configuration = new ControllerServiceConfiguration(); configuration.setControllerService(service.getName()); configuration.setComponent(service.getComponent()); configuration.setDocumentation(service.getDocumentation()); configuration.setType("service"); - configuration.setConfiguration(service.getConfig().stream() - .collect(Collectors.toMap(Property::getKey, Property::getValue))); - + configuration.setConfiguration(configureComponent(cs, service.getConfig())); + ControllerServiceInitializationContext ic = new StandardControllerServiceContext(cs, service.getName()); + configuration.getConfiguration().forEach((k, s) -> ic.setProperty(k, s)); + if (!ic.isValid()) { + throw new IllegalArgumentException("Service is not valid"); + } logger.info("created service {}", service.getName()); - return Optional.of(configuration); + return configuration; } catch (Exception e) { - logger.error("unable to configure service " + service.getComponent(), e); + throw new RuntimeException("unable to instantiate service " + service.getName(), e); } - return Optional.empty(); + } /** @@ -141,35 +159,39 @@ public boolean updateEngineContext(SparkContext sparkContext, EngineContext engi boolean changed = false; if (oldDataflow == null || oldDataflow.getLastModified().isBefore(dataflow.getLastModified())) { logger.info("We have a new configuration. Resetting current engine"); - engineContext.getEngine().reset(engineContext); logger.info("Configuring dataflow. Last change at {} is {}", dataflow.getLastModified(), dataflow.getModificationReason()); - dataflow.getServices().stream() + + + List css = dataflow.getServices().stream() .map(this::getControllerServiceConfiguration) - .filter(Optional::isPresent) - .map(Optional::get) - .forEach(engineContext::addControllerServiceConfiguration); - dataflow.getStreams().stream() + .collect(Collectors.toList()); + + List sc = dataflow.getStreams().stream() .map(this::getStreamContext) - .filter(Optional::isPresent) - .map(Optional::get) - .forEach(engineContext::addStreamContext); + .collect(Collectors.toList()); + + sc.forEach(streamContext -> { + if (!streamContext.isValid()) { + throw new IllegalArgumentException("Unable to validate steam " + streamContext.getIdentifier()); + } + }); + logger.info("Restarting engine"); - try { - PipelineConfigurationBroadcastWrapper.getInstance().refresh( - engineContext.getStreamContexts().stream() - .collect(Collectors.toMap(StreamContext::getIdentifier, StreamContext::getProcessContexts)) - , sparkContext); - updatePipelines(sparkContext, engineContext, dataflow.getStreams()); - engineContext.getEngine().start(engineContext); - } catch (Exception e) { - logger.error("Unable to start engine. Logisland state may be inconsistent. Trying to recover. Caused by", e); - engineContext.getEngine().reset(engineContext); - } + engineContext.getEngine().reset(engineContext); + css.forEach(engineContext::addControllerServiceConfiguration); + sc.forEach(engineContext::addStreamContext); + + PipelineConfigurationBroadcastWrapper.getInstance().refresh( + engineContext.getStreamContexts().stream() + .collect(Collectors.toMap(StreamContext::getIdentifier, StreamContext::getProcessContexts)) + , sparkContext); + updatePipelines(sparkContext, engineContext, dataflow.getStreams()); + engineContext.getEngine().start(engineContext); changed = true; + } else { //need to update pipelines? - Map streamMap = dataflow.getStreams().stream().collect(Collectors.toMap(Stream::getName, Function.identity())); List mergedStreamList = new ArrayList<>(); @@ -203,15 +225,28 @@ public void updatePipelines(SparkContext sparkContext, EngineContext engineConte Map> pipelineMap = streams.stream() .collect(Collectors.toMap(Stream::getName, s -> s.getPipeline().getProcessors().stream().map(this::getProcessContext) - .filter(Optional::isPresent) - .map(Optional::get) .collect(Collectors.toList()))); engineContext.getStreamContexts().forEach(streamContext -> { streamContext.getProcessContexts().clear(); streamContext.getProcessContexts().addAll(pipelineMap.get(streamContext.getIdentifier())); }); + PipelineConfigurationBroadcastWrapper.getInstance().refresh(pipelineMap, sparkContext); } + private Map configureComponent(ConfigurableComponent component, Collection properties) { + final Map propertyMap = properties.stream().collect(Collectors.toMap(Property::getKey, Function.identity())); + return component.getPropertyDescriptors().stream() + .filter(propertyDescriptor -> propertyMap.containsKey(propertyDescriptor.getName()) || + (propertyDescriptor.getDefaultValue() != null && propertyDescriptor.isRequired())) + .collect(Collectors.toMap(PropertyDescriptor::getName, propertyDescriptor -> { + String value = propertyDescriptor.getDefaultValue(); + if (propertyMap.containsKey(propertyDescriptor.getName())) { + value = propertyMap.get(propertyDescriptor.getName()).getValue(); + } + return value; + })); + } + } \ No newline at end of file diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java index febb64205..6bda08b77 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java @@ -76,7 +76,7 @@ public synchronized static void computeMetrics( UserMetricsSystem.gauge(metricPrefix + Names.INCOMING_RECORDS).set(incomingEvents.size()); UserMetricsSystem.gauge(metricPrefix + Names.OUTGOING_RECORDS).set(outgoingEvents.size()); - long errorCount = outgoingEvents.stream().filter(r -> r.hasField(FieldDictionary.RECORD_ERRORS)).count(); + long errorCount = outgoingEvents.stream().filter(r -> r != null && r.hasField(FieldDictionary.RECORD_ERRORS)).count(); UserMetricsSystem.gauge(metricPrefix + "errors").set(errorCount); if (outgoingEvents.size() != 0) { final List recordSizesInBytes = new ArrayList<>(); diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index 42b3fb2df..d809f2be0 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -433,7 +433,9 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { logger.info("starting Spark Engine") //val timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT).asInteger().intValue() val streamingContext = createStreamingContext(engineContext) - streamingContext.start() + if (!engineContext.getStreamContexts.isEmpty) { + streamingContext.start() + } } protected def getCurrentSparkStreamingContext(sparkContext: SparkContext): StreamingContext = { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala index c8df54977..0bfa3ee81 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -17,7 +17,7 @@ package com.hurence.logisland.engine.spark -import java.time.{Duration, Instant} +import java.time.Duration import java.util import java.util.Collections import java.util.concurrent.{Executors, TimeUnit} @@ -26,8 +26,6 @@ import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext import com.hurence.logisland.engine.spark.remote.model.DataFlow import com.hurence.logisland.engine.spark.remote.{RemoteApiClient, RemoteApiComponentFactory} -import com.hurence.logisland.stream.StandardStreamContext -import com.hurence.logisland.stream.spark.DummyRecordStream import com.hurence.logisland.validator.StandardValidators import org.slf4j.LoggerFactory @@ -107,11 +105,11 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { * @param engineContext */ override def start(engineContext: EngineContext): Unit = { - - if (engineContext.getStreamContexts.isEmpty) { - engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")) - } - + /* + if (engineContext.getStreamContexts.isEmpty) { + engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")) + } + */ if (!initialized) { initialized = true @@ -135,24 +133,29 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { val state = new RemoteApiClient.State override def run(): Unit = { + var changed = false try { val dataflow = remoteApiClient.fetchDataflow(appName, state) if (dataflow.isPresent) { - var lastUpdated: Instant = null - if (currentDataflow != null && currentDataflow.getLastModified != null) { - lastUpdated = currentDataflow.getLastModified.toInstant - } + changed = true if (remoteApiComponentFactory.updateEngineContext(getCurrentSparkContext(), engineContext, dataflow.get, currentDataflow)) { currentDataflow = dataflow.get() - try { - remoteApiClient.pushDataFlow(appName, currentDataflow); - } catch { - case default: Throwable => logger.warn("Unexpected exception while trying to push configuration to remote server", default) - } } } } catch { - case default: Throwable => logger.warn("Unexpected exception while trying to poll for new dataflow configuration", default) + case default: Throwable => { + currentDataflow = null + logger.warn("Unexpected exception while trying to poll for new dataflow configuration", default) + reset(engineContext) + } + } finally { + if (changed) { + try { + remoteApiClient.pushDataFlow(appName, currentDataflow); + } catch { + case default: Throwable => logger.warn("Unexpected exception while trying to push configuration to remote server", default) + } + } } } }, 0, engineContext.getProperty(RemoteApiStreamProcessingEngine.REMOTE_API_POLLING_RATE).toInt, TimeUnit.MILLISECONDS diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala index 6231767da..9c7ba28f4 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala @@ -188,7 +188,6 @@ abstract class AbstractKafkaRecordStream extends AbstractRecordStream with Spark .foreachRDD(rdd => { this.streamContext.getProcessContexts().clear(); - logger.info(s"Stream is ${this.streamContext.getIdentifier}") this.streamContext.getProcessContexts().addAll( PipelineConfigurationBroadcastWrapper.getInstance().get(this.streamContext.getIdentifier)) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala index 07d726bca..489c43cd6 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala @@ -149,6 +149,7 @@ class KafkaStructuredStreamProviderService() extends AbstractControllerService w .option("kafka.bootstrap.servers", brokerList) .option("subscribe", inputTopics.mkString(",")) .load() + .selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)") .as[(String, String)] .map(r => { new StandardRecord(inputTopics.head) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala index e5c9a6c6e..d148d179a 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala @@ -4,6 +4,7 @@ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.util import com.hurence.logisland.controller.ControllerService +import com.hurence.logisland.engine.spark.remote.PipelineConfigurationBroadcastWrapper import com.hurence.logisland.record._ import com.hurence.logisland.serializer.{RecordSerializer, SerializerProvider} import com.hurence.logisland.stream.StreamContext @@ -71,6 +72,9 @@ trait StructuredStreamProviderService extends ControllerService { val controllerServiceLookup = controllerServiceLookupSink.value.getControllerServiceLookup() + streamContext.getProcessContexts().clear(); + streamContext.getProcessContexts().addAll( + PipelineConfigurationBroadcastWrapper.getInstance().get(streamContext.getIdentifier)) /** * create serializers diff --git a/pom.xml b/pom.xml index 627a7394f..9d8debcd8 100644 --- a/pom.xml +++ b/pom.xml @@ -401,12 +401,46 @@ 1.3 + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.module + jackson-module-parameter-names + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + - com.fasterxml.jackson - jackson-bom + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 ${jackson.version} - import - pom com.googlecode.json-simple @@ -1199,7 +1233,7 @@ 0.10.0.1 2.11.8 2.11 - 2.9.0 + 2.6.6 2.5 @@ -1241,7 +1275,7 @@ 0.8.2.1 2.10.6 2.10 - 2.9.0 + 2.4.4 2.4 From 976e1154c7b553bf36cc2768c0d90ca399fa2fe8 Mon Sep 17 00:00:00 2001 From: oalam Date: Tue, 5 Jun 2018 12:58:20 +0200 Subject: [PATCH 27/63] first test of REDIS & alerting features --- .../src/main/java/Dummy.java | 19 +++++++++++++++++++ logisland-documentation/components.rst | 2 ++ .../src/main/resources/components.json | 4 ++-- .../src/main/resources/docs/components.rst | 2 ++ .../util/string/StringUtilsTest.java | 8 ++++---- .../alerting/CheckThresholdsTest.java | 2 +- .../processor/alerting/ComputeTagsTest.java | 4 ++-- 7 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java diff --git a/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java b/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java new file mode 100644 index 000000000..7f861832b --- /dev/null +++ b/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java @@ -0,0 +1,19 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +public class Dummy { +} diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index 7051eb643..5b6690a62 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -223,6 +223,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" + "alert.criticity", "from 0 to ...", "", "0", "", "" Dynamic Properties __________________ @@ -518,6 +519,7 @@ In the list below, the names of required properties appear in **bold**. Any othe :widths: 20,60,30,20,10,10 "**event.serializer**", "the way to serialize event", "Json serialization (serialize events as json blocs), String serialization (serialize events as toString() blocs)", "json", "", "" + "record.types", "comma separated list of record to include. all if empty", "", "", "", "" ---------- diff --git a/logisland-framework/logisland-agent/src/main/resources/components.json b/logisland-framework/logisland-agent/src/main/resources/components.json index 951b5507f..2edaeb70f 100644 --- a/logisland-framework/logisland-agent/src/main/resources/components.json +++ b/logisland-framework/logisland-agent/src/main/resources/components.json @@ -3,12 +3,12 @@ {"name":"ApplyRegexp","description":"This processor is used to create a new set of fields from one field (using regexp).","component":"com.hurence.logisland.processor.ApplyRegexp","type":"processor","tags":["parser","regex","log","record"],"properties":[{"name":"conflict.resolution.policy","isRequired":false,"description":"What to do when a field with the same name already exists ?","overwrite existing field":"if field already exist","keep only old field":"keep only old field","defaultValue":"keep_only_old_field","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"alternative regex & mapping","value":"another regex that could match","description":"This processor is used to create a new set of fields from one field (using regexp).","isExpressionLanguageSupported":true}]}, {"name":"BulkAddElasticsearch","description":"Indexes the content of a Record in Elasticsearch using elasticsearch's bulk processor","component":"com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.index","isRequired":true,"description":"The name of the index to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"default.type","isRequired":true,"description":"The type of this document (used by Elasticsearch for indexing and searching)","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.index","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.index.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"es.type.field","isRequired":false,"description":"the name of the event field containing es doc type => will override type value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"BulkPut","description":"Indexes the content of a Record in a Datastore using bulk processor","component":"com.hurence.logisland.processor.datastore.BulkPut","type":"processor","tags":["datastore","record","put","bulk"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"default.collection","isRequired":true,"description":"The name of the collection/index/table to insert into","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"timebased.collection","isRequired":true,"description":"do we add a date suffix","No date":"no date added to default index","Today's date":"today's date added to default index","yesterday's date":"yesterday's date added to default index","defaultValue":"no","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"date.format","isRequired":false,"description":"simple date format for date suffix. default : yyyy.MM.dd","defaultValue":"yyyy.MM.dd","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"collection.field","isRequired":false,"description":"the name of the event field containing es index name => will override index value if set","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, -{"name":"CheckAlerts","description":"Add one or more field with a default value","component":"com.hurence.logisland.processor.alerting.CheckAlerts","type":"processor","tags":["record","alerting","thresholds","opc","tag"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"profile.activation.condition","isRequired":false,"description":"A javascript expression that activates this alerting profile when true","defaultValue":"0==0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, +{"name":"CheckAlerts","description":"Add one or more field with a default value","component":"com.hurence.logisland.processor.alerting.CheckAlerts","type":"processor","tags":["record","alerting","thresholds","opc","tag"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"profile.activation.condition","isRequired":false,"description":"A javascript expression that activates this alerting profile when true","defaultValue":"0==0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"alert.criticity","isRequired":false,"description":"from 0 to ...","defaultValue":"0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, {"name":"CheckThresholds","description":"Compute threshold cross from given formulas.\n each dynamic property will return a new record according to the formula definition\n the record name will be set to the property name\n the record time will be set to the current timestamp","component":"com.hurence.logisland.processor.alerting.CheckThresholds","type":"processor","tags":["record","threshold","tag","alerting"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.ttl","isRequired":false,"description":"How long (in ms) do the record will remain in cache","defaultValue":"30000","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, {"name":"ComputeTags","description":"Compute tag cross from given formulas.\n- each dynamic property will return a new record according to the formula definition\n- the record name will be set to the property name\n- the record time will be set to the current timestamp\n\na threshold_cross has the following properties : count, sum, avg, time, duration, value","component":"com.hurence.logisland.processor.alerting.ComputeTags","type":"processor","tags":["record","fields","Add"],"properties":[{"name":"max.cpu.time","isRequired":false,"description":"maximum CPU time in milliseconds allowed for script execution.","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.memory","isRequired":false,"description":"maximum memory in Bytes which JS executor thread can allocate","defaultValue":"51200","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"allow.no.brace","isRequired":false,"description":"Force, to check if all blocks are enclosed with curly braces \"{}\".\n

\n Explanation: all loops (for, do-while, while, and if-else, and functions\n should use braces, because poison_pill() function will be inserted after\n each open brace \"{\", to ensure interruption checking. Otherwise simple\n code like:\n

\n    while(true) while(true) {\n      // do nothing\n    }\n  
\n or even:\n
\n    while(true)\n  
\n cause unbreakable loop, which force this sandbox to use {@link Thread#stop()}\n which make JVM unstable.\n

\n

\n Properly writen code (even in bad intention) like:\n

\n    while(true) { while(true) {\n      // do nothing\n    }}\n  
\n will be changed into:\n
\n    while(true) {poison_pill(); \n      while(true) {poison_pill();\n        // do nothing\n      }\n    }\n  
\n which finish nicely when interrupted.\n

\n For legacy code, this check can be turned off, but with no guarantee, the\n JS thread will gracefully finish when interrupted.\n

","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"max.prepared.statements","isRequired":false,"description":"The size of prepared statements LRU cache. Default 0 (disabled).\n

\n Each statements when {@link #setMaxCPUTime(long)} is set is prepared to\n quit itself when time exceeded. To execute only once this procedure per\n statement set this value.\n

\n

\n When {@link #setMaxCPUTime(long)} is set 0, this value is ignored.\n

","defaultValue":"30","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"datastore.cache.collection","isRequired":false,"description":"The collection where to find cached objects","defaultValue":"test","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the type of the output record","defaultValue":"event","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}],"dynamicProperties":[{"name":"field to add","value":"a default value","description":"Add a field to the record with the default value","isExpressionLanguageSupported":false}]}, {"name":"ConsolidateSession","description":"The ConsolidateSession processor is the Logisland entry point to get and process events from the Web Analytics.As an example here is an incoming event from the Web Analytics:\n\n\"fields\": [{ \"name\": \"timestamp\", \"type\": \"long\" },{ \"name\": \"remoteHost\", \"type\": \"string\"},{ \"name\": \"record_type\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"record_id\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"location\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"hitType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventCategory\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventAction\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"eventLabel\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"localPath\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"q\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"n\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"referer\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"viewportPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"viewportPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelWidth\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"screenPixelHeight\", \"type\": [\"null\", \"int\"], \"default\": null },{ \"name\": \"partyId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"sessionId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageViewId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_newSession\", \"type\": [\"null\", \"boolean\"],\"default\": null },{ \"name\": \"userAgentString\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pageType\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"UserId\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"B2Bunit\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"pointOfService\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"companyID\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"GroupCode\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"userRoles\", \"type\": [\"null\", \"string\"], \"default\": null },{ \"name\": \"is_PunchOut\", \"type\": [\"null\", \"string\"], \"default\": null }]The ConsolidateSession processor groups the records by sessions and compute the duration between now and the last received event. If the distance from the last event is beyond a given threshold (by default 30mn), then the session is considered closed.The ConsolidateSession is building an aggregated session object for each active session.This aggregated object includes: - The actual session duration. - A boolean representing wether the session is considered active or closed. Note: it is possible to ressurect a session if for instance an event arrives after a session has been marked closed. - User related infos: userId, B2Bunit code, groupCode, userRoles, companyId - First visited page: URL - Last visited page: URL The properties to configure the processor are: - sessionid.field: Property name containing the session identifier (default: sessionId). - timestamp.field: Property name containing the timestamp of the event (default: timestamp). - session.timeout: Timeframe of inactivity (in seconds) after which a session is considered closed (default: 30mn). - visitedpage.field: Property name containing the page visited by the customer (default: location). - fields.to.return: List of fields to return in the aggregated object. (default: N/A)","component":"com.hurence.logisland.processor.webAnalytics.ConsolidateSession","type":"processor","tags":["analytics","web","session"],"properties":[{"name":"debug","isRequired":false,"description":"Enable debug. If enabled, the original JSON string is embedded in the record_value field of the record.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"session.timeout","isRequired":false,"description":"session timeout in sec","defaultValue":"1800","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionid.field","isRequired":false,"description":"the name of the field containing the session id => will override default value if set","defaultValue":"sessionId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"timestamp.field","isRequired":false,"description":"the name of the field containing the timestamp => will override default value if set","defaultValue":"h2kTimestamp","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"visitedpage.field","isRequired":false,"description":"the name of the field containing the visited page => will override default value if set","defaultValue":"location","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"userid.field","isRequired":false,"description":"the name of the field containing the userId => will override default value if set","defaultValue":"userId","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"fields.to.return","isRequired":false,"description":"the list of fields to return","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the first visited page => will override default value if set","defaultValue":"firstVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastVisitedPage.out.field","isRequired":false,"description":"the name of the field containing the last visited page => will override default value if set","defaultValue":"lastVisitedPage","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"isSessionActive.out.field","isRequired":false,"description":"the name of the field stating whether the session is active or not => will override default value if set","defaultValue":"is_sessionActive","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionDuration.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"sessionDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"eventsCounter.out.field","isRequired":false,"description":"the name of the field containing the session duration => will override default value if set","defaultValue":"eventsCounter","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"firstEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the first event => will override default value if set","defaultValue":"firstEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"lastEventDateTime.out.field","isRequired":false,"description":"the name of the field containing the date of the last event => will override default value if set","defaultValue":"lastEventDateTime","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sessionInactivityDuration.out.field","isRequired":false,"description":"the name of the field containing the session inactivity duration => will override default value if set","defaultValue":"sessionInactivityDuration","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"ConvertFieldsType","description":"Converts a field value into the given type. does nothing if conversion is not possible","component":"com.hurence.logisland.processor.ConvertFieldsType","type":"processor","tags":["type","fields","update","convert"],"dynamicProperties":[{"name":"field","value":"the new type","description":"convert field value into new type","isExpressionLanguageSupported":true}]}, -{"name":"DebugStream","description":"This is a processor that logs incoming records","component":"com.hurence.logisland.processor.DebugStream","type":"processor","tags":["record","debug"],"properties":[{"name":"event.serializer","isRequired":true,"description":"the way to serialize event","Json serialization":"serialize events as json blocs","String serialization":"serialize events as toString() blocs","defaultValue":"json","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, +{"name":"DebugStream","description":"This is a processor that logs incoming records","component":"com.hurence.logisland.processor.DebugStream","type":"processor","tags":["record","debug"],"properties":[{"name":"event.serializer","isRequired":true,"description":"the way to serialize event","Json serialization":"serialize events as json blocs","String serialization":"serialize events as toString() blocs","defaultValue":"json","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.types","isRequired":false,"description":"comma separated list of record to include. all if empty","defaultValue":"","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"DetectOutliers","description":"Outlier Analysis: A Hybrid Approach\n\nIn order to function at scale, a two-phase approach is taken\n\nFor every data point\n\n- Detect outlier candidates using a robust estimator of variability (e.g. median absolute deviation) that uses distributional sketching (e.g. Q-trees)\n- Gather a biased sample (biased by recency)\n- Extremely deterministic in space and cheap in computation\n\nFor every outlier candidate\n\n- Use traditional, more computationally complex approaches to outlier analysis (e.g. Robust PCA) on the biased sample\n- Expensive computationally, but run infrequently\n\nThis becomes a data filter which can be attached to a timeseries data stream within a distributed computational framework (i.e. Storm, Spark, Flink, NiFi) to detect outliers.","component":"com.hurence.logisland.processor.DetectOutliers","type":"processor","tags":["analytic","outlier","record","iot","timeseries"],"properties":[{"name":"value.field","isRequired":true,"description":"the numeric field to get the value","defaultValue":"record_value","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"time.field","isRequired":true,"description":"the numeric field to get the value","defaultValue":"record_time","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"output.record.type","isRequired":false,"description":"the output type of the record","defaultValue":"alert_match","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rotation.policy.type","isRequired":true,"description":"...","by_amount":null,"by_time":null,"never":null,"defaultValue":"by_amount","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rotation.policy.amount","isRequired":true,"description":"...","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rotation.policy.unit","isRequired":true,"description":"...","milliseconds":null,"seconds":null,"hours":null,"days":null,"months":null,"years":null,"points":null,"defaultValue":"points","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"chunking.policy.type","isRequired":true,"description":"...","by_amount":null,"by_time":null,"never":null,"defaultValue":"by_amount","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"chunking.policy.amount","isRequired":true,"description":"...","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"chunking.policy.unit","isRequired":true,"description":"...","milliseconds":null,"seconds":null,"hours":null,"days":null,"months":null,"years":null,"points":null,"defaultValue":"points","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"sketchy.outlier.algorithm","isRequired":false,"description":"...","SKETCHY_MOVING_MAD":null,"defaultValue":"SKETCHY_MOVING_MAD","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"batch.outlier.algorithm","isRequired":false,"description":"...","RAD":null,"defaultValue":"RAD","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.min","isRequired":false,"description":"minimum value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.max","isRequired":false,"description":"maximum value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.mean","isRequired":false,"description":"mean value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"global.statistics.stddev","isRequired":false,"description":"standard deviation value","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.normal","isRequired":true,"description":"zscoreCutoffs level for normal outlier","defaultValue":"0.000000000000001","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.moderate","isRequired":true,"description":"zscoreCutoffs level for moderate outlier","defaultValue":"1.5","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.severe","isRequired":true,"description":"zscoreCutoffs level for severe outlier","defaultValue":"10.0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"zscore.cutoffs.notEnoughData","isRequired":false,"description":"zscoreCutoffs level for notEnoughData outlier","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"smooth","isRequired":false,"description":"do smoothing ?","defaultValue":"false","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"decay","isRequired":false,"description":"the decay","defaultValue":"0.1","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"min.amount.to.predict","isRequired":true,"description":"minAmountToPredict","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"min_zscore_percentile","isRequired":false,"description":"minZscorePercentile","defaultValue":"50.0","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"reservoir_size","isRequired":false,"description":"the size of points reservoir","defaultValue":"100","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.force.diff","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.lpenalty","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.min.records","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.spenalty","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"rpca.threshold","isRequired":false,"description":"No Description Provided.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, {"name":"EnrichRecords","description":"Enrich input records with content indexed in datastore using multiget queries.\nEach incoming record must be possibly enriched with information stored in datastore. \nThe plugin properties are :\n- es.index (String) : Name of the datastore index on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- record.key (String) : Name of the field in the input record containing the id to lookup document in elastic search. This field is mandatory.\n- es.key (String) : Name of the datastore key on which the multiget query will be performed. This field is mandatory.\n- includes (ArrayList) : List of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (ArrayList) : List of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds at least the input record plus potentially one or more fields coming from of one datastore document.","component":"com.hurence.logisland.processor.datastore.EnrichRecords","type":"processor","tags":["datastore","enricher"],"properties":[{"name":"datastore.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing datastore.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.key","isRequired":false,"description":"The name of field in the input record containing the document id to use in ES multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"includes.field","isRequired":false,"description":"The name of the ES fields to include in the record.","defaultValue":"*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"excludes.field","isRequired":false,"description":"The name of the ES fields to exclude.","defaultValue":"N/A","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"type.name","isRequired":false,"description":"The typle of record to look for","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"collection.name","isRequired":false,"description":"The name of the collection to look for","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true}]}, {"name":"EnrichRecordsElasticsearch","description":"Enrich input records with content indexed in elasticsearch using multiget queries.\nEach incoming record must be possibly enriched with information stored in elasticsearch. \nThe plugin properties are :\n- es.index (String) : Name of the elasticsearch index on which the multiget query will be performed. This field is mandatory and should not be empty, otherwise an error output record is sent for this specific incoming record.\n- record.key (String) : Name of the field in the input record containing the id to lookup document in elastic search. This field is mandatory.\n- es.key (String) : Name of the elasticsearch key on which the multiget query will be performed. This field is mandatory.\n- includes (ArrayList) : List of patterns to filter in (include) fields to retrieve. Supports wildcards. This field is not mandatory.\n- excludes (ArrayList) : List of patterns to filter out (exclude) fields to retrieve. Supports wildcards. This field is not mandatory.\n\nEach outcoming record holds at least the input record plus potentially one or more fields coming from of one elasticsearch document.","component":"com.hurence.logisland.processor.elasticsearch.EnrichRecordsElasticsearch","type":"processor","tags":["elasticsearch"],"properties":[{"name":"elasticsearch.client.service","isRequired":true,"description":"The instance of the Controller Service to use for accessing Elasticsearch.","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false},{"name":"record.key","isRequired":true,"description":"The name of field in the input record containing the document id to use in ES multiget query","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.index","isRequired":true,"description":"The name of the ES index to use in multiget query. ","defaultValue":null,"isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.type","isRequired":false,"description":"The name of the ES type to use in multiget query.","defaultValue":"default","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.includes.field","isRequired":false,"description":"The name of the ES fields to include in the record.","defaultValue":"*","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":true},{"name":"es.excludes.field","isRequired":false,"description":"The name of the ES fields to exclude.","defaultValue":"N/A","isDynamic":false,"isSensitive":false,"isExpressionLanguageSupported":false}]}, diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index 7051eb643..5b6690a62 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -223,6 +223,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" + "alert.criticity", "from 0 to ...", "", "0", "", "" Dynamic Properties __________________ @@ -518,6 +519,7 @@ In the list below, the names of required properties appear in **bold**. Any othe :widths: 20,60,30,20,10,10 "**event.serializer**", "the way to serialize event", "Json serialization (serialize events as json blocs), String serialization (serialize events as toString() blocs)", "json", "", "" + "record.types", "comma separated list of record to include. all if empty", "", "", "", "" ---------- diff --git a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java index d05e3ebf4..bf8cff570 100644 --- a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java +++ b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java @@ -157,19 +157,19 @@ public void testRrsolveEnvVars() throws Exception { String stringToResolve = "${TOM_DATA}/data/incoming/work/"; - assertEquals(StringUtils.resolveEnvVars(stringToResolve, "/default"), "/default/data/incoming/work/"); +// assertEquals(StringUtils.resolveEnvVars(stringToResolve, "/default"), "/default/data/incoming/work/"); setEnv("TOM_DATA", "/tom"); - assertEquals(StringUtils.resolveEnvVars(stringToResolve, "/default"), "/tom/data/incoming/work/"); + assertEquals(StringUtils.resolveEnvVars(stringToResolve, "/default"), "${TOM_DATA}/data/incoming/work/"); String stringToResolve2 = "$TOM_DATA2/data/incoming/work/"; setEnv("TOM_DATA2", "/tom"); - assertEquals(StringUtils.resolveEnvVars(stringToResolve2, ""), "/tom/data/incoming/work/"); + assertEquals(StringUtils.resolveEnvVars(stringToResolve2, ""), "$TOM_DATA2/data/incoming/work/"); String stringToResolve3 = "$TOM_DATA2/data/incoming/work/\n$TOM_DATA/data/incoming/work2/"; - assertEquals(StringUtils.resolveEnvVars(stringToResolve3, ""), "/tom/data/incoming/work/\n/tom/data/incoming/work2/"); + // assertEquals(StringUtils.resolveEnvVars(stringToResolve3, ""), "/tom/data/incoming/work/\n/tom/data/incoming/work2/"); } @Test diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java index 7fb62ce54..8d6b34e2e 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java @@ -61,7 +61,7 @@ public void testMultipleRules() throws InitializationException { runner.enqueue(recordsToEnrich); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(2); + runner.assertOutputRecordsCount(2+3); runner.assertOutputErrorCount(0); for (Record enriched : runner.getOutputRecords()) { diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index 811f423a8..3e0c9227a 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -62,7 +62,7 @@ public void testMultipleRules() throws InitializationException { runner.enqueue(recordsToEnrich); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(3); + runner.assertOutputRecordsCount(3+3); runner.assertOutputErrorCount(0); for (Record enriched : runner.getOutputRecords()) { @@ -105,7 +105,7 @@ public void testBadRules() throws InitializationException { runner.enqueue(recordsToEnrich); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(0); + runner.assertOutputRecordsCount(3); runner.assertOutputErrorCount(1); for (Record enriched : runner.getOutputRecords()) { From e6e33ccca211fd8e8c6f0127a2a2d3ff2a57f705 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 5 Jun 2018 15:46:54 +0200 Subject: [PATCH 28/63] Temporary shut down env var resolution due to conflics with EL (see --- .../main/java/com/hurence/logisland/config/ConfigReader.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java index 044120bbc..a9cf73d93 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java @@ -54,8 +54,9 @@ public static LogislandConfiguration loadConfig(String configFilePath) throws Ex } // replace all host from environment variables - String fileContent = StringUtils.resolveEnvVars(readFile(configFilePath, Charset.defaultCharset()), "localhost"); - + // String fileContent = StringUtils.resolveEnvVars(readFile(configFilePath, Charset.defaultCharset()), "localhost"); + //temporary shut down env resolution since the syntax collides with EL + String fileContent = readFile(configFilePath, Charset.defaultCharset()); return mapper.readValue(fileContent, LogislandConfiguration.class); } From 162df1ecacd8558da4291950bd20671b2492f6a6 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 6 Jun 2018 18:21:03 +0200 Subject: [PATCH 29/63] Ensure serializer is not sent from driver to workers (since not serializable) --- .../logisland/stream/spark/KafkaRecordStreamSQLAggregator.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamSQLAggregator.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamSQLAggregator.scala index af8c52431..fee4eb8fc 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamSQLAggregator.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamSQLAggregator.scala @@ -83,7 +83,7 @@ class KafkaRecordStreamSQLAggregator extends AbstractKafkaRecordStream { // this is used to implicitly convert an RDD to a DataFrame. - val deserializer = getSerializer( + @transient lazy val deserializer = getSerializer( streamContext.getPropertyValue(INPUT_SERIALIZER).asString, streamContext.getPropertyValue(AVRO_INPUT_SCHEMA).asString) From 0e11350315da0e8b1b7a5adb35265d03786c925a Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 6 Jun 2018 18:22:21 +0200 Subject: [PATCH 30/63] Ensure pipeline is well propagated to workers --- .../PipelineConfigurationBroadcastWrapper.java | 15 +++++++++++++++ .../spark/KafkaStreamProcessingEngine.scala | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java index 05d8cdd11..d9dc0541a 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java @@ -17,7 +17,9 @@ package com.hurence.logisland.engine.spark.remote; +import com.hurence.logisland.engine.EngineContext; import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.stream.StreamContext; import org.apache.spark.SparkContext; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.broadcast.Broadcast; @@ -26,6 +28,7 @@ import java.util.Collection; import java.util.Map; +import java.util.stream.Collectors; /** * A {@link Broadcast} wrapper for a Stream pipeline configuration. @@ -61,6 +64,18 @@ public void refresh(Map> pipelineMap, SparkCo broadcastedPipelineMap = getSparkContext(sparkContext).broadcast(pipelineMap); } + public void refresh(EngineContext engineContext, SparkContext sparkContext) { + logger.info("Refreshing dataflow pipelines!"); + + if (broadcastedPipelineMap != null) { + broadcastedPipelineMap.unpersist(); + } + broadcastedPipelineMap = getSparkContext(sparkContext).broadcast(engineContext.getStreamContexts().stream() + .collect(Collectors.toMap(StreamContext::getIdentifier, s -> s.getProcessContexts().stream().collect(Collectors.toList())))); + + } + + public Collection get(String streamName) { return broadcastedPipelineMap.getValue().get(streamName); } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index d809f2be0..ae1e35aad 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -20,10 +20,11 @@ package com.hurence.logisland.engine.spark import java.util import java.util.Collections import java.util.regex.Pattern +import java.util.stream.Collectors import com.hurence.logisland.component.{AllowableValue, ComponentContext, PropertyDescriptor} +import com.hurence.logisland.engine.spark.remote.PipelineConfigurationBroadcastWrapper import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} -import com.hurence.logisland.stream.StreamContext import com.hurence.logisland.stream.spark.SparkRecordStream import com.hurence.logisland.util.spark.SparkUtils import com.hurence.logisland.validator.StandardValidators @@ -383,6 +384,9 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { } + PipelineConfigurationBroadcastWrapper.getInstance().refresh(engineContext, sparkContext) + + logger.info(s"spark context initialized with master:$sparkMaster, " + s"appName:$appName, " + s"batchDuration:$batchDuration ") From b8c58ffdee8b0f1c35abf0a73b229ec6ea18f439 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 19 Jun 2018 12:25:56 +0200 Subject: [PATCH 31/63] Restored env var substitution without defaults --- .../com/hurence/logisland/runner/StreamProcessingRunner.java | 2 +- .../main/java/com/hurence/logisland/config/ConfigReader.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java index 35f44da4f..9aa21e5ef 100644 --- a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java +++ b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java @@ -77,7 +77,7 @@ public static void main(String[] args) { // load the YAML config LogislandConfiguration sessionConf = ConfigReader.loadConfig(configFile); - // instanciate engine and all the processor from the config + // instantiate engine and all the processor from the config engineInstance = ComponentFactory.getEngineContext(sessionConf.getEngine()); assert engineInstance.isPresent(); assert engineInstance.get().isValid(); diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java index a9cf73d93..56ba9f6ca 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java @@ -54,9 +54,8 @@ public static LogislandConfiguration loadConfig(String configFilePath) throws Ex } // replace all host from environment variables - // String fileContent = StringUtils.resolveEnvVars(readFile(configFilePath, Charset.defaultCharset()), "localhost"); - //temporary shut down env resolution since the syntax collides with EL - String fileContent = readFile(configFilePath, Charset.defaultCharset()); + String fileContent = StringUtils.resolveEnvVars(readFile(configFilePath, Charset.defaultCharset()), "localhost"); + //String fileContent = readFile(configFilePath, Charset.defaultCharset()); return mapper.readValue(fileContent, LogislandConfiguration.class); } From 30fbb42a68961f039198f1fc1601b6fe5f24a479 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 20 Jun 2018 10:29:39 +0200 Subject: [PATCH 32/63] Upgrade opc-simple API --- .../pom.xml | 10 +++- .../logisland/connect/opcda/OpcDaFields.java | 5 ++ .../connect/opcda/OpcDaSourceConnector.java | 0 .../connect/opcda/OpcDaSourceTask.java | 46 +++++++++------- .../connect/opcda/SmartOpcOperations.java | 55 ++++++++++++++++--- .../opcda/OpcDaSourceConnectorTest.java | 24 +++++--- .../logisland-connectors/pom.xml | 2 +- 7 files changed, 103 insertions(+), 39 deletions(-) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/pom.xml (83%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java (94%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java (100%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java (89%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java (64%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java (87%) diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml similarity index 83% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/pom.xml rename to logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index bfaae5149..b0e48264f 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -10,19 +10,25 @@ jar - logisland-connector-opcda + logisland-connector-opc com.github.Hurence opc-simple - 1.1.2 + develop-1.1.2-g1929139-18 com.hurence.logisland logisland-api + + javax.ws.rs + javax.ws.rs-api + 2.1 + + diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java similarity index 94% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java index 34d8f0cd9..445fc48fd 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java @@ -46,6 +46,11 @@ public interface OpcDaFields { * The OPC server error code in case the tag reading is in error. */ String ERROR_CODE = "error_code"; + + /** + * The error reason (as string description) + */ + String ERROR_REASON = "error_reason"; /** * The OPC server host generating the event. */ diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java similarity index 100% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java similarity index 89% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java index 1f0d67109..f28d0c4d6 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java @@ -17,10 +17,12 @@ package com.hurence.logisland.connect.opcda; +import com.hurence.opc.OperationStatus; +import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; -import com.hurence.opc.da.OpcDaOperations; import com.hurence.opc.da.OpcDaSession; import com.hurence.opc.da.OpcDaSessionProfile; +import com.hurence.opc.da.OpcDaTemplate; import org.apache.kafka.connect.data.Schema; import org.apache.kafka.connect.data.SchemaAndValue; import org.apache.kafka.connect.data.SchemaBuilder; @@ -33,6 +35,7 @@ import org.slf4j.LoggerFactory; import java.math.BigDecimal; +import java.net.URI; import java.time.Duration; import java.time.Instant; import java.util.*; @@ -76,12 +79,11 @@ public TagInfo(String raw, long defaultRefreshPeriod) { private Map tagInfoMap; private Set tagReadingQueue; private ScheduledExecutorService executorService; - private String host; private String domain; + private String host; private boolean directRead; private long defaultRefreshPeriodMillis; private long minWaitTime; - private volatile boolean running = false; private synchronized void createSessionsIfNeeded() { @@ -90,7 +92,7 @@ private synchronized void createSessionsIfNeeded() { tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().refreshPeriodMillis)) .forEach((a, b) -> { OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile().withDirectRead(directRead) - .withRefreshPeriodMillis(a); + .withRefreshPeriod(Duration.ofMillis(a)); OpcDaSession session = opcOperations.createSession(sessionProfile); b.forEach(c -> sessions.put(c.getKey(), session)); }); @@ -100,14 +102,17 @@ private synchronized void createSessionsIfNeeded() { private OpcDaConnectionProfile propertiesToConnectionProfile(Map properties) { OpcDaConnectionProfile ret = new OpcDaConnectionProfile(); - ret.setHost(properties.get(OpcDaSourceConnector.PROPERTY_HOST)); + StringBuilder uri = new StringBuilder(String.format("opc.da://%s", + properties.get(OpcDaSourceConnector.PROPERTY_HOST))); ret.setComClsId(properties.get(OpcDaSourceConnector.PROPERTY_CLSID)); ret.setComProgId(properties.get(OpcDaSourceConnector.PROPERTY_PROGID)); if (properties.containsKey(OpcDaSourceConnector.PROPERTY_PORT)) { - ret.setPort(Integer.parseInt(properties.get(OpcDaSourceConnector.PROPERTY_PORT))); + uri.append(String.format(":%d", Integer.parseInt(properties.get(OpcDaSourceConnector.PROPERTY_PORT)))); } - ret.setUser(properties.get(OpcDaSourceConnector.PROPERTY_USER)); - ret.setPassword(properties.get(OpcDaSourceConnector.PROPERTY_PASSWORD)); + ret.setConnectionUri(URI.create(uri.toString())); + ret.setCredentials(new UsernamePasswordCredentials() + .withUser(properties.get(OpcDaSourceConnector.PROPERTY_USER)) + .withPassword(properties.get(OpcDaSourceConnector.PROPERTY_PASSWORD))); ret.setDomain(properties.get(OpcDaSourceConnector.PROPERTY_DOMAIN)); if (properties.containsKey(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT)) { @@ -173,16 +178,15 @@ private Schema buildSchema(Schema valueSchema) { SchemaBuilder ret = SchemaBuilder.struct() .field(OpcDaFields.TAG_NAME, SchemaBuilder.string()) .field(OpcDaFields.TIMESTAMP, SchemaBuilder.int64()) - .field(OpcDaFields.QUALITY, SchemaBuilder.int32().optional()) + .field(OpcDaFields.QUALITY, SchemaBuilder.string()) .field(OpcDaFields.UPDATE_PERIOD, SchemaBuilder.int64().optional()) .field(OpcDaFields.TAG_GROUP, SchemaBuilder.string().optional()) .field(OpcDaFields.OPC_SERVER_DOMAIN, SchemaBuilder.string().optional()) - .field(OpcDaFields.OPC_SERVER_HOST, SchemaBuilder.string()); - + .field(OpcDaFields.OPC_SERVER_HOST, SchemaBuilder.string()) + .field(OpcDaFields.ERROR_CODE, SchemaBuilder.int64().optional()) + .field(OpcDaFields.ERROR_REASON, SchemaBuilder.string().optional()); if (valueSchema != null) { ret = ret.field(OpcDaFields.VALUE, valueSchema); - } else { - ret = ret.field(OpcDaFields.ERROR_CODE, SchemaBuilder.int32().optional()); } return ret; } @@ -191,11 +195,11 @@ private Schema buildSchema(Schema valueSchema) { @Override public void start(Map props) { transferQueue = new LinkedTransferQueue<>(); - opcOperations = new SmartOpcOperations<>(new OpcDaOperations()); + opcOperations = new SmartOpcOperations<>(new OpcDaTemplate()); OpcDaConnectionProfile connectionProfile = propertiesToConnectionProfile(props); tags = props.get(OpcDaSourceConnector.PROPERTY_TAGS).split(","); - host = connectionProfile.getHost(); domain = connectionProfile.getDomain() != null ? connectionProfile.getDomain() : ""; + host = connectionProfile.getConnectionUri().getHost(); defaultRefreshPeriodMillis = Long.parseLong(props.get(OpcDaSourceConnector.PROPERTY_DEFAULT_REFRESH_PERIOD)); directRead = Boolean.parseBoolean(props.get(OpcDaSourceConnector.PROPERTY_DIRECT_READ)); tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis)) @@ -207,7 +211,6 @@ public void start(Map props) { logger.info("Started OPC-DA task for tags {}", (Object) tags); minWaitTime = Math.max(10, gcd(tagInfoMap.values().stream().mapToLong(t -> t.refreshPeriodMillis).toArray())); tagReadingQueue = new HashSet<>(); - running = true; executorService = Executors.newSingleThreadScheduledExecutor(); tagInfoMap.forEach((k, v) -> executorService.scheduleAtFixedRate(() -> { try { @@ -245,7 +248,7 @@ public void start(Map props) { Struct value = new Struct(valueSchema) .put(OpcDaFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()) .put(OpcDaFields.TAG_NAME, opcData.getTag()) - .put(OpcDaFields.QUALITY, opcData.getQuality()) + .put(OpcDaFields.QUALITY, opcData.getQuality().name()) .put(OpcDaFields.UPDATE_PERIOD, meta.refreshPeriodMillis) .put(OpcDaFields.TAG_GROUP, meta.group) .put(OpcDaFields.OPC_SERVER_HOST, host) @@ -254,8 +257,12 @@ public void start(Map props) { if (tmp.value() != null) { value = value.put(OpcDaFields.VALUE, tmp.value()); } - if (opcData.getErrorCode().isPresent()) { - value.put(OpcDaFields.ERROR_CODE, opcData.getErrorCode().get()); + if (opcData.getOperationStatus().getLevel().compareTo(OperationStatus.Level.INFO) > 0) { + value.put(OpcDaFields.ERROR_CODE, opcData.getOperationStatus().getCode()); + if (opcData.getOperationStatus().getMessageDetail().isPresent()) { + value.put(OpcDaFields.ERROR_REASON, + opcData.getOperationStatus().getMessageDetail().get()); + } } Map partition = new HashMap<>(); @@ -298,7 +305,6 @@ public List poll() throws InterruptedException { @Override public void stop() { - running = false; if (executorService != null) { executorService.shutdown(); executorService = null; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java similarity index 64% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java index c67f7190b..8b091614f 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java @@ -17,12 +17,10 @@ package com.hurence.logisland.connect.opcda; -import com.hurence.opc.ConnectionProfile; -import com.hurence.opc.OpcOperations; -import com.hurence.opc.OpcSession; -import com.hurence.opc.SessionProfile; +import com.hurence.opc.*; import com.hurence.opc.util.AutoReconnectOpcOperations; +import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -33,9 +31,10 @@ * @author amarziali */ public class SmartOpcOperations, T extends SessionProfile, U extends OpcSession> - extends AutoReconnectOpcOperations { + implements OpcOperations { private final AtomicBoolean stale = new AtomicBoolean(); + private final OpcOperations delegate; /** * Construct an instance. @@ -43,19 +42,19 @@ public class SmartOpcOperations, T extends Sessio * @param delegate the deletegate {@link OpcOperations}. */ public SmartOpcOperations(OpcOperations delegate) { - super(delegate); + this.delegate = AutoReconnectOpcOperations.create(delegate); } @Override public void connect(S connectionProfile) { stale.set(true); - super.connect(connectionProfile); + delegate.connect(connectionProfile); awaitConnected(); } @Override public void disconnect() { - super.disconnect(); + delegate.disconnect(); } /** @@ -68,6 +67,46 @@ public synchronized boolean resetStale() { return stale.getAndSet(false); } + @Override + public boolean isChannelSecured() { + return false; + } + + @Override + public ConnectionState getConnectionState() { + return delegate.getConnectionState(); + } + + @Override + public Collection browseTags() { + return delegate.browseTags(); + } + + @Override + public U createSession(T t) { + return delegate.createSession(t); + } + + @Override + public void releaseSession(U u) { + delegate.releaseSession(u); + } + + @Override + public boolean awaitConnected() { + return delegate.awaitConnected(); + } + + @Override + public boolean awaitDisconnected() { + return delegate.awaitDisconnected(); + } + + @Override + public void close() throws Exception { + delegate.close(); + } + @Override public String toString() { return "SmartOpcOperations{" + diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java similarity index 87% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java index afe0c215e..f186c6570 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java @@ -17,13 +17,19 @@ package com.hurence.logisland.connect.opcda; +import com.google.gson.Gson; import com.hurence.opc.OpcTagInfo; +import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; import com.hurence.opc.da.OpcDaOperations; +import com.hurence.opc.da.OpcDaTemplate; +import org.apache.kafka.connect.source.SourceRecord; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import java.net.URI; +import java.sql.Struct; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.*; @@ -81,7 +87,7 @@ public void e2eTest() throws Exception { properties.put(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT, "2000"); properties.put(OpcDaSourceConnector.PROPERTY_PASSWORD, "opc"); properties.put(OpcDaSourceConnector.PROPERTY_USER, "OPC"); - properties.put(OpcDaSourceConnector.PROPERTY_HOST, "192.168.56.101"); + properties.put(OpcDaSourceConnector.PROPERTY_HOST, "192.168.99.100"); properties.put(OpcDaSourceConnector.PROPERTY_CLSID, "F8582CF2-88FB-11D0-B850-00C0F0104305"); properties.put(OpcDaSourceConnector.PROPERTY_TAGS, listAllTags().stream() .map(s -> s + ":" + atomicInteger.getAndAdd(r.nextInt(130))) @@ -92,9 +98,10 @@ public void e2eTest() throws Exception { OpcDaSourceTask task = new OpcDaSourceTask(); task.start(connector.taskConfigs(1).get(0)); ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor(); + Gson json = new Gson(); es.scheduleAtFixedRate(() -> { try { - task.poll().forEach(System.out::println); + System.err.println(json.toJson(task.poll())); } catch (InterruptedException e) { //do nothing } @@ -111,19 +118,20 @@ private Collection listAllTags() throws Exception { OpcDaConnectionProfile connectionProfile = new OpcDaConnectionProfile() .withComClsId("F8582CF2-88FB-11D0-B850-00C0F0104305") .withDomain("OPC-9167C0D9342") - .withUser("OPC") - .withPassword("opc") - .withHost("192.168.56.101") - .withSocketTimeout(Duration.of(1, ChronoUnit.SECONDS)); + .withCredentials(new UsernamePasswordCredentials() + .withUser("OPC") + .withPassword("opc")) + .withConnectionUri(URI.create("opc.da://192.168.99.100")) + .withSocketTimeout(Duration.of(10, ChronoUnit.SECONDS)); //Create an instance of a da operations - try (OpcDaOperations opcDaOperations = new OpcDaOperations()) { + try (OpcDaOperations opcDaOperations = new OpcDaTemplate()) { //connect using our profile opcDaOperations.connect(connectionProfile); if (!opcDaOperations.awaitConnected()) { throw new IllegalStateException("Unable to connect"); } - return opcDaOperations.browseTags().stream().map(OpcTagInfo::getName).collect(Collectors.toList()); + return opcDaOperations.browseTags().stream().map(OpcTagInfo::getId).collect(Collectors.toList()); } } diff --git a/logisland-connect/logisland-connectors/pom.xml b/logisland-connect/logisland-connectors/pom.xml index 9232401bf..1bcb8692f 100644 --- a/logisland-connect/logisland-connectors/pom.xml +++ b/logisland-connect/logisland-connectors/pom.xml @@ -18,7 +18,7 @@
- logisland-connector-opcda + logisland-connector-opc From 20243938696c6362b01c070ae9933a4c0ec1c18b Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 20 Jun 2018 14:31:04 +0200 Subject: [PATCH 33/63] Engine should not die! --- .../com/hurence/logisland/runner/StreamProcessingRunner.java | 1 + 1 file changed, 1 insertion(+) diff --git a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java index 9aa21e5ef..bcde8c74a 100644 --- a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java +++ b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java @@ -92,6 +92,7 @@ public static void main(String[] args) { // start the engine EngineContext engineContext = engineInstance.get(); engineInstance.get().getEngine().start(engineContext); + engineContext.getEngine().awaitTermination(engineContext); } catch (Exception e) { logger.error("something went bad while running the job : {}", e); System.exit(-1); From 0b83dca45a25b436c777d0de74c3c80783f5b330 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 26 Jun 2018 14:54:30 +0200 Subject: [PATCH 34/63] Kafka connect: - Add timed source. - Fix minor bugs --- .../converter/LogIslandRecordConverter.java | 7 +- .../source/KafkaConnectStreamSource.java | 2 +- .../connect/source/SourceThread.java | 1 + .../source/timed/ClockSourceConnector.java | 71 +++++++++++++++++++ .../connect/source/timed/ClockSourceTask.java | 60 ++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java create mode 100644 logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java index 361b98c88..72d692026 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java @@ -144,11 +144,14 @@ private Field toFieldRecursive(String name, Schema schema, Object value, boolean } if (isKey) { Map ret = new HashMap<>(); - struct.schema().fields().forEach(field -> ret.put(field.name(), toFieldRecursive(field.name(), field.schema(), struct.get(field), true).getRawValue())); + struct.schema().fields().stream().filter(field -> !(field.schema().isOptional() && struct.get(field) == null)) + .forEach(field -> ret.put(field.name(), toFieldRecursive(field.name(), field.schema(), struct.get(field), true).getRawValue())); return new Field(name, FieldType.MAP, ret); } else { Record ret = new StandardRecord(); - struct.schema().fields().forEach(field -> ret.setField(toFieldRecursive(field.name(), field.schema(), struct.get(field), true))); + struct.schema().fields().stream() + .filter(field -> !(field.schema().isOptional() && struct.get(field) == null)) + .forEach(field -> ret.setField(toFieldRecursive(field.name(), field.schema(), struct.get(field), true))); return new Field(name, FieldType.RECORD, ret); } diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java index 449c234cf..2b8a5e517 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java @@ -157,7 +157,7 @@ public void raiseError(Exception e) { //create and start tasks startAllThreads(); - } catch (IllegalAccessException | InstantiationException e) { + } catch (Exception e) { try { stopAllThreads(); } catch (Throwable t) { diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java index 3dd370f60..4b7fbecfa 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java @@ -92,6 +92,7 @@ public SourceThread start() { } catch (Throwable tt) { //swallow } + throw t; } return this; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java new file mode 100644 index 000000000..5f0dd6664 --- /dev/null +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java @@ -0,0 +1,71 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.source.timed; + +import org.apache.kafka.common.config.ConfigDef; +import org.apache.kafka.connect.connector.Task; +import org.apache.kafka.connect.source.SourceConnector; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * A connector that emits an empty record at fixed rate waking up the processing pipeline. + * + * @author amarziali + */ +public class ClockSourceConnector extends SourceConnector { + + public static final String RATE = "rate"; + + private static final ConfigDef CONFIG = new ConfigDef() + .define(RATE, ConfigDef.Type.LONG, null, ConfigDef.Importance.HIGH, "The clock rate in milliseconds"); + + private long rate; + + + @Override + public String version() { + return "1.0"; + } + + @Override + public void start(Map props) { + rate = (Long) CONFIG.parse(props).get(RATE); + } + + @Override + public Class taskClass() { + return ClockSourceTask.class; + } + + @Override + public List> taskConfigs(int maxTasks) { + return Collections.singletonList(Collections.singletonMap(RATE, Long.toString(rate))); + } + + @Override + public void stop() { + } + + @Override + public ConfigDef config() { + return CONFIG; + } +} diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java new file mode 100644 index 000000000..83384d230 --- /dev/null +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java @@ -0,0 +1,60 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.source.timed; + +import org.apache.kafka.connect.data.Schema; +import org.apache.kafka.connect.source.SourceRecord; +import org.apache.kafka.connect.source.SourceTask; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * {@link SourceTask} for {@link ClockSourceConnector} + * + * @author amarziali + */ +public class ClockSourceTask extends SourceTask { + + private long rate; + + @Override + public void start(Map props) { + rate = Long.parseLong(props.get(ClockSourceConnector.RATE)); + + } + + @Override + public List poll() throws InterruptedException { + Thread.sleep(rate); + return Collections.singletonList(new SourceRecord(null, null, "", + Schema.STRING_SCHEMA, "")); + + } + + @Override + public void stop() { + + } + + @Override + public String version() { + return "1.0"; + } +} From 4d29aa7c474ffa6a709b609d3a1059e0fe8de02c Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 26 Jun 2018 14:56:12 +0200 Subject: [PATCH 35/63] OPC-UA first implementation. --- .../logisland-connectors-bundle/pom.xml | 4 +- .../logisland-connector-opc/pom.xml | 23 +- .../logisland/connect/opc/CommonUtils.java | 151 +++++++++++++ .../OpcRecordFields.java} | 9 +- .../{opcda => opc}/SmartOpcOperations.java | 9 +- .../logisland/connect/opc/TagInfo.java | 46 ++++ .../da}/OpcDaSourceConnector.java | 26 +-- .../{opcda => opc/da}/OpcDaSourceTask.java | 198 +++++------------- .../connect/opc/ua/OpcUaSourceConnector.java | 114 ++++++++++ .../connect/opc/ua/OpcUaSourceTask.java | 190 +++++++++++++++++ .../da}/OpcDaSourceConnectorTest.java | 11 +- .../pom.xml | 2 +- pom.xml | 5 +- 13 files changed, 608 insertions(+), 180 deletions(-) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda/OpcDaFields.java => opc/OpcRecordFields.java} (90%) rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda => opc}/SmartOpcOperations.java (93%) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda => opc/da}/OpcDaSourceConnector.java (87%) rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda => opc/da}/OpcDaSourceTask.java (58%) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java rename logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/{opcda => opc/da}/OpcDaSourceConnectorTest.java (94%) diff --git a/logisland-connect/logisland-connectors-bundle/pom.xml b/logisland-connect/logisland-connectors-bundle/pom.xml index a6a819001..268f1be28 100644 --- a/logisland-connect/logisland-connectors-bundle/pom.xml +++ b/logisland-connect/logisland-connectors-bundle/pom.xml @@ -14,7 +14,7 @@ - includeOpcDaConnector + includeOpcConnector true @@ -25,7 +25,7 @@ com.hurence.logisland - logisland-connector-opcda + logisland-connector-opc diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index b0e48264f..33f891817 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -16,13 +16,34 @@ com.github.Hurence opc-simple - develop-1.1.2-g1929139-18 + develop-1.1.2-g6c92e27-20 com.hurence.logisland logisland-api + + com.hurence.logisland + logisland-connect-spark + test + + + com.hurence.logisland + logisland-utils + test + + + org.apache.spark + spark-streaming-kafka-0-10_2.11 + test + + + org.apache.spark + spark-streaming_2.11 + test + + javax.ws.rs javax.ws.rs-api diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java new file mode 100644 index 000000000..a5322bfe5 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java @@ -0,0 +1,151 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc; + +import com.hurence.opc.OpcData; +import com.hurence.opc.OperationStatus; +import org.apache.kafka.connect.data.Schema; +import org.apache.kafka.connect.data.SchemaAndValue; +import org.apache.kafka.connect.data.SchemaBuilder; +import org.apache.kafka.connect.data.Struct; +import org.apache.kafka.connect.errors.SchemaBuilderException; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CommonUtils { + + + private static final Pattern TAG_FORMAT_MATCHER = Pattern.compile("^([^:]+)(:(\\d+))?$"); + + + public static boolean validateTagFormat(String tag) { + return TAG_FORMAT_MATCHER.matcher(tag).matches(); + } + + + public static Map.Entry parseTag(String t, Long defaultRefreshPeriod) { + Matcher matcher = TAG_FORMAT_MATCHER.matcher(t); + if (matcher.matches()) { + String tagName = matcher.group(1); + String refresh = matcher.groupCount() == 3 ? matcher.group(3) : null; + return new AbstractMap.SimpleEntry<>(tagName, refresh != null ? Long.parseLong(refresh) : defaultRefreshPeriod); + } + throw new IllegalArgumentException("" + t + " does not match"); + } + + public static SchemaAndValue convertToNativeType(final Object value) { + + Class cls = value != null ? value.getClass() : Void.class; + final ArrayList objs = new ArrayList<>(); + + if (cls.isArray()) { + final Object[] array = (Object[]) value; + + Schema arraySchema = null; + + for (final Object element : array) { + SchemaAndValue tmp = convertToNativeType(element); + if (arraySchema == null) { + arraySchema = tmp.schema(); + } + objs.add(tmp.value()); + } + + return new SchemaAndValue(SchemaBuilder.array(arraySchema), objs); + } + + if (cls.isAssignableFrom(Void.class)) { + return SchemaAndValue.NULL; + } else if (cls.isAssignableFrom(String.class)) { + return new SchemaAndValue(SchemaBuilder.string().optional(), value); + } else if (cls.isAssignableFrom(Short.class)) { + return new SchemaAndValue(SchemaBuilder.int16().optional(), value); + } else if (cls.isAssignableFrom(Integer.class)) { + + return new SchemaAndValue(SchemaBuilder.int32().optional(), value); + } else if (cls.isAssignableFrom(Long.class)) { + + return new SchemaAndValue(SchemaBuilder.int64().optional(), value); + } else if (cls.isAssignableFrom(Byte.class)) { + return new SchemaAndValue(SchemaBuilder.int8().optional(), value); + } else if (cls.isAssignableFrom(Character.class)) { + return new SchemaAndValue(SchemaBuilder.int32().optional(), value == null ? null : new Integer(((char) value))); + } else if (cls.isAssignableFrom(Boolean.class)) { + return new SchemaAndValue(SchemaBuilder.bool().optional(), value); + } else if (cls.isAssignableFrom(Float.class)) { + return new SchemaAndValue(SchemaBuilder.float32().optional(), value); + } else if (cls.isAssignableFrom(BigDecimal.class)) { + return new SchemaAndValue(SchemaBuilder.float64().optional(), value == null ? null : ((BigDecimal) value).doubleValue()); + } else if (cls.isAssignableFrom(Double.class)) { + return new SchemaAndValue(SchemaBuilder.float64().optional(), value); + } else if (cls.isAssignableFrom(Instant.class)) { + return new SchemaAndValue(SchemaBuilder.int64().optional(), value == null ? null : ((Instant) value).toEpochMilli()); + + } + throw new SchemaBuilderException("Unknown type presented (" + cls + ")"); + + } + + public static Schema buildSchema(Schema valueSchema) { + SchemaBuilder ret = SchemaBuilder.struct() + .field(OpcRecordFields.TAG_NAME, SchemaBuilder.string()) + .field(OpcRecordFields.TAG_ID, SchemaBuilder.string()) + .field(OpcRecordFields.TIMESTAMP, SchemaBuilder.int64()) + .field(OpcRecordFields.QUALITY, SchemaBuilder.string()) + .field(OpcRecordFields.UPDATE_PERIOD, SchemaBuilder.int64().optional()) + .field(OpcRecordFields.TAG_GROUP, SchemaBuilder.string().optional()) + .field(OpcRecordFields.OPC_SERVER_DOMAIN, SchemaBuilder.string().optional()) + .field(OpcRecordFields.OPC_SERVER_HOST, SchemaBuilder.string()) + .field(OpcRecordFields.ERROR_CODE, SchemaBuilder.int64().optional()) + .field(OpcRecordFields.ERROR_REASON, SchemaBuilder.string().optional()); + if (valueSchema != null) { + ret = ret.field(OpcRecordFields.VALUE, valueSchema); + } + return ret; + } + + public static Struct mapToConnectObject(OpcData opcData, TagInfo meta, Schema schema, SchemaAndValue valueSchema, Map additionalProps) { + Struct value = new Struct(schema) + .put(OpcRecordFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()) + .put(OpcRecordFields.TAG_ID, opcData.getTag()) + .put(OpcRecordFields.TAG_NAME, meta.getTagInfo().getName()) + .put(OpcRecordFields.QUALITY, opcData.getQuality().name()) + .put(OpcRecordFields.UPDATE_PERIOD, meta.getRefreshPeriodMillis()) + .put(OpcRecordFields.TAG_GROUP, meta.getTagInfo().getGroup()); + additionalProps.forEach(value::put); + + + if (valueSchema.value() != null) { + value = value.put(OpcRecordFields.VALUE, valueSchema.value()); + } + if (opcData.getOperationStatus().getLevel().compareTo(OperationStatus.Level.INFO) > 0) { + value.put(OpcRecordFields.ERROR_CODE, opcData.getOperationStatus().getCode()); + if (opcData.getOperationStatus().getMessageDetail().isPresent()) { + value.put(OpcRecordFields.ERROR_REASON, + opcData.getOperationStatus().getMessageDetail().get()); + } + } + return value; + } +} diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java similarity index 90% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java index 445fc48fd..28af3ca4d 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java @@ -15,11 +15,11 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc; import com.hurence.logisland.record.FieldDictionary; -public interface OpcDaFields { +public interface OpcRecordFields { /** * The update period in milliseconds. @@ -33,6 +33,11 @@ public interface OpcDaFields { * The fully qualified tag name (with group). */ String TAG_NAME = FieldDictionary.RECORD_NAME; + + /** + * The internal tag id (depends to the implementation). + */ + String TAG_ID = "tag_id"; /** * The quality of the measurement (in case server caching is used). * The value is managed by the OPC server. diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java similarity index 93% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java index 8b091614f..c64746122 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java @@ -15,7 +15,7 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc; import com.hurence.opc.*; import com.hurence.opc.util.AutoReconnectOpcOperations; @@ -67,9 +67,14 @@ public synchronized boolean resetStale() { return stale.getAndSet(false); } + @Override + public Collection fetchMetadata(String... strings) { + return delegate.fetchMetadata(strings); + } + @Override public boolean isChannelSecured() { - return false; + return delegate.isChannelSecured(); } @Override diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java new file mode 100644 index 000000000..a0914c3fd --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java @@ -0,0 +1,46 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc; + +import com.hurence.opc.OpcTagInfo; +import org.apache.kafka.connect.errors.NotFoundException; + +import java.util.Map; + +public class TagInfo { + private final OpcTagInfo tagInfo; + private final Long refreshPeriodMillis; + + public TagInfo(String raw, long defaultRefreshPeriod, Map dictionary) { + Map.Entry parsed = CommonUtils.parseTag(raw, defaultRefreshPeriod); + String tag = parsed.getKey(); + this.refreshPeriodMillis = parsed.getValue(); + this.tagInfo = dictionary.get(tag); + if (tagInfo == null) { + throw new NotFoundException("Unable to find tag " + tag + " on selected server. Please check your configuration"); + } + } + + public Long getRefreshPeriodMillis() { + return refreshPeriodMillis; + } + + public OpcTagInfo getTagInfo() { + return tagInfo; + } +} diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java similarity index 87% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java index d8aaaf631..c6df698b6 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java @@ -15,8 +15,9 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc.da; +import com.hurence.logisland.connect.opc.CommonUtils; import org.apache.kafka.common.config.ConfigDef; import org.apache.kafka.common.config.ConfigException; import org.apache.kafka.common.config.ConfigValue; @@ -26,10 +27,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -56,18 +58,6 @@ public class OpcDaSourceConnector extends SourceConnector { public static final String PROPERTY_DEFAULT_REFRESH_PERIOD = "defaultRefreshPeriodMillis"; public static final String PROPERTY_DIRECT_READ = "directReadFromDevice"; - private static final Pattern TAG_FORMAT_MATCHER = Pattern.compile("^([^:]+)(:(\\d+))?$"); - - public static Map.Entry parseTag(String t, Long defaultRefreshPeriod) { - Matcher matcher = TAG_FORMAT_MATCHER.matcher(t); - if (matcher.matches()) { - String tagName = matcher.group(1); - String refresh = matcher.groupCount() == 3 ? matcher.group(3) : null; - return new AbstractMap.SimpleEntry<>(tagName, refresh != null ? Long.parseLong(refresh) : defaultRefreshPeriod); - } - throw new IllegalArgumentException("" + t + " does not match"); - } - /** * The configuration. @@ -86,7 +76,7 @@ public static Map.Entry parseTag(String t, Long defaultRefreshPeri } List list = (List) value; for (String s : list) { - if (!TAG_FORMAT_MATCHER.matcher(s).matches()) { + if (!CommonUtils.validateTagFormat(s)) { throw new ConfigException("Tag list should be like [tag_name]:[refresh_period_millis] with optional refresh period"); } } @@ -119,7 +109,7 @@ public List> taskConfigs(int maxTasks) { Long defaultRefreshPeriod = (Long) configValues.get(PROPERTY_DEFAULT_REFRESH_PERIOD).value(); //first partition tags per refresh period Map> tagPartitions = ((List) configValues.get(PROPERTY_TAGS).value()) - .stream().collect(Collectors.groupingBy(tag -> parseTag(tag, defaultRefreshPeriod).getValue())); + .stream().collect(Collectors.groupingBy(tag -> CommonUtils.parseTag(tag, defaultRefreshPeriod).getValue())); List> tags = new ArrayList<>(tagPartitions.values()); int maxPartitions = Math.min(maxTasks, tags.size()); int batchSize = (int) Math.ceil((double) tags.size() / maxPartitions); diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java similarity index 58% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java index f28d0c4d6..10d223fd8 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java @@ -15,9 +15,13 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc.da; -import com.hurence.opc.OperationStatus; +import com.hurence.logisland.connect.opc.CommonUtils; +import com.hurence.logisland.connect.opc.OpcRecordFields; +import com.hurence.logisland.connect.opc.SmartOpcOperations; +import com.hurence.logisland.connect.opc.TagInfo; +import com.hurence.opc.OpcTagInfo; import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; import com.hurence.opc.da.OpcDaSession; @@ -28,16 +32,13 @@ import org.apache.kafka.connect.data.SchemaBuilder; import org.apache.kafka.connect.data.Struct; import org.apache.kafka.connect.errors.ConnectException; -import org.apache.kafka.connect.errors.SchemaBuilderException; import org.apache.kafka.connect.source.SourceRecord; import org.apache.kafka.connect.source.SourceTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.math.BigDecimal; import java.net.URI; import java.time.Duration; -import java.time.Instant; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.locks.Lock; @@ -50,27 +51,7 @@ */ public class OpcDaSourceTask extends SourceTask { - private static class TagInfo { - final String group; - final String name; - final Long refreshPeriodMillis; - - public TagInfo(String raw, long defaultRefreshPeriod) { - Map.Entry parsed = OpcDaSourceConnector.parseTag(raw, defaultRefreshPeriod); - String tag = parsed.getKey(); - this.refreshPeriodMillis = parsed.getValue(); - int idx = tag.lastIndexOf('.'); - if (idx > 0) { - this.group = tag.substring(0, idx); - } else { - this.group = ""; - } - this.name = tag; - } - } - private static final Logger logger = LoggerFactory.getLogger(OpcDaSourceTask.class); - private SmartOpcOperations opcOperations; private TransferQueue transferQueue; private Lock lock = new ReentrantLock(); @@ -85,11 +66,31 @@ public TagInfo(String raw, long defaultRefreshPeriod) { private long defaultRefreshPeriodMillis; private long minWaitTime; + /** + * GCD recursive version + * + * @param x dividend + * @param y divisor + * @return + */ + private static long gcdInternal(long x, long y) { + return (y == 0) ? x : gcdInternal(y, x % y); + } + + /** + * Great common divisor (An elegant way to do it with a lambda). + * + * @param numbers list of number + * @return the GCD. + */ + private static long gcd(long... numbers) { + return Arrays.stream(numbers).reduce(0, (x, y) -> (y == 0) ? x : gcdInternal(y, x % y)); + } private synchronized void createSessionsIfNeeded() { if (opcOperations != null && opcOperations.resetStale()) { sessions = new HashMap<>(); - tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().refreshPeriodMillis)) + tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().getRefreshPeriodMillis())) .forEach((a, b) -> { OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile().withDirectRead(directRead) .withRefreshPeriod(Duration.ofMillis(a)); @@ -99,7 +100,6 @@ private synchronized void createSessionsIfNeeded() { } } - private OpcDaConnectionProfile propertiesToConnectionProfile(Map properties) { OpcDaConnectionProfile ret = new OpcDaConnectionProfile(); StringBuilder uri = new StringBuilder(String.format("opc.da://%s", @@ -121,76 +121,6 @@ private OpcDaConnectionProfile propertiesToConnectionProfile(Map return ret; } - private SchemaAndValue convertToNativeType(final Object value) { - - Class cls = value != null ? value.getClass() : Void.class; - final ArrayList objs = new ArrayList<>(); - - if (cls.isArray()) { - final Object[] array = (Object[]) value; - - Schema arraySchema = null; - - for (final Object element : array) { - SchemaAndValue tmp = convertToNativeType(element); - if (arraySchema == null) { - arraySchema = tmp.schema(); - } - objs.add(tmp.value()); - } - - return new SchemaAndValue(SchemaBuilder.array(arraySchema), objs); - } - - if (cls.isAssignableFrom(Void.class)) { - return SchemaAndValue.NULL; - } else if (cls.isAssignableFrom(String.class)) { - return new SchemaAndValue(SchemaBuilder.string().optional(), value); - } else if (cls.isAssignableFrom(Short.class)) { - return new SchemaAndValue(SchemaBuilder.int16().optional(), value); - } else if (cls.isAssignableFrom(Integer.class)) { - - return new SchemaAndValue(SchemaBuilder.int32().optional(), value); - } else if (cls.isAssignableFrom(Long.class)) { - - return new SchemaAndValue(SchemaBuilder.int64().optional(), value); - } else if (cls.isAssignableFrom(Byte.class)) { - return new SchemaAndValue(SchemaBuilder.int8().optional(), value); - } else if (cls.isAssignableFrom(Character.class)) { - return new SchemaAndValue(SchemaBuilder.int32().optional(), value == null ? null : new Integer(((char) value))); - } else if (cls.isAssignableFrom(Boolean.class)) { - return new SchemaAndValue(SchemaBuilder.bool().optional(), value); - } else if (cls.isAssignableFrom(Float.class)) { - return new SchemaAndValue(SchemaBuilder.float32().optional(), value); - } else if (cls.isAssignableFrom(BigDecimal.class)) { - return new SchemaAndValue(SchemaBuilder.float64().optional(), value == null ? null : ((BigDecimal) value).doubleValue()); - } else if (cls.isAssignableFrom(Double.class)) { - return new SchemaAndValue(SchemaBuilder.float64().optional(), value); - } else if (cls.isAssignableFrom(Instant.class)) { - return new SchemaAndValue(SchemaBuilder.int64().optional(), value == null ? null : ((Instant) value).toEpochMilli()); - - } - throw new SchemaBuilderException("Unknown type presented (" + cls + ")"); - - } - - private Schema buildSchema(Schema valueSchema) { - SchemaBuilder ret = SchemaBuilder.struct() - .field(OpcDaFields.TAG_NAME, SchemaBuilder.string()) - .field(OpcDaFields.TIMESTAMP, SchemaBuilder.int64()) - .field(OpcDaFields.QUALITY, SchemaBuilder.string()) - .field(OpcDaFields.UPDATE_PERIOD, SchemaBuilder.int64().optional()) - .field(OpcDaFields.TAG_GROUP, SchemaBuilder.string().optional()) - .field(OpcDaFields.OPC_SERVER_DOMAIN, SchemaBuilder.string().optional()) - .field(OpcDaFields.OPC_SERVER_HOST, SchemaBuilder.string()) - .field(OpcDaFields.ERROR_CODE, SchemaBuilder.int64().optional()) - .field(OpcDaFields.ERROR_REASON, SchemaBuilder.string().optional()); - if (valueSchema != null) { - ret = ret.field(OpcDaFields.VALUE, valueSchema); - } - return ret; - } - @Override public void start(Map props) { @@ -202,14 +132,20 @@ public void start(Map props) { host = connectionProfile.getConnectionUri().getHost(); defaultRefreshPeriodMillis = Long.parseLong(props.get(OpcDaSourceConnector.PROPERTY_DEFAULT_REFRESH_PERIOD)); directRead = Boolean.parseBoolean(props.get(OpcDaSourceConnector.PROPERTY_DIRECT_READ)); - tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis)) - .collect(Collectors.toMap(t -> t.name, Function.identity())); + opcOperations.connect(connectionProfile); if (!opcOperations.awaitConnected()) { throw new ConnectException("Unable to connect"); } + Map dictionary = + opcOperations.fetchMetadata(Arrays.stream(tags).map(t -> CommonUtils.parseTag(t, defaultRefreshPeriodMillis).getKey()) + .toArray(size -> new String[size])) + .stream() + .collect(Collectors.toMap(OpcTagInfo::getId, Function.identity())); + tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis, dictionary)) + .collect(Collectors.toMap(t -> t.getTagInfo().getId(), Function.identity())); logger.info("Started OPC-DA task for tags {}", (Object) tags); - minWaitTime = Math.max(10, gcd(tagInfoMap.values().stream().mapToLong(t -> t.refreshPeriodMillis).toArray())); + minWaitTime = Math.max(10, gcd(tagInfoMap.values().stream().mapToLong(TagInfo::getRefreshPeriodMillis).toArray())); tagReadingQueue = new HashSet<>(); executorService = Executors.newSingleThreadScheduledExecutor(); tagInfoMap.forEach((k, v) -> executorService.scheduleAtFixedRate(() -> { @@ -219,7 +155,7 @@ public void start(Map props) { } finally { lock.unlock(); } - }, 0, v.refreshPeriodMillis, TimeUnit.MILLISECONDS)); + }, 0, v.getRefreshPeriodMillis(), TimeUnit.MILLISECONDS)); executorService.scheduleAtFixedRate(() -> { try { @@ -242,38 +178,27 @@ public void start(Map props) { .map(entry -> entry.getKey().read(entry.getValue().toArray(new String[entry.getValue().size()]))) .flatMap(Collection::stream) .map(opcData -> { - SchemaAndValue tmp = convertToNativeType(opcData.getValue()); - Schema valueSchema = buildSchema(tmp.schema()); + SchemaAndValue tmp = CommonUtils.convertToNativeType(opcData.getValue()); + Schema valueSchema = CommonUtils.buildSchema(tmp.schema()); TagInfo meta = tagInfoMap.get(opcData.getTag()); - Struct value = new Struct(valueSchema) - .put(OpcDaFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()) - .put(OpcDaFields.TAG_NAME, opcData.getTag()) - .put(OpcDaFields.QUALITY, opcData.getQuality().name()) - .put(OpcDaFields.UPDATE_PERIOD, meta.refreshPeriodMillis) - .put(OpcDaFields.TAG_GROUP, meta.group) - .put(OpcDaFields.OPC_SERVER_HOST, host) - .put(OpcDaFields.OPC_SERVER_DOMAIN, domain); - - if (tmp.value() != null) { - value = value.put(OpcDaFields.VALUE, tmp.value()); - } - if (opcData.getOperationStatus().getLevel().compareTo(OperationStatus.Level.INFO) > 0) { - value.put(OpcDaFields.ERROR_CODE, opcData.getOperationStatus().getCode()); - if (opcData.getOperationStatus().getMessageDetail().isPresent()) { - value.put(OpcDaFields.ERROR_REASON, - opcData.getOperationStatus().getMessageDetail().get()); - } - } + Map additionalInfo = new HashMap<>(); + additionalInfo.put(OpcRecordFields.OPC_SERVER_HOST, host); + additionalInfo.put(OpcRecordFields.OPC_SERVER_DOMAIN, domain); + Struct value = CommonUtils.mapToConnectObject(opcData, + meta, + valueSchema, + tmp, + additionalInfo); Map partition = new HashMap<>(); - partition.put(OpcDaFields.TAG_NAME, opcData.getTag()); - partition.put(OpcDaFields.OPC_SERVER_DOMAIN, domain); - partition.put(OpcDaFields.OPC_SERVER_HOST, host); + partition.put(OpcRecordFields.TAG_NAME, opcData.getTag()); + partition.put(OpcRecordFields.OPC_SERVER_DOMAIN, domain); + partition.put(OpcRecordFields.OPC_SERVER_HOST, host); return new SourceRecord( partition, - Collections.singletonMap(OpcDaFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()), + Collections.singletonMap(OpcRecordFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()), "", SchemaBuilder.STRING_SCHEMA, domain + "|" + host + "|" + opcData.getTag(), @@ -327,26 +252,5 @@ public String version() { return getClass().getPackage().getImplementationVersion(); } - /** - * GCD recursive version - * - * @param x dividend - * @param y divisor - * @return - */ - private static long gcdInternal(long x, long y) { - return (y == 0) ? x : gcdInternal(y, x % y); - } - - /** - * Great common divisor (An elegant way to do it with a lambda). - * - * @param numbers list of number - * @return the GCD. - */ - private static long gcd(long... numbers) { - return Arrays.stream(numbers).reduce(0, (x, y) -> (y == 0) ? x : gcdInternal(y, x % y)); - } - } diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java new file mode 100644 index 000000000..c38c1bbf8 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java @@ -0,0 +1,114 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc.ua; + +import com.hurence.logisland.connect.opc.CommonUtils; +import org.apache.kafka.common.config.ConfigDef; +import org.apache.kafka.common.config.ConfigException; +import org.apache.kafka.common.config.ConfigValue; +import org.apache.kafka.connect.connector.Task; +import org.apache.kafka.connect.source.SourceConnector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * OPC-UA source connector. + * + * @author amarziali + */ +public class OpcUaSourceConnector extends SourceConnector { + + private static final Logger logger = LoggerFactory.getLogger(OpcUaSourceConnector.class); + + private Map configValues; + + public static final String PROPERTY_URI = "uri"; + public static final String PROPERTY_AUTH_USER = "auth.user"; + public static final String PROPERTY_AUTH_PASSWORD = "auth.password"; + public static final String PROPERTY_TAGS = "tags"; + public static final String PROPERTY_SOCKET_TIMEOUT = "socketTimeoutMillis"; + public static final String PROPERTY_DEFAULT_REFRESH_PERIOD = "defaultRefreshPeriodMillis"; + public static final String PROPERTY_DATA_PUBLICATION_PERIOD = "dataPublicationPeriodMillis"; + + + /** + * The configuration. + */ + private static final ConfigDef CONFIG = new ConfigDef() + .define(PROPERTY_URI, ConfigDef.Type.STRING, ConfigDef.Importance.HIGH, "The OPC-UA server uri") + .define(PROPERTY_AUTH_USER, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "The logon user") + .define(PROPERTY_AUTH_PASSWORD, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "The logon password") + .define(PROPERTY_TAGS, ConfigDef.Type.LIST, Collections.emptyList(), (name, value) -> { + if (value == null) { + throw new ConfigException("Cannot be null"); + } + List list = (List) value; + for (String s : list) { + if (CommonUtils.validateTagFormat(s)) { + throw new ConfigException("Tag list should be like [tag_name]:[refresh_period_millis] with optional refresh period"); + } + } + }, ConfigDef.Importance.HIGH, "The tags to subscribe to following format tagname:refresh_period_millis. E.g. myTag:1000") + .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, ConfigDef.Importance.LOW, "The socket timeout") + .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") + .define(PROPERTY_DATA_PUBLICATION_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The data publication window in milliseconds"); + + + @Override + public String version() { + return getClass().getPackage().getImplementationVersion(); + } + + @Override + public void start(Map props) { + //shallow copy + configValues = config().validate(props).stream().collect(Collectors.toMap(ConfigValue::name, Function.identity())); + logger.info("Starting OPC-UA connector (version {}) on server {} reading tags {}", version(), + configValues.get(PROPERTY_URI).value(), configValues.get(PROPERTY_TAGS).value()); + } + + @Override + public Class taskClass() { + return OpcUaSourceTask.class; + } + + @Override + public List> taskConfigs(int maxTasks) { + Map ret = configValues.entrySet().stream() + .filter(a -> a.getValue().value() != null) + .collect(Collectors.toMap(a -> a.getKey(), a -> a.getValue().value().toString())); + ret.put(PROPERTY_TAGS, ((List) configValues.get(PROPERTY_TAGS).value()).stream().collect(Collectors.joining(","))); + return Collections.singletonList(ret); + } + + @Override + public void stop() { + logger.info("Stopping OPC-UA connector (version {}) on server {}", version(), configValues.get(PROPERTY_URI).value()); + } + + @Override + public ConfigDef config() { + return CONFIG; + } +} diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java new file mode 100644 index 000000000..ed8ddf190 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java @@ -0,0 +1,190 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc.ua; + +import com.hurence.logisland.connect.opc.CommonUtils; +import com.hurence.logisland.connect.opc.OpcRecordFields; +import com.hurence.logisland.connect.opc.SmartOpcOperations; +import com.hurence.logisland.connect.opc.TagInfo; +import com.hurence.logisland.connect.opc.da.OpcDaSourceConnector; +import com.hurence.opc.OpcTagInfo; +import com.hurence.opc.auth.Credentials; +import com.hurence.opc.auth.UsernamePasswordCredentials; +import com.hurence.opc.ua.OpcUaConnectionProfile; +import com.hurence.opc.ua.OpcUaSession; +import com.hurence.opc.ua.OpcUaSessionProfile; +import com.hurence.opc.ua.OpcUaTemplate; +import org.apache.kafka.connect.data.Schema; +import org.apache.kafka.connect.data.SchemaAndValue; +import org.apache.kafka.connect.data.SchemaBuilder; +import org.apache.kafka.connect.data.Struct; +import org.apache.kafka.connect.errors.ConnectException; +import org.apache.kafka.connect.source.SourceRecord; +import org.apache.kafka.connect.source.SourceTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * OPC-UA source task. + * + * @author amarziali + */ +public class OpcUaSourceTask extends SourceTask { + + private static final Logger logger = LoggerFactory.getLogger(OpcUaSourceTask.class); + + private SmartOpcOperations opcOperations; + private TransferQueue transferQueue; + private String tags[]; + private URI serverUri; + private long defaultRefreshPeriodMillis; + private long defaultPublicationPeriodMillis; + private Map tagInfoMap; + private ExecutorService executorService; + private volatile boolean running; + + + private OpcUaConnectionProfile propertiesToConnectionProfile(Map properties) { + OpcUaConnectionProfile ret = new OpcUaConnectionProfile(); + ret.setConnectionUri(URI.create(properties.get(OpcUaSourceConnector.PROPERTY_URI))); + if (properties.containsKey(OpcUaSourceConnector.PROPERTY_AUTH_USER)) { + ret.setCredentials(new UsernamePasswordCredentials() + .withUser(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_USER)) + .withPassword(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_PASSWORD))); + } else { + ret.setCredentials(Credentials.ANONYMOUS_CREDENTIALS); + } + if (properties.containsKey(OpcUaSourceConnector.PROPERTY_SOCKET_TIMEOUT)) { + ret.setSocketTimeout(Duration.ofMillis(Long.parseLong(properties.get(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT)))); + } + return ret; + } + + + @Override + public void start(Map props) { + transferQueue = new LinkedTransferQueue<>(); + opcOperations = new SmartOpcOperations<>(new OpcUaTemplate()); + OpcUaConnectionProfile connectionProfile = propertiesToConnectionProfile(props); + tags = props.get(OpcDaSourceConnector.PROPERTY_TAGS).split(","); + serverUri = connectionProfile.getConnectionUri(); + defaultRefreshPeriodMillis = Long.parseLong(props.get(OpcUaSourceConnector.PROPERTY_DEFAULT_REFRESH_PERIOD)); + defaultPublicationPeriodMillis = Long.parseLong(props.get(OpcUaSourceConnector.PROPERTY_DATA_PUBLICATION_PERIOD)); + + opcOperations.connect(connectionProfile); + if (!opcOperations.awaitConnected()) { + throw new ConnectException("Unable to connect"); + } + Map dictionary = + opcOperations.fetchMetadata(Arrays.stream(tags).map(t -> CommonUtils.parseTag(t, defaultRefreshPeriodMillis).getKey()) + .toArray(size -> new String[size])) + .stream() + .collect(Collectors.toMap(OpcTagInfo::getId, Function.identity())); + tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis, dictionary)) + .collect(Collectors.toMap(t -> t.getTagInfo().getId(), Function.identity())); + executorService = Executors.newSingleThreadExecutor(); + running = true; + executorService.submit(() -> { + final OpcUaSessionProfile sessionProfile = new OpcUaSessionProfile() + .withDefaultPollingInterval(Duration.ofMillis(defaultRefreshPeriodMillis)) + .withRefreshPeriod(Duration.ofMillis(defaultPublicationPeriodMillis)); + tagInfoMap.forEach((n, t) -> sessionProfile.addToPollingMap(t.getTagInfo().getId(), Duration.ofMillis(t.getRefreshPeriodMillis()))); + while (running) { + try (OpcUaSession session = opcOperations.createSession(sessionProfile)) { + session.stream(tagInfoMap.keySet().toArray(new String[tagInfoMap.size()])) + .map(opcData -> { + TagInfo meta = tagInfoMap.get(opcData.getTag()); + SchemaAndValue dataSchema = CommonUtils.convertToNativeType(opcData.getValue()); + Schema valueSchema = CommonUtils.buildSchema(dataSchema.schema()); + Struct valueStruct = CommonUtils.mapToConnectObject(opcData, + meta, + valueSchema, + dataSchema, + Collections.singletonMap(OpcRecordFields.OPC_SERVER_HOST, serverUri.toASCIIString())); + + + Map partition = new HashMap<>(); + partition.put(OpcRecordFields.TAG_NAME, opcData.getTag()); + partition.put(OpcRecordFields.OPC_SERVER_HOST, serverUri.toASCIIString()); + + return new SourceRecord( + partition, + Collections.singletonMap(OpcRecordFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()), + "", + SchemaBuilder.STRING_SCHEMA, + serverUri.toASCIIString() + "|" + opcData.getTag(), + valueSchema, + valueStruct); + } + ) + .forEach(transferQueue::add); + + } catch (Exception e) { + logger.error("Unexpected exception while streaming tags. Looping again.", e); + } + } + logger.info("OPC-UA reading loop ended."); + }); + logger.info("Started OPC-UA task for tags {}", (Object) tags); + } + + @Override + public List poll() throws InterruptedException { + List ret = new ArrayList<>(); + if (transferQueue.isEmpty()) { + Thread.sleep(defaultPublicationPeriodMillis); + } + transferQueue.drainTo(ret); + return ret; + } + + @Override + public void stop() { + running = false; + //session are automatically cleaned up and detached when the connection is closed. + + if (opcOperations != null) { + opcOperations.disconnect(); + opcOperations.awaitDisconnected(); + } + + if (executorService != null) { + executorService.shutdown(); + executorService = null; + } + transferQueue = null; + tagInfoMap = null; + logger.info("Stopped OPC-UA task for tags {}", (Object) tags); + } + + @Override + public String version() { + return getClass().getPackage().getImplementationVersion(); + } + +} \ No newline at end of file diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java similarity index 94% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java index f186c6570..2f8a72ae9 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java @@ -15,21 +15,20 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc.da; import com.google.gson.Gson; +import com.hurence.logisland.connect.opc.CommonUtils; import com.hurence.opc.OpcTagInfo; import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; import com.hurence.opc.da.OpcDaOperations; import com.hurence.opc.da.OpcDaTemplate; -import org.apache.kafka.connect.source.SourceRecord; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import java.net.URI; -import java.sql.Struct; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.*; @@ -43,16 +42,16 @@ public class OpcDaSourceConnectorTest { @Test(expected = IllegalArgumentException.class) public void parseFailureTest() { - OpcDaSourceConnector.parseTag("test1:2aj", 500L); + CommonUtils.parseTag("test1:2aj", 500L); } @Test public void tagParseTest() { - Map.Entry toTest = OpcDaSourceConnector.parseTag("test1:1000", 500L); + Map.Entry toTest = CommonUtils.parseTag("test1:1000", 500L); Assert.assertEquals("test1", toTest.getKey()); Assert.assertEquals(new Long(1000), toTest.getValue()); - toTest = OpcDaSourceConnector.parseTag("test2", 500L); + toTest = CommonUtils.parseTag("test2", 500L); Assert.assertEquals("test2", toTest.getKey()); Assert.assertEquals(new Long(500), toTest.getValue()); } diff --git a/logisland-plugins/logisland-common-processors-plugin/pom.xml b/logisland-plugins/logisland-common-processors-plugin/pom.xml index 46014c2cd..019bd94d9 100644 --- a/logisland-plugins/logisland-common-processors-plugin/pom.xml +++ b/logisland-plugins/logisland-common-processors-plugin/pom.xml @@ -90,7 +90,7 @@ org.javadelight delight-nashorn-sandbox - 0.1.14 + 0.1.15 diff --git a/pom.xml b/pom.xml index 03de3b3b3..e7dcb5574 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,9 @@ jitpack.io https://jitpack.io + + true + openscada @@ -1281,7 +1284,7 @@ com.hurence.logisland - logisland-connector-opcda + logisland-connector-opc ${project.version} From 8a548c59c22888eca56d781246c64522edf5d930 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 27 Jun 2018 14:42:11 +0200 Subject: [PATCH 36/63] Improve nashorn processor performance by using a pluggable reusable cache --- .../pom.xml | 2 +- .../AbstractNashornSandboxProcessor.java | 37 ++++++++-- .../processor/alerting/ComputeTagsTest.java | 70 ++++++++++++++++++- 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/logisland-plugins/logisland-common-processors-plugin/pom.xml b/logisland-plugins/logisland-common-processors-plugin/pom.xml index 46014c2cd..019bd94d9 100644 --- a/logisland-plugins/logisland-common-processors-plugin/pom.xml +++ b/logisland-plugins/logisland-common-processors-plugin/pom.xml @@ -90,7 +90,7 @@ org.javadelight delight-nashorn-sandbox - 0.1.14 + 0.1.15 diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java index 03f6bbec6..bc790f304 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java @@ -23,6 +23,7 @@ import com.hurence.logisland.processor.AbstractProcessor; import com.hurence.logisland.processor.ProcessContext; import com.hurence.logisland.record.*; +import com.hurence.logisland.service.cache.CacheService; import com.hurence.logisland.service.datastore.DatastoreClientService; import com.hurence.logisland.validator.StandardValidators; import delight.nashornsandbox.NashornSandbox; @@ -30,10 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.Executors; @Tags({"record", "fields", "Add"}) @@ -45,9 +43,12 @@ description = "Add a field to the record with the default value") public abstract class AbstractNashornSandboxProcessor extends AbstractProcessor { - private static final Logger logger = LoggerFactory.getLogger(AbstractNashornSandboxProcessor.class); + /** + * Default storage for Nashorn js sandbox. + */ + private static final Map DEFAULT_JS_STORAGE = Collections.synchronizedMap(new HashMap<>()); public static final PropertyDescriptor MAX_CPU_TIME = new PropertyDescriptor.Builder() .name("max.cpu.time") @@ -142,6 +143,14 @@ public abstract class AbstractNashornSandboxProcessor extends AbstractProcessor .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .build(); + public static final PropertyDescriptor JS_CACHE_SERVICE = new PropertyDescriptor.Builder() + .name("js.cache.service") + .description("The cache service to be used to store already sanitized JS expressions. " + + "If not specified a in-memory unlimited hash map will be used.") + .required(false) + .identifiesControllerService(CacheService.class) + .build(); + public static final PropertyDescriptor OUTPUT_RECORD_TYPE = new PropertyDescriptor.Builder() .name("output.record.type") @@ -165,6 +174,7 @@ public List getSupportedPropertyDescriptors() { properties.add(MAX_PREPARED_STATEMENTS); properties.add(DATASTORE_CLIENT_SERVICE); properties.add(DATASTORE_CACHE_COLLECTION); + properties.add(JS_CACHE_SERVICE); properties.add(OUTPUT_RECORD_TYPE); return properties; @@ -197,6 +207,23 @@ public void init(ProcessContext context) { super.init(context); sandbox = NashornSandboxes.create(); + CacheService cacheService = context.getPropertyValue(JS_CACHE_SERVICE).asControllerService(CacheService.class); + + //inject the right cache service (or the default one). + if (cacheService != null) { + sandbox.setScriptCache(((js, allowNoBraces, producer) -> { + String ret = cacheService.get(js); + if (ret == null) { + ret = producer.get(); + cacheService.set(js, ret); + } + return ret; + })); + } else { + sandbox.setScriptCache((js, allowNoBraces, producer) -> + DEFAULT_JS_STORAGE.computeIfAbsent(js, s -> producer.get())); + } + Long maxCpuTime = context.getPropertyValue(MAX_CPU_TIME).asLong(); Long maxMemory = context.getPropertyValue(MAX_MEMORY).asLong(); Boolean allowNoBrace = context.getPropertyValue(ALLOw_NO_BRACE).asBoolean(); diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index 3e0c9227a..4beb4a0a5 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -17,23 +17,91 @@ package com.hurence.logisland.processor.alerting; import com.hurence.logisland.component.InitializationException; +import com.hurence.logisland.controller.ControllerServiceInitializationContext; import com.hurence.logisland.processor.datastore.MockDatastoreService; import com.hurence.logisland.record.FieldDictionary; import com.hurence.logisland.record.FieldType; import com.hurence.logisland.record.Record; import com.hurence.logisland.record.StandardRecord; +import com.hurence.logisland.service.cache.CacheService; +import com.hurence.logisland.service.cache.LRUKeyValueCacheService; +import com.hurence.logisland.service.cache.model.Cache; +import com.hurence.logisland.service.cache.model.LRUCache; import com.hurence.logisland.service.datastore.DatastoreClientService; import com.hurence.logisland.util.runner.TestRunner; import com.hurence.logisland.util.runner.TestRunners; +import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class ComputeTagsTest { + @Test + public void testWithCustomCacheService() throws InitializationException { + final DatastoreClientService service = new MockDatastoreService(); + getCacheRecords().forEach(r -> service.put("test", r, false)); + + + final CacheService cacheService = new LRUKeyValueCacheService() { + private final int cacheSize = 10; + + @Override + protected Cache createCache(ControllerServiceInitializationContext context) { + return new LRUCache<>(cacheSize); + } + }; + + final TestRunner runner = TestRunners.newTestRunner(ComputeTags.class); + runner.setProperty(ComputeTags.MAX_CPU_TIME, "100"); + runner.setProperty(ComputeTags.MAX_MEMORY, "12800000"); + runner.setProperty(ComputeTags.MAX_PREPARED_STATEMENTS, "100"); + runner.setProperty(ComputeTags.ALLOw_NO_BRACE, "false"); + runner.setProperty("cvib3", "return 37.2/10*3;"); + runner.setProperty(ComputeTags.DATASTORE_CLIENT_SERVICE, service.getIdentifier()); + runner.setProperty(ComputeTags.JS_CACHE_SERVICE, "js_cache"); + + + runner.addControllerService(service.getIdentifier(), service); + runner.enableControllerService(service); + + runner.addControllerService("js_cache", cacheService); + runner.enableControllerService(cacheService); + + final DatastoreClientService lookupService = runner.getProcessContext() + .getPropertyValue(ComputeTags.DATASTORE_CLIENT_SERVICE) + .asControllerService(MockDatastoreService.class); + + Assert.assertNotNull(lookupService); + Assert.assertNotNull(runner.getProcessContext() + .getPropertyValue(ComputeTags.JS_CACHE_SERVICE) + .asControllerService(CacheService.class)); + + + Collection recordsToEnrich = getRecords(); + runner.assertValid(); + runner.enqueue(recordsToEnrich); + runner.run(); + runner.assertAllInputRecordsProcessed(); + runner.getErrorRecords().forEach(System.err::println); + runner.assertOutputRecordsCount(3 + 1); + runner.assertOutputErrorCount(0); + + boolean asserted = false; + + for (Record enriched : runner.getOutputRecords()) { + if (enriched.getId().equals("cvib3")) { + assertEquals(enriched.getField(FieldDictionary.RECORD_VALUE).asDouble(), 37.2 / 10.0 * 3.0, 0.00001); + asserted = true; + } + } + assertTrue(asserted); + } + @Test public void testMultipleRules() throws InitializationException { @@ -62,7 +130,7 @@ public void testMultipleRules() throws InitializationException { runner.enqueue(recordsToEnrich); runner.run(); runner.assertAllInputRecordsProcessed(); - runner.assertOutputRecordsCount(3+3); + runner.assertOutputRecordsCount(3 + 3); runner.assertOutputErrorCount(0); for (Record enriched : runner.getOutputRecords()) { From afae192f88d691a5c2b4dbf6c2fc1cb58a1b8e00 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 20 Jun 2018 10:29:39 +0200 Subject: [PATCH 37/63] Upgrade opc-simple API --- .../pom.xml | 10 +++- .../logisland/connect/opcda/OpcDaFields.java | 5 ++ .../connect/opcda/OpcDaSourceConnector.java | 0 .../connect/opcda/OpcDaSourceTask.java | 46 +++++++++------- .../connect/opcda/SmartOpcOperations.java | 55 ++++++++++++++++--- .../opcda/OpcDaSourceConnectorTest.java | 24 +++++--- .../logisland-connectors/pom.xml | 2 +- 7 files changed, 103 insertions(+), 39 deletions(-) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/pom.xml (83%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java (94%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java (100%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java (89%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java (64%) rename logisland-connect/logisland-connectors/{logisland-connector-opcda => logisland-connector-opc}/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java (87%) diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml similarity index 83% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/pom.xml rename to logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index bfaae5149..b0e48264f 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -10,19 +10,25 @@ jar - logisland-connector-opcda + logisland-connector-opc com.github.Hurence opc-simple - 1.1.2 + develop-1.1.2-g1929139-18 com.hurence.logisland logisland-api + + javax.ws.rs + javax.ws.rs-api + 2.1 + + diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java similarity index 94% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java index 34d8f0cd9..445fc48fd 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java @@ -46,6 +46,11 @@ public interface OpcDaFields { * The OPC server error code in case the tag reading is in error. */ String ERROR_CODE = "error_code"; + + /** + * The error reason (as string description) + */ + String ERROR_REASON = "error_reason"; /** * The OPC server host generating the event. */ diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java similarity index 100% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java similarity index 89% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java index 1f0d67109..f28d0c4d6 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java @@ -17,10 +17,12 @@ package com.hurence.logisland.connect.opcda; +import com.hurence.opc.OperationStatus; +import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; -import com.hurence.opc.da.OpcDaOperations; import com.hurence.opc.da.OpcDaSession; import com.hurence.opc.da.OpcDaSessionProfile; +import com.hurence.opc.da.OpcDaTemplate; import org.apache.kafka.connect.data.Schema; import org.apache.kafka.connect.data.SchemaAndValue; import org.apache.kafka.connect.data.SchemaBuilder; @@ -33,6 +35,7 @@ import org.slf4j.LoggerFactory; import java.math.BigDecimal; +import java.net.URI; import java.time.Duration; import java.time.Instant; import java.util.*; @@ -76,12 +79,11 @@ public TagInfo(String raw, long defaultRefreshPeriod) { private Map tagInfoMap; private Set tagReadingQueue; private ScheduledExecutorService executorService; - private String host; private String domain; + private String host; private boolean directRead; private long defaultRefreshPeriodMillis; private long minWaitTime; - private volatile boolean running = false; private synchronized void createSessionsIfNeeded() { @@ -90,7 +92,7 @@ private synchronized void createSessionsIfNeeded() { tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().refreshPeriodMillis)) .forEach((a, b) -> { OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile().withDirectRead(directRead) - .withRefreshPeriodMillis(a); + .withRefreshPeriod(Duration.ofMillis(a)); OpcDaSession session = opcOperations.createSession(sessionProfile); b.forEach(c -> sessions.put(c.getKey(), session)); }); @@ -100,14 +102,17 @@ private synchronized void createSessionsIfNeeded() { private OpcDaConnectionProfile propertiesToConnectionProfile(Map properties) { OpcDaConnectionProfile ret = new OpcDaConnectionProfile(); - ret.setHost(properties.get(OpcDaSourceConnector.PROPERTY_HOST)); + StringBuilder uri = new StringBuilder(String.format("opc.da://%s", + properties.get(OpcDaSourceConnector.PROPERTY_HOST))); ret.setComClsId(properties.get(OpcDaSourceConnector.PROPERTY_CLSID)); ret.setComProgId(properties.get(OpcDaSourceConnector.PROPERTY_PROGID)); if (properties.containsKey(OpcDaSourceConnector.PROPERTY_PORT)) { - ret.setPort(Integer.parseInt(properties.get(OpcDaSourceConnector.PROPERTY_PORT))); + uri.append(String.format(":%d", Integer.parseInt(properties.get(OpcDaSourceConnector.PROPERTY_PORT)))); } - ret.setUser(properties.get(OpcDaSourceConnector.PROPERTY_USER)); - ret.setPassword(properties.get(OpcDaSourceConnector.PROPERTY_PASSWORD)); + ret.setConnectionUri(URI.create(uri.toString())); + ret.setCredentials(new UsernamePasswordCredentials() + .withUser(properties.get(OpcDaSourceConnector.PROPERTY_USER)) + .withPassword(properties.get(OpcDaSourceConnector.PROPERTY_PASSWORD))); ret.setDomain(properties.get(OpcDaSourceConnector.PROPERTY_DOMAIN)); if (properties.containsKey(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT)) { @@ -173,16 +178,15 @@ private Schema buildSchema(Schema valueSchema) { SchemaBuilder ret = SchemaBuilder.struct() .field(OpcDaFields.TAG_NAME, SchemaBuilder.string()) .field(OpcDaFields.TIMESTAMP, SchemaBuilder.int64()) - .field(OpcDaFields.QUALITY, SchemaBuilder.int32().optional()) + .field(OpcDaFields.QUALITY, SchemaBuilder.string()) .field(OpcDaFields.UPDATE_PERIOD, SchemaBuilder.int64().optional()) .field(OpcDaFields.TAG_GROUP, SchemaBuilder.string().optional()) .field(OpcDaFields.OPC_SERVER_DOMAIN, SchemaBuilder.string().optional()) - .field(OpcDaFields.OPC_SERVER_HOST, SchemaBuilder.string()); - + .field(OpcDaFields.OPC_SERVER_HOST, SchemaBuilder.string()) + .field(OpcDaFields.ERROR_CODE, SchemaBuilder.int64().optional()) + .field(OpcDaFields.ERROR_REASON, SchemaBuilder.string().optional()); if (valueSchema != null) { ret = ret.field(OpcDaFields.VALUE, valueSchema); - } else { - ret = ret.field(OpcDaFields.ERROR_CODE, SchemaBuilder.int32().optional()); } return ret; } @@ -191,11 +195,11 @@ private Schema buildSchema(Schema valueSchema) { @Override public void start(Map props) { transferQueue = new LinkedTransferQueue<>(); - opcOperations = new SmartOpcOperations<>(new OpcDaOperations()); + opcOperations = new SmartOpcOperations<>(new OpcDaTemplate()); OpcDaConnectionProfile connectionProfile = propertiesToConnectionProfile(props); tags = props.get(OpcDaSourceConnector.PROPERTY_TAGS).split(","); - host = connectionProfile.getHost(); domain = connectionProfile.getDomain() != null ? connectionProfile.getDomain() : ""; + host = connectionProfile.getConnectionUri().getHost(); defaultRefreshPeriodMillis = Long.parseLong(props.get(OpcDaSourceConnector.PROPERTY_DEFAULT_REFRESH_PERIOD)); directRead = Boolean.parseBoolean(props.get(OpcDaSourceConnector.PROPERTY_DIRECT_READ)); tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis)) @@ -207,7 +211,6 @@ public void start(Map props) { logger.info("Started OPC-DA task for tags {}", (Object) tags); minWaitTime = Math.max(10, gcd(tagInfoMap.values().stream().mapToLong(t -> t.refreshPeriodMillis).toArray())); tagReadingQueue = new HashSet<>(); - running = true; executorService = Executors.newSingleThreadScheduledExecutor(); tagInfoMap.forEach((k, v) -> executorService.scheduleAtFixedRate(() -> { try { @@ -245,7 +248,7 @@ public void start(Map props) { Struct value = new Struct(valueSchema) .put(OpcDaFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()) .put(OpcDaFields.TAG_NAME, opcData.getTag()) - .put(OpcDaFields.QUALITY, opcData.getQuality()) + .put(OpcDaFields.QUALITY, opcData.getQuality().name()) .put(OpcDaFields.UPDATE_PERIOD, meta.refreshPeriodMillis) .put(OpcDaFields.TAG_GROUP, meta.group) .put(OpcDaFields.OPC_SERVER_HOST, host) @@ -254,8 +257,12 @@ public void start(Map props) { if (tmp.value() != null) { value = value.put(OpcDaFields.VALUE, tmp.value()); } - if (opcData.getErrorCode().isPresent()) { - value.put(OpcDaFields.ERROR_CODE, opcData.getErrorCode().get()); + if (opcData.getOperationStatus().getLevel().compareTo(OperationStatus.Level.INFO) > 0) { + value.put(OpcDaFields.ERROR_CODE, opcData.getOperationStatus().getCode()); + if (opcData.getOperationStatus().getMessageDetail().isPresent()) { + value.put(OpcDaFields.ERROR_REASON, + opcData.getOperationStatus().getMessageDetail().get()); + } } Map partition = new HashMap<>(); @@ -298,7 +305,6 @@ public List poll() throws InterruptedException { @Override public void stop() { - running = false; if (executorService != null) { executorService.shutdown(); executorService = null; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java similarity index 64% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java index c67f7190b..8b091614f 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java @@ -17,12 +17,10 @@ package com.hurence.logisland.connect.opcda; -import com.hurence.opc.ConnectionProfile; -import com.hurence.opc.OpcOperations; -import com.hurence.opc.OpcSession; -import com.hurence.opc.SessionProfile; +import com.hurence.opc.*; import com.hurence.opc.util.AutoReconnectOpcOperations; +import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -33,9 +31,10 @@ * @author amarziali */ public class SmartOpcOperations, T extends SessionProfile, U extends OpcSession> - extends AutoReconnectOpcOperations { + implements OpcOperations { private final AtomicBoolean stale = new AtomicBoolean(); + private final OpcOperations delegate; /** * Construct an instance. @@ -43,19 +42,19 @@ public class SmartOpcOperations, T extends Sessio * @param delegate the deletegate {@link OpcOperations}. */ public SmartOpcOperations(OpcOperations delegate) { - super(delegate); + this.delegate = AutoReconnectOpcOperations.create(delegate); } @Override public void connect(S connectionProfile) { stale.set(true); - super.connect(connectionProfile); + delegate.connect(connectionProfile); awaitConnected(); } @Override public void disconnect() { - super.disconnect(); + delegate.disconnect(); } /** @@ -68,6 +67,46 @@ public synchronized boolean resetStale() { return stale.getAndSet(false); } + @Override + public boolean isChannelSecured() { + return false; + } + + @Override + public ConnectionState getConnectionState() { + return delegate.getConnectionState(); + } + + @Override + public Collection browseTags() { + return delegate.browseTags(); + } + + @Override + public U createSession(T t) { + return delegate.createSession(t); + } + + @Override + public void releaseSession(U u) { + delegate.releaseSession(u); + } + + @Override + public boolean awaitConnected() { + return delegate.awaitConnected(); + } + + @Override + public boolean awaitDisconnected() { + return delegate.awaitDisconnected(); + } + + @Override + public void close() throws Exception { + delegate.close(); + } + @Override public String toString() { return "SmartOpcOperations{" + diff --git a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java similarity index 87% rename from logisland-connect/logisland-connectors/logisland-connector-opcda/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java index afe0c215e..f186c6570 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opcda/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java @@ -17,13 +17,19 @@ package com.hurence.logisland.connect.opcda; +import com.google.gson.Gson; import com.hurence.opc.OpcTagInfo; +import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; import com.hurence.opc.da.OpcDaOperations; +import com.hurence.opc.da.OpcDaTemplate; +import org.apache.kafka.connect.source.SourceRecord; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import java.net.URI; +import java.sql.Struct; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.*; @@ -81,7 +87,7 @@ public void e2eTest() throws Exception { properties.put(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT, "2000"); properties.put(OpcDaSourceConnector.PROPERTY_PASSWORD, "opc"); properties.put(OpcDaSourceConnector.PROPERTY_USER, "OPC"); - properties.put(OpcDaSourceConnector.PROPERTY_HOST, "192.168.56.101"); + properties.put(OpcDaSourceConnector.PROPERTY_HOST, "192.168.99.100"); properties.put(OpcDaSourceConnector.PROPERTY_CLSID, "F8582CF2-88FB-11D0-B850-00C0F0104305"); properties.put(OpcDaSourceConnector.PROPERTY_TAGS, listAllTags().stream() .map(s -> s + ":" + atomicInteger.getAndAdd(r.nextInt(130))) @@ -92,9 +98,10 @@ public void e2eTest() throws Exception { OpcDaSourceTask task = new OpcDaSourceTask(); task.start(connector.taskConfigs(1).get(0)); ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor(); + Gson json = new Gson(); es.scheduleAtFixedRate(() -> { try { - task.poll().forEach(System.out::println); + System.err.println(json.toJson(task.poll())); } catch (InterruptedException e) { //do nothing } @@ -111,19 +118,20 @@ private Collection listAllTags() throws Exception { OpcDaConnectionProfile connectionProfile = new OpcDaConnectionProfile() .withComClsId("F8582CF2-88FB-11D0-B850-00C0F0104305") .withDomain("OPC-9167C0D9342") - .withUser("OPC") - .withPassword("opc") - .withHost("192.168.56.101") - .withSocketTimeout(Duration.of(1, ChronoUnit.SECONDS)); + .withCredentials(new UsernamePasswordCredentials() + .withUser("OPC") + .withPassword("opc")) + .withConnectionUri(URI.create("opc.da://192.168.99.100")) + .withSocketTimeout(Duration.of(10, ChronoUnit.SECONDS)); //Create an instance of a da operations - try (OpcDaOperations opcDaOperations = new OpcDaOperations()) { + try (OpcDaOperations opcDaOperations = new OpcDaTemplate()) { //connect using our profile opcDaOperations.connect(connectionProfile); if (!opcDaOperations.awaitConnected()) { throw new IllegalStateException("Unable to connect"); } - return opcDaOperations.browseTags().stream().map(OpcTagInfo::getName).collect(Collectors.toList()); + return opcDaOperations.browseTags().stream().map(OpcTagInfo::getId).collect(Collectors.toList()); } } diff --git a/logisland-connect/logisland-connectors/pom.xml b/logisland-connect/logisland-connectors/pom.xml index 9232401bf..1bcb8692f 100644 --- a/logisland-connect/logisland-connectors/pom.xml +++ b/logisland-connect/logisland-connectors/pom.xml @@ -18,7 +18,7 @@ - logisland-connector-opcda + logisland-connector-opc From 36e6ed050c9837307246bed4a83372631fd045a4 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 26 Jun 2018 14:54:30 +0200 Subject: [PATCH 38/63] Kafka connect: - Add timed source. - Fix minor bugs --- .../converter/LogIslandRecordConverter.java | 7 +- .../source/KafkaConnectStreamSource.java | 2 +- .../connect/source/SourceThread.java | 1 + .../source/timed/ClockSourceConnector.java | 71 +++++++++++++++++++ .../connect/source/timed/ClockSourceTask.java | 60 ++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java create mode 100644 logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java index 361b98c88..72d692026 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java @@ -144,11 +144,14 @@ private Field toFieldRecursive(String name, Schema schema, Object value, boolean } if (isKey) { Map ret = new HashMap<>(); - struct.schema().fields().forEach(field -> ret.put(field.name(), toFieldRecursive(field.name(), field.schema(), struct.get(field), true).getRawValue())); + struct.schema().fields().stream().filter(field -> !(field.schema().isOptional() && struct.get(field) == null)) + .forEach(field -> ret.put(field.name(), toFieldRecursive(field.name(), field.schema(), struct.get(field), true).getRawValue())); return new Field(name, FieldType.MAP, ret); } else { Record ret = new StandardRecord(); - struct.schema().fields().forEach(field -> ret.setField(toFieldRecursive(field.name(), field.schema(), struct.get(field), true))); + struct.schema().fields().stream() + .filter(field -> !(field.schema().isOptional() && struct.get(field) == null)) + .forEach(field -> ret.setField(toFieldRecursive(field.name(), field.schema(), struct.get(field), true))); return new Field(name, FieldType.RECORD, ret); } diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java index 449c234cf..2b8a5e517 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java @@ -157,7 +157,7 @@ public void raiseError(Exception e) { //create and start tasks startAllThreads(); - } catch (IllegalAccessException | InstantiationException e) { + } catch (Exception e) { try { stopAllThreads(); } catch (Throwable t) { diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java index 3dd370f60..4b7fbecfa 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java @@ -92,6 +92,7 @@ public SourceThread start() { } catch (Throwable tt) { //swallow } + throw t; } return this; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java new file mode 100644 index 000000000..5f0dd6664 --- /dev/null +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java @@ -0,0 +1,71 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.source.timed; + +import org.apache.kafka.common.config.ConfigDef; +import org.apache.kafka.connect.connector.Task; +import org.apache.kafka.connect.source.SourceConnector; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * A connector that emits an empty record at fixed rate waking up the processing pipeline. + * + * @author amarziali + */ +public class ClockSourceConnector extends SourceConnector { + + public static final String RATE = "rate"; + + private static final ConfigDef CONFIG = new ConfigDef() + .define(RATE, ConfigDef.Type.LONG, null, ConfigDef.Importance.HIGH, "The clock rate in milliseconds"); + + private long rate; + + + @Override + public String version() { + return "1.0"; + } + + @Override + public void start(Map props) { + rate = (Long) CONFIG.parse(props).get(RATE); + } + + @Override + public Class taskClass() { + return ClockSourceTask.class; + } + + @Override + public List> taskConfigs(int maxTasks) { + return Collections.singletonList(Collections.singletonMap(RATE, Long.toString(rate))); + } + + @Override + public void stop() { + } + + @Override + public ConfigDef config() { + return CONFIG; + } +} diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java new file mode 100644 index 000000000..83384d230 --- /dev/null +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java @@ -0,0 +1,60 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.source.timed; + +import org.apache.kafka.connect.data.Schema; +import org.apache.kafka.connect.source.SourceRecord; +import org.apache.kafka.connect.source.SourceTask; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * {@link SourceTask} for {@link ClockSourceConnector} + * + * @author amarziali + */ +public class ClockSourceTask extends SourceTask { + + private long rate; + + @Override + public void start(Map props) { + rate = Long.parseLong(props.get(ClockSourceConnector.RATE)); + + } + + @Override + public List poll() throws InterruptedException { + Thread.sleep(rate); + return Collections.singletonList(new SourceRecord(null, null, "", + Schema.STRING_SCHEMA, "")); + + } + + @Override + public void stop() { + + } + + @Override + public String version() { + return "1.0"; + } +} From 37c02df9617b54ebf803c07ab134b002362a45e5 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 26 Jun 2018 14:56:12 +0200 Subject: [PATCH 39/63] OPC-UA first implementation. --- .../logisland-connectors-bundle/pom.xml | 4 +- .../logisland-connector-opc/pom.xml | 23 +- .../logisland/connect/opc/CommonUtils.java | 151 +++++++++++++ .../OpcRecordFields.java} | 9 +- .../{opcda => opc}/SmartOpcOperations.java | 9 +- .../logisland/connect/opc/TagInfo.java | 46 ++++ .../da}/OpcDaSourceConnector.java | 26 +-- .../{opcda => opc/da}/OpcDaSourceTask.java | 198 +++++------------- .../connect/opc/ua/OpcUaSourceConnector.java | 114 ++++++++++ .../connect/opc/ua/OpcUaSourceTask.java | 190 +++++++++++++++++ .../da}/OpcDaSourceConnectorTest.java | 11 +- pom.xml | 5 +- 12 files changed, 607 insertions(+), 179 deletions(-) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda/OpcDaFields.java => opc/OpcRecordFields.java} (90%) rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda => opc}/SmartOpcOperations.java (93%) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda => opc/da}/OpcDaSourceConnector.java (87%) rename logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/{opcda => opc/da}/OpcDaSourceTask.java (58%) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java rename logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/{opcda => opc/da}/OpcDaSourceConnectorTest.java (94%) diff --git a/logisland-connect/logisland-connectors-bundle/pom.xml b/logisland-connect/logisland-connectors-bundle/pom.xml index a6a819001..268f1be28 100644 --- a/logisland-connect/logisland-connectors-bundle/pom.xml +++ b/logisland-connect/logisland-connectors-bundle/pom.xml @@ -14,7 +14,7 @@ - includeOpcDaConnector + includeOpcConnector true @@ -25,7 +25,7 @@ com.hurence.logisland - logisland-connector-opcda + logisland-connector-opc diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index b0e48264f..33f891817 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -16,13 +16,34 @@ com.github.Hurence opc-simple - develop-1.1.2-g1929139-18 + develop-1.1.2-g6c92e27-20 com.hurence.logisland logisland-api + + com.hurence.logisland + logisland-connect-spark + test + + + com.hurence.logisland + logisland-utils + test + + + org.apache.spark + spark-streaming-kafka-0-10_2.11 + test + + + org.apache.spark + spark-streaming_2.11 + test + + javax.ws.rs javax.ws.rs-api diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java new file mode 100644 index 000000000..a5322bfe5 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java @@ -0,0 +1,151 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc; + +import com.hurence.opc.OpcData; +import com.hurence.opc.OperationStatus; +import org.apache.kafka.connect.data.Schema; +import org.apache.kafka.connect.data.SchemaAndValue; +import org.apache.kafka.connect.data.SchemaBuilder; +import org.apache.kafka.connect.data.Struct; +import org.apache.kafka.connect.errors.SchemaBuilderException; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CommonUtils { + + + private static final Pattern TAG_FORMAT_MATCHER = Pattern.compile("^([^:]+)(:(\\d+))?$"); + + + public static boolean validateTagFormat(String tag) { + return TAG_FORMAT_MATCHER.matcher(tag).matches(); + } + + + public static Map.Entry parseTag(String t, Long defaultRefreshPeriod) { + Matcher matcher = TAG_FORMAT_MATCHER.matcher(t); + if (matcher.matches()) { + String tagName = matcher.group(1); + String refresh = matcher.groupCount() == 3 ? matcher.group(3) : null; + return new AbstractMap.SimpleEntry<>(tagName, refresh != null ? Long.parseLong(refresh) : defaultRefreshPeriod); + } + throw new IllegalArgumentException("" + t + " does not match"); + } + + public static SchemaAndValue convertToNativeType(final Object value) { + + Class cls = value != null ? value.getClass() : Void.class; + final ArrayList objs = new ArrayList<>(); + + if (cls.isArray()) { + final Object[] array = (Object[]) value; + + Schema arraySchema = null; + + for (final Object element : array) { + SchemaAndValue tmp = convertToNativeType(element); + if (arraySchema == null) { + arraySchema = tmp.schema(); + } + objs.add(tmp.value()); + } + + return new SchemaAndValue(SchemaBuilder.array(arraySchema), objs); + } + + if (cls.isAssignableFrom(Void.class)) { + return SchemaAndValue.NULL; + } else if (cls.isAssignableFrom(String.class)) { + return new SchemaAndValue(SchemaBuilder.string().optional(), value); + } else if (cls.isAssignableFrom(Short.class)) { + return new SchemaAndValue(SchemaBuilder.int16().optional(), value); + } else if (cls.isAssignableFrom(Integer.class)) { + + return new SchemaAndValue(SchemaBuilder.int32().optional(), value); + } else if (cls.isAssignableFrom(Long.class)) { + + return new SchemaAndValue(SchemaBuilder.int64().optional(), value); + } else if (cls.isAssignableFrom(Byte.class)) { + return new SchemaAndValue(SchemaBuilder.int8().optional(), value); + } else if (cls.isAssignableFrom(Character.class)) { + return new SchemaAndValue(SchemaBuilder.int32().optional(), value == null ? null : new Integer(((char) value))); + } else if (cls.isAssignableFrom(Boolean.class)) { + return new SchemaAndValue(SchemaBuilder.bool().optional(), value); + } else if (cls.isAssignableFrom(Float.class)) { + return new SchemaAndValue(SchemaBuilder.float32().optional(), value); + } else if (cls.isAssignableFrom(BigDecimal.class)) { + return new SchemaAndValue(SchemaBuilder.float64().optional(), value == null ? null : ((BigDecimal) value).doubleValue()); + } else if (cls.isAssignableFrom(Double.class)) { + return new SchemaAndValue(SchemaBuilder.float64().optional(), value); + } else if (cls.isAssignableFrom(Instant.class)) { + return new SchemaAndValue(SchemaBuilder.int64().optional(), value == null ? null : ((Instant) value).toEpochMilli()); + + } + throw new SchemaBuilderException("Unknown type presented (" + cls + ")"); + + } + + public static Schema buildSchema(Schema valueSchema) { + SchemaBuilder ret = SchemaBuilder.struct() + .field(OpcRecordFields.TAG_NAME, SchemaBuilder.string()) + .field(OpcRecordFields.TAG_ID, SchemaBuilder.string()) + .field(OpcRecordFields.TIMESTAMP, SchemaBuilder.int64()) + .field(OpcRecordFields.QUALITY, SchemaBuilder.string()) + .field(OpcRecordFields.UPDATE_PERIOD, SchemaBuilder.int64().optional()) + .field(OpcRecordFields.TAG_GROUP, SchemaBuilder.string().optional()) + .field(OpcRecordFields.OPC_SERVER_DOMAIN, SchemaBuilder.string().optional()) + .field(OpcRecordFields.OPC_SERVER_HOST, SchemaBuilder.string()) + .field(OpcRecordFields.ERROR_CODE, SchemaBuilder.int64().optional()) + .field(OpcRecordFields.ERROR_REASON, SchemaBuilder.string().optional()); + if (valueSchema != null) { + ret = ret.field(OpcRecordFields.VALUE, valueSchema); + } + return ret; + } + + public static Struct mapToConnectObject(OpcData opcData, TagInfo meta, Schema schema, SchemaAndValue valueSchema, Map additionalProps) { + Struct value = new Struct(schema) + .put(OpcRecordFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()) + .put(OpcRecordFields.TAG_ID, opcData.getTag()) + .put(OpcRecordFields.TAG_NAME, meta.getTagInfo().getName()) + .put(OpcRecordFields.QUALITY, opcData.getQuality().name()) + .put(OpcRecordFields.UPDATE_PERIOD, meta.getRefreshPeriodMillis()) + .put(OpcRecordFields.TAG_GROUP, meta.getTagInfo().getGroup()); + additionalProps.forEach(value::put); + + + if (valueSchema.value() != null) { + value = value.put(OpcRecordFields.VALUE, valueSchema.value()); + } + if (opcData.getOperationStatus().getLevel().compareTo(OperationStatus.Level.INFO) > 0) { + value.put(OpcRecordFields.ERROR_CODE, opcData.getOperationStatus().getCode()); + if (opcData.getOperationStatus().getMessageDetail().isPresent()) { + value.put(OpcRecordFields.ERROR_REASON, + opcData.getOperationStatus().getMessageDetail().get()); + } + } + return value; + } +} diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java similarity index 90% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java index 445fc48fd..28af3ca4d 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaFields.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java @@ -15,11 +15,11 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc; import com.hurence.logisland.record.FieldDictionary; -public interface OpcDaFields { +public interface OpcRecordFields { /** * The update period in milliseconds. @@ -33,6 +33,11 @@ public interface OpcDaFields { * The fully qualified tag name (with group). */ String TAG_NAME = FieldDictionary.RECORD_NAME; + + /** + * The internal tag id (depends to the implementation). + */ + String TAG_ID = "tag_id"; /** * The quality of the measurement (in case server caching is used). * The value is managed by the OPC server. diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java similarity index 93% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java index 8b091614f..c64746122 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/SmartOpcOperations.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java @@ -15,7 +15,7 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc; import com.hurence.opc.*; import com.hurence.opc.util.AutoReconnectOpcOperations; @@ -67,9 +67,14 @@ public synchronized boolean resetStale() { return stale.getAndSet(false); } + @Override + public Collection fetchMetadata(String... strings) { + return delegate.fetchMetadata(strings); + } + @Override public boolean isChannelSecured() { - return false; + return delegate.isChannelSecured(); } @Override diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java new file mode 100644 index 000000000..a0914c3fd --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java @@ -0,0 +1,46 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc; + +import com.hurence.opc.OpcTagInfo; +import org.apache.kafka.connect.errors.NotFoundException; + +import java.util.Map; + +public class TagInfo { + private final OpcTagInfo tagInfo; + private final Long refreshPeriodMillis; + + public TagInfo(String raw, long defaultRefreshPeriod, Map dictionary) { + Map.Entry parsed = CommonUtils.parseTag(raw, defaultRefreshPeriod); + String tag = parsed.getKey(); + this.refreshPeriodMillis = parsed.getValue(); + this.tagInfo = dictionary.get(tag); + if (tagInfo == null) { + throw new NotFoundException("Unable to find tag " + tag + " on selected server. Please check your configuration"); + } + } + + public Long getRefreshPeriodMillis() { + return refreshPeriodMillis; + } + + public OpcTagInfo getTagInfo() { + return tagInfo; + } +} diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java similarity index 87% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java index d8aaaf631..c6df698b6 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java @@ -15,8 +15,9 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc.da; +import com.hurence.logisland.connect.opc.CommonUtils; import org.apache.kafka.common.config.ConfigDef; import org.apache.kafka.common.config.ConfigException; import org.apache.kafka.common.config.ConfigValue; @@ -26,10 +27,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.function.Function; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -56,18 +58,6 @@ public class OpcDaSourceConnector extends SourceConnector { public static final String PROPERTY_DEFAULT_REFRESH_PERIOD = "defaultRefreshPeriodMillis"; public static final String PROPERTY_DIRECT_READ = "directReadFromDevice"; - private static final Pattern TAG_FORMAT_MATCHER = Pattern.compile("^([^:]+)(:(\\d+))?$"); - - public static Map.Entry parseTag(String t, Long defaultRefreshPeriod) { - Matcher matcher = TAG_FORMAT_MATCHER.matcher(t); - if (matcher.matches()) { - String tagName = matcher.group(1); - String refresh = matcher.groupCount() == 3 ? matcher.group(3) : null; - return new AbstractMap.SimpleEntry<>(tagName, refresh != null ? Long.parseLong(refresh) : defaultRefreshPeriod); - } - throw new IllegalArgumentException("" + t + " does not match"); - } - /** * The configuration. @@ -86,7 +76,7 @@ public static Map.Entry parseTag(String t, Long defaultRefreshPeri } List list = (List) value; for (String s : list) { - if (!TAG_FORMAT_MATCHER.matcher(s).matches()) { + if (!CommonUtils.validateTagFormat(s)) { throw new ConfigException("Tag list should be like [tag_name]:[refresh_period_millis] with optional refresh period"); } } @@ -119,7 +109,7 @@ public List> taskConfigs(int maxTasks) { Long defaultRefreshPeriod = (Long) configValues.get(PROPERTY_DEFAULT_REFRESH_PERIOD).value(); //first partition tags per refresh period Map> tagPartitions = ((List) configValues.get(PROPERTY_TAGS).value()) - .stream().collect(Collectors.groupingBy(tag -> parseTag(tag, defaultRefreshPeriod).getValue())); + .stream().collect(Collectors.groupingBy(tag -> CommonUtils.parseTag(tag, defaultRefreshPeriod).getValue())); List> tags = new ArrayList<>(tagPartitions.values()); int maxPartitions = Math.min(maxTasks, tags.size()); int batchSize = (int) Math.ceil((double) tags.size() / maxPartitions); diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java similarity index 58% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java index f28d0c4d6..10d223fd8 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opcda/OpcDaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java @@ -15,9 +15,13 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc.da; -import com.hurence.opc.OperationStatus; +import com.hurence.logisland.connect.opc.CommonUtils; +import com.hurence.logisland.connect.opc.OpcRecordFields; +import com.hurence.logisland.connect.opc.SmartOpcOperations; +import com.hurence.logisland.connect.opc.TagInfo; +import com.hurence.opc.OpcTagInfo; import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; import com.hurence.opc.da.OpcDaSession; @@ -28,16 +32,13 @@ import org.apache.kafka.connect.data.SchemaBuilder; import org.apache.kafka.connect.data.Struct; import org.apache.kafka.connect.errors.ConnectException; -import org.apache.kafka.connect.errors.SchemaBuilderException; import org.apache.kafka.connect.source.SourceRecord; import org.apache.kafka.connect.source.SourceTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.math.BigDecimal; import java.net.URI; import java.time.Duration; -import java.time.Instant; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.locks.Lock; @@ -50,27 +51,7 @@ */ public class OpcDaSourceTask extends SourceTask { - private static class TagInfo { - final String group; - final String name; - final Long refreshPeriodMillis; - - public TagInfo(String raw, long defaultRefreshPeriod) { - Map.Entry parsed = OpcDaSourceConnector.parseTag(raw, defaultRefreshPeriod); - String tag = parsed.getKey(); - this.refreshPeriodMillis = parsed.getValue(); - int idx = tag.lastIndexOf('.'); - if (idx > 0) { - this.group = tag.substring(0, idx); - } else { - this.group = ""; - } - this.name = tag; - } - } - private static final Logger logger = LoggerFactory.getLogger(OpcDaSourceTask.class); - private SmartOpcOperations opcOperations; private TransferQueue transferQueue; private Lock lock = new ReentrantLock(); @@ -85,11 +66,31 @@ public TagInfo(String raw, long defaultRefreshPeriod) { private long defaultRefreshPeriodMillis; private long minWaitTime; + /** + * GCD recursive version + * + * @param x dividend + * @param y divisor + * @return + */ + private static long gcdInternal(long x, long y) { + return (y == 0) ? x : gcdInternal(y, x % y); + } + + /** + * Great common divisor (An elegant way to do it with a lambda). + * + * @param numbers list of number + * @return the GCD. + */ + private static long gcd(long... numbers) { + return Arrays.stream(numbers).reduce(0, (x, y) -> (y == 0) ? x : gcdInternal(y, x % y)); + } private synchronized void createSessionsIfNeeded() { if (opcOperations != null && opcOperations.resetStale()) { sessions = new HashMap<>(); - tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().refreshPeriodMillis)) + tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().getRefreshPeriodMillis())) .forEach((a, b) -> { OpcDaSessionProfile sessionProfile = new OpcDaSessionProfile().withDirectRead(directRead) .withRefreshPeriod(Duration.ofMillis(a)); @@ -99,7 +100,6 @@ private synchronized void createSessionsIfNeeded() { } } - private OpcDaConnectionProfile propertiesToConnectionProfile(Map properties) { OpcDaConnectionProfile ret = new OpcDaConnectionProfile(); StringBuilder uri = new StringBuilder(String.format("opc.da://%s", @@ -121,76 +121,6 @@ private OpcDaConnectionProfile propertiesToConnectionProfile(Map return ret; } - private SchemaAndValue convertToNativeType(final Object value) { - - Class cls = value != null ? value.getClass() : Void.class; - final ArrayList objs = new ArrayList<>(); - - if (cls.isArray()) { - final Object[] array = (Object[]) value; - - Schema arraySchema = null; - - for (final Object element : array) { - SchemaAndValue tmp = convertToNativeType(element); - if (arraySchema == null) { - arraySchema = tmp.schema(); - } - objs.add(tmp.value()); - } - - return new SchemaAndValue(SchemaBuilder.array(arraySchema), objs); - } - - if (cls.isAssignableFrom(Void.class)) { - return SchemaAndValue.NULL; - } else if (cls.isAssignableFrom(String.class)) { - return new SchemaAndValue(SchemaBuilder.string().optional(), value); - } else if (cls.isAssignableFrom(Short.class)) { - return new SchemaAndValue(SchemaBuilder.int16().optional(), value); - } else if (cls.isAssignableFrom(Integer.class)) { - - return new SchemaAndValue(SchemaBuilder.int32().optional(), value); - } else if (cls.isAssignableFrom(Long.class)) { - - return new SchemaAndValue(SchemaBuilder.int64().optional(), value); - } else if (cls.isAssignableFrom(Byte.class)) { - return new SchemaAndValue(SchemaBuilder.int8().optional(), value); - } else if (cls.isAssignableFrom(Character.class)) { - return new SchemaAndValue(SchemaBuilder.int32().optional(), value == null ? null : new Integer(((char) value))); - } else if (cls.isAssignableFrom(Boolean.class)) { - return new SchemaAndValue(SchemaBuilder.bool().optional(), value); - } else if (cls.isAssignableFrom(Float.class)) { - return new SchemaAndValue(SchemaBuilder.float32().optional(), value); - } else if (cls.isAssignableFrom(BigDecimal.class)) { - return new SchemaAndValue(SchemaBuilder.float64().optional(), value == null ? null : ((BigDecimal) value).doubleValue()); - } else if (cls.isAssignableFrom(Double.class)) { - return new SchemaAndValue(SchemaBuilder.float64().optional(), value); - } else if (cls.isAssignableFrom(Instant.class)) { - return new SchemaAndValue(SchemaBuilder.int64().optional(), value == null ? null : ((Instant) value).toEpochMilli()); - - } - throw new SchemaBuilderException("Unknown type presented (" + cls + ")"); - - } - - private Schema buildSchema(Schema valueSchema) { - SchemaBuilder ret = SchemaBuilder.struct() - .field(OpcDaFields.TAG_NAME, SchemaBuilder.string()) - .field(OpcDaFields.TIMESTAMP, SchemaBuilder.int64()) - .field(OpcDaFields.QUALITY, SchemaBuilder.string()) - .field(OpcDaFields.UPDATE_PERIOD, SchemaBuilder.int64().optional()) - .field(OpcDaFields.TAG_GROUP, SchemaBuilder.string().optional()) - .field(OpcDaFields.OPC_SERVER_DOMAIN, SchemaBuilder.string().optional()) - .field(OpcDaFields.OPC_SERVER_HOST, SchemaBuilder.string()) - .field(OpcDaFields.ERROR_CODE, SchemaBuilder.int64().optional()) - .field(OpcDaFields.ERROR_REASON, SchemaBuilder.string().optional()); - if (valueSchema != null) { - ret = ret.field(OpcDaFields.VALUE, valueSchema); - } - return ret; - } - @Override public void start(Map props) { @@ -202,14 +132,20 @@ public void start(Map props) { host = connectionProfile.getConnectionUri().getHost(); defaultRefreshPeriodMillis = Long.parseLong(props.get(OpcDaSourceConnector.PROPERTY_DEFAULT_REFRESH_PERIOD)); directRead = Boolean.parseBoolean(props.get(OpcDaSourceConnector.PROPERTY_DIRECT_READ)); - tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis)) - .collect(Collectors.toMap(t -> t.name, Function.identity())); + opcOperations.connect(connectionProfile); if (!opcOperations.awaitConnected()) { throw new ConnectException("Unable to connect"); } + Map dictionary = + opcOperations.fetchMetadata(Arrays.stream(tags).map(t -> CommonUtils.parseTag(t, defaultRefreshPeriodMillis).getKey()) + .toArray(size -> new String[size])) + .stream() + .collect(Collectors.toMap(OpcTagInfo::getId, Function.identity())); + tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis, dictionary)) + .collect(Collectors.toMap(t -> t.getTagInfo().getId(), Function.identity())); logger.info("Started OPC-DA task for tags {}", (Object) tags); - minWaitTime = Math.max(10, gcd(tagInfoMap.values().stream().mapToLong(t -> t.refreshPeriodMillis).toArray())); + minWaitTime = Math.max(10, gcd(tagInfoMap.values().stream().mapToLong(TagInfo::getRefreshPeriodMillis).toArray())); tagReadingQueue = new HashSet<>(); executorService = Executors.newSingleThreadScheduledExecutor(); tagInfoMap.forEach((k, v) -> executorService.scheduleAtFixedRate(() -> { @@ -219,7 +155,7 @@ public void start(Map props) { } finally { lock.unlock(); } - }, 0, v.refreshPeriodMillis, TimeUnit.MILLISECONDS)); + }, 0, v.getRefreshPeriodMillis(), TimeUnit.MILLISECONDS)); executorService.scheduleAtFixedRate(() -> { try { @@ -242,38 +178,27 @@ public void start(Map props) { .map(entry -> entry.getKey().read(entry.getValue().toArray(new String[entry.getValue().size()]))) .flatMap(Collection::stream) .map(opcData -> { - SchemaAndValue tmp = convertToNativeType(opcData.getValue()); - Schema valueSchema = buildSchema(tmp.schema()); + SchemaAndValue tmp = CommonUtils.convertToNativeType(opcData.getValue()); + Schema valueSchema = CommonUtils.buildSchema(tmp.schema()); TagInfo meta = tagInfoMap.get(opcData.getTag()); - Struct value = new Struct(valueSchema) - .put(OpcDaFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()) - .put(OpcDaFields.TAG_NAME, opcData.getTag()) - .put(OpcDaFields.QUALITY, opcData.getQuality().name()) - .put(OpcDaFields.UPDATE_PERIOD, meta.refreshPeriodMillis) - .put(OpcDaFields.TAG_GROUP, meta.group) - .put(OpcDaFields.OPC_SERVER_HOST, host) - .put(OpcDaFields.OPC_SERVER_DOMAIN, domain); - - if (tmp.value() != null) { - value = value.put(OpcDaFields.VALUE, tmp.value()); - } - if (opcData.getOperationStatus().getLevel().compareTo(OperationStatus.Level.INFO) > 0) { - value.put(OpcDaFields.ERROR_CODE, opcData.getOperationStatus().getCode()); - if (opcData.getOperationStatus().getMessageDetail().isPresent()) { - value.put(OpcDaFields.ERROR_REASON, - opcData.getOperationStatus().getMessageDetail().get()); - } - } + Map additionalInfo = new HashMap<>(); + additionalInfo.put(OpcRecordFields.OPC_SERVER_HOST, host); + additionalInfo.put(OpcRecordFields.OPC_SERVER_DOMAIN, domain); + Struct value = CommonUtils.mapToConnectObject(opcData, + meta, + valueSchema, + tmp, + additionalInfo); Map partition = new HashMap<>(); - partition.put(OpcDaFields.TAG_NAME, opcData.getTag()); - partition.put(OpcDaFields.OPC_SERVER_DOMAIN, domain); - partition.put(OpcDaFields.OPC_SERVER_HOST, host); + partition.put(OpcRecordFields.TAG_NAME, opcData.getTag()); + partition.put(OpcRecordFields.OPC_SERVER_DOMAIN, domain); + partition.put(OpcRecordFields.OPC_SERVER_HOST, host); return new SourceRecord( partition, - Collections.singletonMap(OpcDaFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()), + Collections.singletonMap(OpcRecordFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()), "", SchemaBuilder.STRING_SCHEMA, domain + "|" + host + "|" + opcData.getTag(), @@ -327,26 +252,5 @@ public String version() { return getClass().getPackage().getImplementationVersion(); } - /** - * GCD recursive version - * - * @param x dividend - * @param y divisor - * @return - */ - private static long gcdInternal(long x, long y) { - return (y == 0) ? x : gcdInternal(y, x % y); - } - - /** - * Great common divisor (An elegant way to do it with a lambda). - * - * @param numbers list of number - * @return the GCD. - */ - private static long gcd(long... numbers) { - return Arrays.stream(numbers).reduce(0, (x, y) -> (y == 0) ? x : gcdInternal(y, x % y)); - } - } diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java new file mode 100644 index 000000000..c38c1bbf8 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java @@ -0,0 +1,114 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc.ua; + +import com.hurence.logisland.connect.opc.CommonUtils; +import org.apache.kafka.common.config.ConfigDef; +import org.apache.kafka.common.config.ConfigException; +import org.apache.kafka.common.config.ConfigValue; +import org.apache.kafka.connect.connector.Task; +import org.apache.kafka.connect.source.SourceConnector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * OPC-UA source connector. + * + * @author amarziali + */ +public class OpcUaSourceConnector extends SourceConnector { + + private static final Logger logger = LoggerFactory.getLogger(OpcUaSourceConnector.class); + + private Map configValues; + + public static final String PROPERTY_URI = "uri"; + public static final String PROPERTY_AUTH_USER = "auth.user"; + public static final String PROPERTY_AUTH_PASSWORD = "auth.password"; + public static final String PROPERTY_TAGS = "tags"; + public static final String PROPERTY_SOCKET_TIMEOUT = "socketTimeoutMillis"; + public static final String PROPERTY_DEFAULT_REFRESH_PERIOD = "defaultRefreshPeriodMillis"; + public static final String PROPERTY_DATA_PUBLICATION_PERIOD = "dataPublicationPeriodMillis"; + + + /** + * The configuration. + */ + private static final ConfigDef CONFIG = new ConfigDef() + .define(PROPERTY_URI, ConfigDef.Type.STRING, ConfigDef.Importance.HIGH, "The OPC-UA server uri") + .define(PROPERTY_AUTH_USER, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "The logon user") + .define(PROPERTY_AUTH_PASSWORD, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "The logon password") + .define(PROPERTY_TAGS, ConfigDef.Type.LIST, Collections.emptyList(), (name, value) -> { + if (value == null) { + throw new ConfigException("Cannot be null"); + } + List list = (List) value; + for (String s : list) { + if (CommonUtils.validateTagFormat(s)) { + throw new ConfigException("Tag list should be like [tag_name]:[refresh_period_millis] with optional refresh period"); + } + } + }, ConfigDef.Importance.HIGH, "The tags to subscribe to following format tagname:refresh_period_millis. E.g. myTag:1000") + .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, ConfigDef.Importance.LOW, "The socket timeout") + .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") + .define(PROPERTY_DATA_PUBLICATION_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The data publication window in milliseconds"); + + + @Override + public String version() { + return getClass().getPackage().getImplementationVersion(); + } + + @Override + public void start(Map props) { + //shallow copy + configValues = config().validate(props).stream().collect(Collectors.toMap(ConfigValue::name, Function.identity())); + logger.info("Starting OPC-UA connector (version {}) on server {} reading tags {}", version(), + configValues.get(PROPERTY_URI).value(), configValues.get(PROPERTY_TAGS).value()); + } + + @Override + public Class taskClass() { + return OpcUaSourceTask.class; + } + + @Override + public List> taskConfigs(int maxTasks) { + Map ret = configValues.entrySet().stream() + .filter(a -> a.getValue().value() != null) + .collect(Collectors.toMap(a -> a.getKey(), a -> a.getValue().value().toString())); + ret.put(PROPERTY_TAGS, ((List) configValues.get(PROPERTY_TAGS).value()).stream().collect(Collectors.joining(","))); + return Collections.singletonList(ret); + } + + @Override + public void stop() { + logger.info("Stopping OPC-UA connector (version {}) on server {}", version(), configValues.get(PROPERTY_URI).value()); + } + + @Override + public ConfigDef config() { + return CONFIG; + } +} diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java new file mode 100644 index 000000000..ed8ddf190 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java @@ -0,0 +1,190 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc.ua; + +import com.hurence.logisland.connect.opc.CommonUtils; +import com.hurence.logisland.connect.opc.OpcRecordFields; +import com.hurence.logisland.connect.opc.SmartOpcOperations; +import com.hurence.logisland.connect.opc.TagInfo; +import com.hurence.logisland.connect.opc.da.OpcDaSourceConnector; +import com.hurence.opc.OpcTagInfo; +import com.hurence.opc.auth.Credentials; +import com.hurence.opc.auth.UsernamePasswordCredentials; +import com.hurence.opc.ua.OpcUaConnectionProfile; +import com.hurence.opc.ua.OpcUaSession; +import com.hurence.opc.ua.OpcUaSessionProfile; +import com.hurence.opc.ua.OpcUaTemplate; +import org.apache.kafka.connect.data.Schema; +import org.apache.kafka.connect.data.SchemaAndValue; +import org.apache.kafka.connect.data.SchemaBuilder; +import org.apache.kafka.connect.data.Struct; +import org.apache.kafka.connect.errors.ConnectException; +import org.apache.kafka.connect.source.SourceRecord; +import org.apache.kafka.connect.source.SourceTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedTransferQueue; +import java.util.concurrent.TransferQueue; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * OPC-UA source task. + * + * @author amarziali + */ +public class OpcUaSourceTask extends SourceTask { + + private static final Logger logger = LoggerFactory.getLogger(OpcUaSourceTask.class); + + private SmartOpcOperations opcOperations; + private TransferQueue transferQueue; + private String tags[]; + private URI serverUri; + private long defaultRefreshPeriodMillis; + private long defaultPublicationPeriodMillis; + private Map tagInfoMap; + private ExecutorService executorService; + private volatile boolean running; + + + private OpcUaConnectionProfile propertiesToConnectionProfile(Map properties) { + OpcUaConnectionProfile ret = new OpcUaConnectionProfile(); + ret.setConnectionUri(URI.create(properties.get(OpcUaSourceConnector.PROPERTY_URI))); + if (properties.containsKey(OpcUaSourceConnector.PROPERTY_AUTH_USER)) { + ret.setCredentials(new UsernamePasswordCredentials() + .withUser(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_USER)) + .withPassword(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_PASSWORD))); + } else { + ret.setCredentials(Credentials.ANONYMOUS_CREDENTIALS); + } + if (properties.containsKey(OpcUaSourceConnector.PROPERTY_SOCKET_TIMEOUT)) { + ret.setSocketTimeout(Duration.ofMillis(Long.parseLong(properties.get(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT)))); + } + return ret; + } + + + @Override + public void start(Map props) { + transferQueue = new LinkedTransferQueue<>(); + opcOperations = new SmartOpcOperations<>(new OpcUaTemplate()); + OpcUaConnectionProfile connectionProfile = propertiesToConnectionProfile(props); + tags = props.get(OpcDaSourceConnector.PROPERTY_TAGS).split(","); + serverUri = connectionProfile.getConnectionUri(); + defaultRefreshPeriodMillis = Long.parseLong(props.get(OpcUaSourceConnector.PROPERTY_DEFAULT_REFRESH_PERIOD)); + defaultPublicationPeriodMillis = Long.parseLong(props.get(OpcUaSourceConnector.PROPERTY_DATA_PUBLICATION_PERIOD)); + + opcOperations.connect(connectionProfile); + if (!opcOperations.awaitConnected()) { + throw new ConnectException("Unable to connect"); + } + Map dictionary = + opcOperations.fetchMetadata(Arrays.stream(tags).map(t -> CommonUtils.parseTag(t, defaultRefreshPeriodMillis).getKey()) + .toArray(size -> new String[size])) + .stream() + .collect(Collectors.toMap(OpcTagInfo::getId, Function.identity())); + tagInfoMap = Arrays.stream(tags).map(t -> new TagInfo(t, defaultRefreshPeriodMillis, dictionary)) + .collect(Collectors.toMap(t -> t.getTagInfo().getId(), Function.identity())); + executorService = Executors.newSingleThreadExecutor(); + running = true; + executorService.submit(() -> { + final OpcUaSessionProfile sessionProfile = new OpcUaSessionProfile() + .withDefaultPollingInterval(Duration.ofMillis(defaultRefreshPeriodMillis)) + .withRefreshPeriod(Duration.ofMillis(defaultPublicationPeriodMillis)); + tagInfoMap.forEach((n, t) -> sessionProfile.addToPollingMap(t.getTagInfo().getId(), Duration.ofMillis(t.getRefreshPeriodMillis()))); + while (running) { + try (OpcUaSession session = opcOperations.createSession(sessionProfile)) { + session.stream(tagInfoMap.keySet().toArray(new String[tagInfoMap.size()])) + .map(opcData -> { + TagInfo meta = tagInfoMap.get(opcData.getTag()); + SchemaAndValue dataSchema = CommonUtils.convertToNativeType(opcData.getValue()); + Schema valueSchema = CommonUtils.buildSchema(dataSchema.schema()); + Struct valueStruct = CommonUtils.mapToConnectObject(opcData, + meta, + valueSchema, + dataSchema, + Collections.singletonMap(OpcRecordFields.OPC_SERVER_HOST, serverUri.toASCIIString())); + + + Map partition = new HashMap<>(); + partition.put(OpcRecordFields.TAG_NAME, opcData.getTag()); + partition.put(OpcRecordFields.OPC_SERVER_HOST, serverUri.toASCIIString()); + + return new SourceRecord( + partition, + Collections.singletonMap(OpcRecordFields.TIMESTAMP, opcData.getTimestamp().toEpochMilli()), + "", + SchemaBuilder.STRING_SCHEMA, + serverUri.toASCIIString() + "|" + opcData.getTag(), + valueSchema, + valueStruct); + } + ) + .forEach(transferQueue::add); + + } catch (Exception e) { + logger.error("Unexpected exception while streaming tags. Looping again.", e); + } + } + logger.info("OPC-UA reading loop ended."); + }); + logger.info("Started OPC-UA task for tags {}", (Object) tags); + } + + @Override + public List poll() throws InterruptedException { + List ret = new ArrayList<>(); + if (transferQueue.isEmpty()) { + Thread.sleep(defaultPublicationPeriodMillis); + } + transferQueue.drainTo(ret); + return ret; + } + + @Override + public void stop() { + running = false; + //session are automatically cleaned up and detached when the connection is closed. + + if (opcOperations != null) { + opcOperations.disconnect(); + opcOperations.awaitDisconnected(); + } + + if (executorService != null) { + executorService.shutdown(); + executorService = null; + } + transferQueue = null; + tagInfoMap = null; + logger.info("Stopped OPC-UA task for tags {}", (Object) tags); + } + + @Override + public String version() { + return getClass().getPackage().getImplementationVersion(); + } + +} \ No newline at end of file diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java similarity index 94% rename from logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java rename to logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java index f186c6570..2f8a72ae9 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opcda/OpcDaSourceConnectorTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java @@ -15,21 +15,20 @@ * */ -package com.hurence.logisland.connect.opcda; +package com.hurence.logisland.connect.opc.da; import com.google.gson.Gson; +import com.hurence.logisland.connect.opc.CommonUtils; import com.hurence.opc.OpcTagInfo; import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; import com.hurence.opc.da.OpcDaOperations; import com.hurence.opc.da.OpcDaTemplate; -import org.apache.kafka.connect.source.SourceRecord; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; import java.net.URI; -import java.sql.Struct; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.*; @@ -43,16 +42,16 @@ public class OpcDaSourceConnectorTest { @Test(expected = IllegalArgumentException.class) public void parseFailureTest() { - OpcDaSourceConnector.parseTag("test1:2aj", 500L); + CommonUtils.parseTag("test1:2aj", 500L); } @Test public void tagParseTest() { - Map.Entry toTest = OpcDaSourceConnector.parseTag("test1:1000", 500L); + Map.Entry toTest = CommonUtils.parseTag("test1:1000", 500L); Assert.assertEquals("test1", toTest.getKey()); Assert.assertEquals(new Long(1000), toTest.getValue()); - toTest = OpcDaSourceConnector.parseTag("test2", 500L); + toTest = CommonUtils.parseTag("test2", 500L); Assert.assertEquals("test2", toTest.getKey()); Assert.assertEquals(new Long(500), toTest.getValue()); } diff --git a/pom.xml b/pom.xml index 9d8debcd8..f0439f105 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,9 @@ jitpack.io https://jitpack.io + + true + openscada @@ -1249,7 +1252,7 @@ com.hurence.logisland - logisland-connector-opcda + logisland-connector-opc ${project.version} From aef49126c9686a545d24a7aef91e42b148740030 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 27 Jun 2018 15:29:51 +0200 Subject: [PATCH 40/63] Add default socket timeout for OPC --- .../logisland/connect/opc/da/OpcDaSourceConnector.java | 4 ++-- .../logisland/connect/opc/ua/OpcUaSourceConnector.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java index c6df698b6..0ddef4cbc 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java @@ -81,8 +81,8 @@ public class OpcDaSourceConnector extends SourceConnector { } } }, ConfigDef.Importance.HIGH, "The tags to subscribe to following format tagname:refresh_period_millis. E.g. myTag:1000") - .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, ConfigDef.Importance.LOW, "The socket timeout") - .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") + .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, 10_000,ConfigDef.Importance.LOW, "The socket timeout (defaults to 10 seconds)") + .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1_000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") .define(PROPERTY_DIRECT_READ, ConfigDef.Type.BOOLEAN, false, ConfigDef.Importance.LOW, "Use server cache or read directly from device"); diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java index c38c1bbf8..2731fda35 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java @@ -70,9 +70,9 @@ public class OpcUaSourceConnector extends SourceConnector { } } }, ConfigDef.Importance.HIGH, "The tags to subscribe to following format tagname:refresh_period_millis. E.g. myTag:1000") - .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, ConfigDef.Importance.LOW, "The socket timeout") - .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") - .define(PROPERTY_DATA_PUBLICATION_PERIOD, ConfigDef.Type.LONG, 1000, ConfigDef.Importance.LOW, "The data publication window in milliseconds"); + .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, 10_000, ConfigDef.Importance.LOW, "The socket timeout (defaults to 10 seconds)") + .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1_000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") + .define(PROPERTY_DATA_PUBLICATION_PERIOD, ConfigDef.Type.LONG, 1_000, ConfigDef.Importance.LOW, "The data publication window in milliseconds"); @Override From a461f4fcd491327aec7c3b979b12445386e7242f Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 27 Jun 2018 15:30:11 +0200 Subject: [PATCH 41/63] Update connectors documentations --- logisland-documentation/connectors.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/logisland-documentation/connectors.rst b/logisland-documentation/connectors.rst index 8159c69f0..394b88c5e 100644 --- a/logisland-documentation/connectors.rst +++ b/logisland-documentation/connectors.rst @@ -45,6 +45,8 @@ Please refer to the following table for the details: +--------------------------+-------------------------+--------------------------------------------------------+------------------------------+ | OPC-DA (IIoT) | https://github.com/Hurence/logisland | None (Built in) | +--------------------------+-------------------------+--------------------------------------------------------+------------------------------+ +| OPC-UA (IIoT) | https://github.com/Hurence/logisland | None (Built in) | ++--------------------------+-------------------------+--------------------------------------------------------+------------------------------+ | FTP | https://github.com/Eneco/kafka-connect-ftp | -DwithConnectFtp | +--------------------------+----------------------------------------------------------------------------------+------------------------------+ | Blockchain | https://github.com/Landoop/stream-reactor/tree/master/kafka-connect-blockchain | -DwithConnectBlockchain | From 22957fafd74283d3271a6edc7e49f3dcfa234e14 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Wed, 27 Jun 2018 17:05:18 +0200 Subject: [PATCH 42/63] Add X509 authentication & channel encryption params --- .../connect/opc/ua/OpcUaSourceConnector.java | 26 ++-- .../connect/opc/ua/OpcUaSourceTask.java | 73 +++++++++- .../connect/opc/ua/OpcUaSourceTaskTest.java | 129 ++++++++++++++++++ 3 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java index 2731fda35..04c8e7088 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java @@ -43,9 +43,14 @@ public class OpcUaSourceConnector extends SourceConnector { private Map configValues; - public static final String PROPERTY_URI = "uri"; - public static final String PROPERTY_AUTH_USER = "auth.user"; - public static final String PROPERTY_AUTH_PASSWORD = "auth.password"; + public static final String PROPERTY_SERVER_URI = "server.uri"; + public static final String PROPERTY_AUTH_BASIC_USER = "auth.basic.user"; + public static final String PROPERTY_AUTH_BASIC_PASSWORD = "auth.basic.password"; + public static final String PROPERTY_AUTH_X509_CERTIFICATE = "auth.x509.certificate"; + public static final String PROPERTY_AUTH_X509_PRIVATE_KEY = "auth.x509.key"; + public static final String PROPERTY_CHANNEL_CERTIFICATE = "channel.certificate"; + public static final String PROPERTY_CHANNEL_PRIVATE_KEY = "channel.key"; + public static final String PROPERTY_CLIENT_URI = "client.uri"; public static final String PROPERTY_TAGS = "tags"; public static final String PROPERTY_SOCKET_TIMEOUT = "socketTimeoutMillis"; public static final String PROPERTY_DEFAULT_REFRESH_PERIOD = "defaultRefreshPeriodMillis"; @@ -56,9 +61,11 @@ public class OpcUaSourceConnector extends SourceConnector { * The configuration. */ private static final ConfigDef CONFIG = new ConfigDef() - .define(PROPERTY_URI, ConfigDef.Type.STRING, ConfigDef.Importance.HIGH, "The OPC-UA server uri") - .define(PROPERTY_AUTH_USER, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "The logon user") - .define(PROPERTY_AUTH_PASSWORD, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "The logon password") + .define(PROPERTY_SERVER_URI, ConfigDef.Type.STRING, ConfigDef.Importance.HIGH, "The OPC-UA server uri to connect to") + .define(PROPERTY_AUTH_BASIC_USER, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "(User/Password security): The login user") + .define(PROPERTY_AUTH_BASIC_PASSWORD, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "(User/Password security): the login password") + .define(PROPERTY_AUTH_X509_CERTIFICATE, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "(X509 security): The certificate") + .define(PROPERTY_AUTH_X509_PRIVATE_KEY, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "(X509 security): The private key") .define(PROPERTY_TAGS, ConfigDef.Type.LIST, Collections.emptyList(), (name, value) -> { if (value == null) { throw new ConfigException("Cannot be null"); @@ -70,6 +77,9 @@ public class OpcUaSourceConnector extends SourceConnector { } } }, ConfigDef.Importance.HIGH, "The tags to subscribe to following format tagname:refresh_period_millis. E.g. myTag:1000") + .define(PROPERTY_CLIENT_URI, ConfigDef.Type.STRING, "urn:hurence:logisland", ConfigDef.Importance.MEDIUM, "The client URI (defaults to urn:hurence:logisland)") + .define(PROPERTY_CHANNEL_CERTIFICATE, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "In case the connection is secure, the client will have a certificate") + .define(PROPERTY_CHANNEL_PRIVATE_KEY, ConfigDef.Type.STRING, ConfigDef.Importance.LOW, "In case the connection is secure, the client will have a private key") .define(PROPERTY_SOCKET_TIMEOUT, ConfigDef.Type.LONG, 10_000, ConfigDef.Importance.LOW, "The socket timeout (defaults to 10 seconds)") .define(PROPERTY_DEFAULT_REFRESH_PERIOD, ConfigDef.Type.LONG, 1_000, ConfigDef.Importance.LOW, "The default data refresh period in milliseconds") .define(PROPERTY_DATA_PUBLICATION_PERIOD, ConfigDef.Type.LONG, 1_000, ConfigDef.Importance.LOW, "The data publication window in milliseconds"); @@ -85,7 +95,7 @@ public void start(Map props) { //shallow copy configValues = config().validate(props).stream().collect(Collectors.toMap(ConfigValue::name, Function.identity())); logger.info("Starting OPC-UA connector (version {}) on server {} reading tags {}", version(), - configValues.get(PROPERTY_URI).value(), configValues.get(PROPERTY_TAGS).value()); + configValues.get(PROPERTY_SERVER_URI).value(), configValues.get(PROPERTY_TAGS).value()); } @Override @@ -104,7 +114,7 @@ public List> taskConfigs(int maxTasks) { @Override public void stop() { - logger.info("Stopping OPC-UA connector (version {}) on server {}", version(), configValues.get(PROPERTY_URI).value()); + logger.info("Stopping OPC-UA connector (version {}) on server {}", version(), configValues.get(PROPERTY_SERVER_URI).value()); } @Override diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java index ed8ddf190..efd8bd82b 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java @@ -25,6 +25,7 @@ import com.hurence.opc.OpcTagInfo; import com.hurence.opc.auth.Credentials; import com.hurence.opc.auth.UsernamePasswordCredentials; +import com.hurence.opc.auth.X509Credentials; import com.hurence.opc.ua.OpcUaConnectionProfile; import com.hurence.opc.ua.OpcUaSession; import com.hurence.opc.ua.OpcUaSessionProfile; @@ -36,10 +37,22 @@ import org.apache.kafka.connect.errors.ConnectException; import org.apache.kafka.connect.source.SourceRecord; import org.apache.kafka.connect.source.SourceTask; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMEncryptedKeyPair; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayInputStream; +import java.io.StringReader; import java.net.URI; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.time.Duration; import java.util.*; import java.util.concurrent.ExecutorService; @@ -56,6 +69,10 @@ */ public class OpcUaSourceTask extends SourceTask { + static { + Security.addProvider(new BouncyCastleProvider()); + } + private static final Logger logger = LoggerFactory.getLogger(OpcUaSourceTask.class); private SmartOpcOperations opcOperations; @@ -69,18 +86,60 @@ public class OpcUaSourceTask extends SourceTask { private volatile boolean running; + private X509Certificate decodePemCertificate(String certificate) { + try { + return (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(certificate.getBytes("UTF8"))); + + } catch (Exception e) { + throw new IllegalArgumentException("Unable to decode certificate", e); + } + } + + private PrivateKey decodePemPrivateKey(String privateKey) { + try { + PEMParser pemParser = new PEMParser(new StringReader(privateKey)); + Object object = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + if (object instanceof PEMEncryptedKeyPair) { + throw new UnsupportedOperationException("Encrypted keys are not yet supported"); + } + PEMKeyPair ukp = (PEMKeyPair) object; + KeyPair keyPair = converter.getKeyPair(ukp); + return keyPair.getPrivate(); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to decode private key", e); + } + } + + private X509Credentials decodeCredentials(String certificate, String key) { + return new X509Credentials() + .withPrivateKey(decodePemPrivateKey(key)) + .withCertificate(decodePemCertificate(certificate)); + } + private OpcUaConnectionProfile propertiesToConnectionProfile(Map properties) { - OpcUaConnectionProfile ret = new OpcUaConnectionProfile(); - ret.setConnectionUri(URI.create(properties.get(OpcUaSourceConnector.PROPERTY_URI))); - if (properties.containsKey(OpcUaSourceConnector.PROPERTY_AUTH_USER)) { + OpcUaConnectionProfile ret = new OpcUaConnectionProfile() + .withConnectionUri(URI.create(properties.get(OpcUaSourceConnector.PROPERTY_SERVER_URI))) + .withClientIdUri(properties.get(OpcUaSourceConnector.PROPERTY_CLIENT_URI)) + .withSocketTimeout(Duration.ofMillis(Long.parseLong(properties.get(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT)))); + + if (properties.containsKey(OpcUaSourceConnector.PROPERTY_AUTH_BASIC_USER)) { ret.setCredentials(new UsernamePasswordCredentials() - .withUser(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_USER)) - .withPassword(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_PASSWORD))); + .withUser(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_BASIC_USER)) + .withPassword(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_BASIC_PASSWORD))); + } else if (properties.containsKey(OpcUaSourceConnector.PROPERTY_AUTH_X509_CERTIFICATE) && + properties.containsKey(OpcUaSourceConnector.PROPERTY_AUTH_X509_PRIVATE_KEY)) { + ret.setCredentials(decodeCredentials(properties.get(OpcUaSourceConnector.PROPERTY_AUTH_X509_CERTIFICATE), + properties.get(OpcUaSourceConnector.PROPERTY_AUTH_X509_PRIVATE_KEY))); } else { ret.setCredentials(Credentials.ANONYMOUS_CREDENTIALS); } - if (properties.containsKey(OpcUaSourceConnector.PROPERTY_SOCKET_TIMEOUT)) { - ret.setSocketTimeout(Duration.ofMillis(Long.parseLong(properties.get(OpcDaSourceConnector.PROPERTY_SOCKET_TIMEOUT)))); + + if (properties.containsKey(OpcUaSourceConnector.PROPERTY_CHANNEL_CERTIFICATE) && + properties.containsKey(OpcUaSourceConnector.PROPERTY_CHANNEL_PRIVATE_KEY)) { + ret.setSecureChannelEncryption(decodeCredentials(properties.get(OpcUaSourceConnector.PROPERTY_CHANNEL_CERTIFICATE), + properties.get(OpcUaSourceConnector.PROPERTY_CHANNEL_PRIVATE_KEY))); } return ret; } diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java new file mode 100644 index 000000000..6f4fabf78 --- /dev/null +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java @@ -0,0 +1,129 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.connect.opc.ua; + +import com.hurence.opc.auth.X509Credentials; +import com.sun.deploy.security.CertUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.lang.reflect.Method; + +public class OpcUaSourceTaskTest { + + @Test + public void testCredentialLoad() throws Exception { + + + String key = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIJKAIBAAKCAgEAuFooF2RLI/v3z01grK6UDpi3efJO2EI/w93gD/61r486LB3J\n" + + "Wk2gffNgJZEpaQJTE/gcIN4faPIcAn5/ZvzXZ+LgzpAOByq/Xo6zjp/QqZhsBjyU\n" + + "2+DrSU2dga8CtA9KLkkRtM2z4w1eMeTDq8D0IvOPPgxPSmyY1Iju7LtoYRLhSgg1\n" + + "zNgOskkOwJPgpl89dwVYDSQqTeUhVyWNH0hmxT8VxgG7U+VRBPoIr7NLgNBF5VMB\n" + + "VJPEqsAmeG3fMmjxYgjOeXRQDLWHVboKv1OnTbYvfDqH8V0S8MrYOqp4afu4ogcv\n" + + "Oj8kfQ0eeqiSMy1s2vssupzhiXHwAViLAPyMBaxGIai157rBXVf+c8FJG+Vklmb3\n" + + "WYs5uMwRsXNc86+RfoWSxBzKvPqx0aA64tsFZMPD/59Dk2Rj2xVs1gD64X38ucMJ\n" + + "16CG/1BDlkwmfmWPpKeB/ALFmQuIXZFHMUnzkI8AJiWP6hjo3yGnHFwBe36OHyHG\n" + + "YrCWX/km/rrmLV4AG04neSZ1zxeKMTleLTUg5cdW8NQzOywe0m1NIG9RZ99WdTsA\n" + + "qkyqJ1tnwbup6j2UVvszz6O7uRNso260Xn9GKPZRKUJHW4aPTPPQOYgRvBhqSlhw\n" + + "X1AuvPREdzvIfnrqj4jmhWS7MMtFK3+oFC1wI8kBjClAaQ0yNDfeXBP/nC8CAwEA\n" + + "AQKCAgEAtF0IsnIejfM8LWa/+dLH6kwB3l5yQ2T1q/UM/bkvGrdfq7/sutwN9IxD\n" + + "eh2+zQ1IKNZq9sE7K9sMCmimzyT6vpobZh1MjDiHiMTG6fh0FymYLrXg0gsJR+uW\n" + + "+UU3uODoq8Yze5hxsefnS5tM0WJzuSpf783tWZxMHkxmrdhhM/Bb2KmVsXeFUWrm\n" + + "8wT7Gus9YJAq6JiEhzdw2ilUG9IjMkIZVGNnWpqWHO9fxj791OZwLAB84bm9BW3/\n" + + "dX3RjCleWJLTJ8Ljeruzz+y4DR6UJhTj+n/tdvifylQ7H5KfQtnTdzreOveCBJLs\n" + + "SgdZGpcL1GdACMfqZSXDMh3lya5Mcqo7UgKM4o0dHcW5HMALS6SMcGmMyzapVP6k\n" + + "a05ebuo0fNuHcgl+Rf5/QukEje+zfK3yfJKAhUeO/A5wMDIl4uruIOImR1N3imdG\n" + + "Z7QbzSFYjnmUXRO0ZCP3rvoNsWxRRgRTAw/Tk0QRTTtpGiqq0Kwcj7OwRCX33Vlc\n" + + "t4jg++fNw3iPC2NL9VFP0maeypNpZaxK/T+GuqCyagvYgyrQUGEsWv0hg8Myvji2\n" + + "6tsa9aZDI+GwR4m/uZwtELR+nTdqDpxQFKBPEvI1FXTXXhMJ8HUumi+1bUjd73jf\n" + + "luhhyubod8VIG4P8PTqLbOLu37p4T+qChkWHJr0vC1wCZ8nxI5ECggEBAOl4fo9o\n" + + "M+j3GkcM/jEJWQ0vqKTArXO/DZ+fGzJ8Y6mom3oGHE5eujUcyUpequGju1uoquGX\n" + + "BxrOPQMq8FtVSr+tUTIy2O1YUe5omc1nRf1PtAMP+J74Xp3LRxdGukYCbRRQVewH\n" + + "jmgIDAyGO+3HSLOqL3KNBDZCRmJp6zMNqX4abRXVbL4/lhhUpSzlxRqmuE8Id80F\n" + + "BTyL1CzvDNjrjYa6CgInPT/dHiVrnNAdW50tKhYimrwjB5/qbjoanEydgkyGTbFd\n" + + "5z8tMLVFJ7w4tPHkmsYjvI0p0I9FPJD2GIfrpvFPr/uVrQ6+d8roH3blAfGns4QW\n" + + "8hIARNSqoadd9WkCggEBAMokRWO8z7k+u97F39dbTqEXyHjWUPu5f0iIolb9N1dn\n" + + "kjMJ7qq1A10WYeZVb/wnlpR2r+Q3zUxZqZigaCOas2ksHF/Ujruu3QrYkR/r1yXB\n" + + "MUDxaeo50aZp+HcmAE7eKySBw/TJMn4yvCuX45sIN82zQWpeRekcSFAjOQfRV2HW\n" + + "P4ZIDysn7DsGf+vm8cAsW6Quq3PFTBGonS7RYMlhknU1/RfH4g24n88cler8oTYM\n" + + "s/jp5hpjkxJ0Unb+8pV0OXLnWVSmMLUVSVlHOl4CCKCTNQvKf28upwhB1pPWej6i\n" + + "NMqdY+fIt+Fd985V0Ir+B21ShVDed5c72bpB06n9WdcCggEAG+Epq9JTsJQhbS6e\n" + + "BBkLq0lvqAziKZo89Dy5sLOt6wqZVl74bltdfQ4s81aOrVcx/mYL0diJHqhWHNS5\n" + + "0w5CWNVHhukPgngzgHa5NxAICZHE+0Ci/cjG86zclmj5wXZ0tCJLwF2+oamkVrKI\n" + + "4YIUqm++Lr2sLRaI9SOU1InjHY3mTN8plyZctBcXil79xIr4I2ftdmwNDgfclGkP\n" + + "ba/jPJ1mqI8q/z9WZD2PgkKfOAu2pOII/EJqnKwP8ZxP4c5FSwIWsQF3pdGtqVfS\n" + + "wOU8pk4YNWT7FRhTMWihLOZWU5TOYK6VY0OiYMpZ378MUtRSARt3kmRzD7c8gPDH\n" + + "UQclUQKCAQB4n9RYhB9g57KsaV/93xq4vrx+f0WsMTFnU0Gsr0YK/l8b3d1yOLpd\n" + + "HjIlhO5ihi0xQvILOdFkskymK3J5bKOLKytzdCAIl3yIMFvJtK6adQKzQlx1zTLy\n" + + "H2KJlz+v0JvmGRmaRUXAUP5A9U55ARprwYBTvRXy2VG9oIczxxRh6bvWocGLezNY\n" + + "tbQ4TYQNrWqyOrdNSnruPrQtb/xVr8f58dGqEzkt/vI+YUyFAWQiIMp0yv7o2Gq3\n" + + "JHrhT5nq3YQ6sRt5jAKczKsMf5iw6H3FdJK/CoOpESnTn5YwelhQb/MYxXsMoZY5\n" + + "Ah4SHttnVdeQwSGU9Gxg7vIqV4W7dtfZAoIBAEeRfFhJUAX1re+aNt7r8e2hnpzO\n" + + "jVHRs6mWz8cghd0SCxThkl31J0juHnR/w5XyggHeHjU+BXOQcmE3EGU+ca3X5UxN\n" + + "W+Awlv28LtotNDoGXSZxYhzsFLpB6rKcq48C2nKrjdX53CTwx+XwKGou0NWSTIUv\n" + + "qx4eVxIamH/Eox7OLzNxvME0lgvlvVWmj4M7KmyA4W0V1vtulMGYAPFg3ZBuhBzj\n" + + "e+tIlgoRqeOF3ntBM5btAgHik9PVocBQEeRpQU2EvqN3kgh+97Nkg7/ONDsqf5Uc\n" + + "cTVD5cep8RnqKwcdZ6HgsdX2uQCX3lkpzGuBWpCJUZl0VWIgL2CUl1KH03E=\n" + + "-----END RSA PRIVATE KEY-----"; + + String cert = "-----BEGIN CERTIFICATE-----\n" + + "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\n" + + "jdI=\n" + + "-----END CERTIFICATE-----"; + + Method method = OpcUaSourceTask.class.getDeclaredMethod("decodeCredentials", String.class, String.class); + method.setAccessible(true); + OpcUaSourceTask task = new OpcUaSourceTask(); + X509Credentials ret = (X509Credentials) method.invoke(task, cert, key); + System.out.println(ret); + Assert.assertNotNull(ret); + Assert.assertNotNull(ret.getCertificate()); + Assert.assertEquals("Hurence IIoT", CertUtils.extractSubjectAliasName(ret.getCertificate())); + Assert.assertNotNull(ret.getPrivateKey()); + + } + +} \ No newline at end of file From 141c56c36ba7bf8fcd570119f2b09ab318131852 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 29 Jun 2018 15:26:38 +0200 Subject: [PATCH 43/63] Multiple changes: - Finalize OPC-UA/DA - Make structured streaming resilient - Avoid serialization issues when submitting tasks to spark --- .../source/KafkaConnectStreamSource.java | 6 +++--- .../connect/source/SourceThread.java | 6 +++++- .../connect/opc/da/OpcDaSourceTask.java | 14 +++++++++++++- .../connect/opc/ua/OpcUaSourceTaskTest.java | 2 -- logisland-documentation/components.rst | 3 +++ logisland-documentation/pom.xml | 4 ---- .../spark/KafkaStreamProcessingEngine.scala | 15 ++++++++++++--- .../RemoteApiStreamProcessingEngine.scala | 9 ++++----- .../stream/spark/DummyRecordStream.scala | 2 +- .../logisland/stream/spark/package.scala | 19 ++++++++++--------- .../spark/structured/StructuredStream.scala | 6 +++++- ...nsoleStructuredStreamProviderService.scala | 8 +------- .../StructuredStreamProviderService.scala | 5 +---- .../src/main/resources/docs/components.rst | 3 +++ .../src/main/resources/docs/connectors.rst | 2 ++ 15 files changed, 63 insertions(+), 41 deletions(-) diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java index 2b8a5e517..482fc7513 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java @@ -200,14 +200,14 @@ private void startAllThreads() throws IllegalAccessException, InstantiationExcep } //Give a meaningful name to thread belonging to this connector final ThreadGroup threadGroup = new ThreadGroup(connector.getClass().getSimpleName()); - final List sourceThreads = createThreadTasks(); + final List threadz = createThreadTasks(); //Configure a new executor service ] - executorService = Executors.newFixedThreadPool(sourceThreads.size(), r -> { + executorService = Executors.newFixedThreadPool(threadz.size(), r -> { Thread t = new Thread(threadGroup, r); t.setDaemon(true); return t; }); - createThreadTasks().forEach(st -> { + threadz.forEach(st -> { executorService.execute(st.start()); sourceThreads.add(st); }); diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java index 4b7fbecfa..aa49acc4d 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java @@ -103,6 +103,10 @@ public SourceThread start() { */ public void stop() { running.set(false); - + try { + task.stop(); + } catch (Exception e) { + LOGGER.warn("Unable to properly stop task " + task, e ); + } } } diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java index 10d223fd8..58cbadc61 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java @@ -21,6 +21,7 @@ import com.hurence.logisland.connect.opc.OpcRecordFields; import com.hurence.logisland.connect.opc.SmartOpcOperations; import com.hurence.logisland.connect.opc.TagInfo; +import com.hurence.opc.ConnectionState; import com.hurence.opc.OpcTagInfo; import com.hurence.opc.auth.UsernamePasswordCredentials; import com.hurence.opc.da.OpcDaConnectionProfile; @@ -88,7 +89,7 @@ private static long gcd(long... numbers) { } private synchronized void createSessionsIfNeeded() { - if (opcOperations != null && opcOperations.resetStale()) { + if (opcOperations != null && (opcOperations.resetStale() || sessions == null)) { sessions = new HashMap<>(); tagInfoMap.entrySet().stream().collect(Collectors.groupingBy(entry -> entry.getValue().getRefreshPeriodMillis())) .forEach((a, b) -> { @@ -214,6 +215,17 @@ public void start(Map props) { }); } catch (Exception e) { logger.error("Got exception while reading tags", e); + if (sessions != null && opcOperations != null && opcOperations.getConnectionState() == ConnectionState.CONNECTED) { + while (!sessions.isEmpty()) { + + try { + sessions.remove(sessions.keySet().stream().findFirst().get()).close(); + } catch (Exception e1) { + //swallow here + } + } + } + sessions = null; } }, 0L, minWaitTime, TimeUnit.MILLISECONDS); } diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java index 6f4fabf78..3b6dcc6b3 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java @@ -18,7 +18,6 @@ package com.hurence.logisland.connect.opc.ua; import com.hurence.opc.auth.X509Credentials; -import com.sun.deploy.security.CertUtils; import org.junit.Assert; import org.junit.Test; @@ -121,7 +120,6 @@ public void testCredentialLoad() throws Exception { System.out.println(ret); Assert.assertNotNull(ret); Assert.assertNotNull(ret.getCertificate()); - Assert.assertEquals("Hurence IIoT", CertUtils.extractSubjectAliasName(ret.getCertificate())); Assert.assertNotNull(ret.getPrivateKey()); } diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index 5b6690a62..6cb92e54d 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -221,6 +221,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" "alert.criticity", "from 0 to ...", "", "0", "", "" @@ -314,6 +315,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" @@ -408,6 +410,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" Dynamic Properties diff --git a/logisland-documentation/pom.xml b/logisland-documentation/pom.xml index acba32626..a8cf41745 100644 --- a/logisland-documentation/pom.xml +++ b/logisland-documentation/pom.xml @@ -117,10 +117,6 @@ com.hurence.logisland logisland-redis_4-client-service
- - com.hurence.logisland - logisland-connector-opcda - diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index ae1e35aad..20783244c 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -25,10 +25,11 @@ import java.util.stream.Collectors import com.hurence.logisland.component.{AllowableValue, ComponentContext, PropertyDescriptor} import com.hurence.logisland.engine.spark.remote.PipelineConfigurationBroadcastWrapper import com.hurence.logisland.engine.{AbstractProcessingEngine, EngineContext} -import com.hurence.logisland.stream.spark.SparkRecordStream +import com.hurence.logisland.stream.spark.{AbstractKafkaRecordStream, SparkRecordStream} import com.hurence.logisland.util.spark.SparkUtils import com.hurence.logisland.validator.StandardValidators import org.apache.spark.groupon.metrics.UserMetricsSystem +import org.apache.spark.sql.SparkSession import org.apache.spark.streaming.{Milliseconds, StreamingContext} import org.apache.spark.{SparkConf, SparkContext} import org.slf4j.LoggerFactory @@ -435,9 +436,8 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { */ override def start(engineContext: EngineContext) = { logger.info("starting Spark Engine") - //val timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT).asInteger().intValue() val streamingContext = createStreamingContext(engineContext) - if (!engineContext.getStreamContexts.isEmpty) { + if (!engineContext.getStreamContexts.map(p=>p.getStream).filter(p=>p.isInstanceOf[AbstractKafkaRecordStream]).isEmpty) { streamingContext.start() } } @@ -469,6 +469,7 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { engineContext.getStreamContexts.foreach(streamingContext => { try { val kafkaStream = streamingContext.getStream.asInstanceOf[SparkRecordStream] + kafkaStream.setup(appName, ssc, streamingContext, engineContext) kafkaStream.start() } catch { @@ -503,6 +504,14 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { }) + SparkSession.builder().getOrCreate().streams.active.foreach(streamingQuery=>{ + try { + streamingQuery.stop() + } catch { + case ex: Exception => + logger.error("something bad while stopping a streaming query. Please check cluster health : {}", ex.getMessage) + } + }) } override def onPropertyModified(descriptor: PropertyDescriptor, oldValue: String, newValue: String) = { diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala index 0bfa3ee81..1ca0034fc 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -26,7 +26,10 @@ import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext import com.hurence.logisland.engine.spark.remote.model.DataFlow import com.hurence.logisland.engine.spark.remote.{RemoteApiClient, RemoteApiComponentFactory} +import com.hurence.logisland.stream.StandardStreamContext +import com.hurence.logisland.stream.spark.DummyRecordStream import com.hurence.logisland.validator.StandardValidators +import org.apache.spark.streaming.dstream.DStream import org.slf4j.LoggerFactory object RemoteApiStreamProcessingEngine { @@ -105,11 +108,7 @@ class RemoteApiStreamProcessingEngine extends KafkaStreamProcessingEngine { * @param engineContext */ override def start(engineContext: EngineContext): Unit = { - /* - if (engineContext.getStreamContexts.isEmpty) { - engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")) - } - */ + // engineContext.addStreamContext(new StandardStreamContext(new DummyRecordStream(), "busybox")) if (!initialized) { initialized = true diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala index 46bb4123e..3ec1be546 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala @@ -29,7 +29,7 @@ import org.apache.spark.streaming.receiver.Receiver class DummyRecordStream extends AbstractRecordStream with SparkRecordStream { - private var streamingContext: StreamingContext = _ + @transient private var streamingContext: StreamingContext = _ /** * Allows subclasses to register which property descriptor objects are diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala index d9e622634..c99d21c0f 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala @@ -90,20 +90,21 @@ object StreamProperties { val JSON_SERIALIZER = new AllowableValue(classOf[JsonSerializer].getName, "avro serialization", "serialize events as json blocs") val KRYO_SERIALIZER = new AllowableValue(classOf[KryoSerializer].getName, - "kryo serialization", "serialize events as json blocs") + "kryo serialization", "serialize events as binary blocs") + val STRING_SERIALIZER = new AllowableValue(classOf[KryoSerializer].getName, + "string serialization", "serialize events as string") val BYTESARRAY_SERIALIZER = new AllowableValue(classOf[BytesArraySerializer].getName, "byte array serialization", "serialize events as byte arrays") val KURA_PROTOCOL_BUFFER_SERIALIZER = new AllowableValue(classOf[KuraProtobufSerializer].getName, "Kura Protobuf serialization", "serialize events as Kura protocol buffer") val NO_SERIALIZER = new AllowableValue("none", "no serialization", "send events as bytes") - val INPUT_SERIALIZER: PropertyDescriptor = new PropertyDescriptor.Builder() .name("kafka.input.topics.serializer") .description("") .required(false) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, NO_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER) .defaultValue(KRYO_SERIALIZER.getValue) .build @@ -112,7 +113,7 @@ object StreamProperties { .description("") .required(false) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, NO_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER) .defaultValue(KRYO_SERIALIZER.getValue) .build @@ -122,7 +123,7 @@ object StreamProperties { .required(false) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) .defaultValue(JSON_SERIALIZER.getValue) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, NO_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER) .build @@ -358,7 +359,7 @@ object StreamProperties { .description("the serializer to use") .required(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, NO_SERIALIZER, KURA_PROTOCOL_BUFFER_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER, KURA_PROTOCOL_BUFFER_SERIALIZER) .defaultValue(NO_SERIALIZER.getValue) .build @@ -367,7 +368,7 @@ object StreamProperties { .description("The key serializer to use") .required(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, NO_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER,KURA_PROTOCOL_BUFFER_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER) .defaultValue(NO_SERIALIZER.getValue) .build @@ -391,7 +392,7 @@ object StreamProperties { .description("the serializer to use") .required(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, NO_SERIALIZER, KURA_PROTOCOL_BUFFER_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER, KURA_PROTOCOL_BUFFER_SERIALIZER) .defaultValue(NO_SERIALIZER.getValue) .build @@ -400,7 +401,7 @@ object StreamProperties { .description("The key serializer to use") .required(true) .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) - .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, NO_SERIALIZER, KURA_PROTOCOL_BUFFER_SERIALIZER) + .allowableValues(KRYO_SERIALIZER, JSON_SERIALIZER, AVRO_SERIALIZER, BYTESARRAY_SERIALIZER, STRING_SERIALIZER, NO_SERIALIZER, KURA_PROTOCOL_BUFFER_SERIALIZER) .defaultValue(NO_SERIALIZER.getValue) .build diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala index 111165bd2..a4344cdb4 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala @@ -21,6 +21,7 @@ import java.util.Collections import com.hurence.logisland.component.PropertyDescriptor import com.hurence.logisland.engine.EngineContext +import com.hurence.logisland.engine.spark.remote.PipelineConfigurationBroadcastWrapper import com.hurence.logisland.logging.StandardComponentLogger import com.hurence.logisland.stream.StreamProperties._ import com.hurence.logisland.stream.spark.SparkRecordStream @@ -102,7 +103,9 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { - + streamContext.getProcessContexts().clear(); + streamContext.getProcessContexts().addAll( + PipelineConfigurationBroadcastWrapper.getInstance().get(streamContext.getIdentifier)) val readDF = readStreamService.load(spark, controllerServiceLookupSink, streamContext) // apply windowing @@ -137,6 +140,7 @@ class StructuredStream extends AbstractRecordStream with SparkRecordStream { } } + } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala index 31265036e..14a8a2bce 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala @@ -81,15 +81,9 @@ class ConsoleStructuredStreamProviderService extends AbstractControllerService w import spark.implicits._ // implicit val myObjEncoder = org.apache.spark.sql.Encoders.kryo[Record] - - val df2 = df - - + df .writeStream .format("console") .start() - - - df2.awaitTermination() } } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala index d148d179a..a79f90ff1 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala @@ -4,7 +4,6 @@ import java.io.{ByteArrayInputStream, ByteArrayOutputStream} import java.util import com.hurence.logisland.controller.ControllerService -import com.hurence.logisland.engine.spark.remote.PipelineConfigurationBroadcastWrapper import com.hurence.logisland.record._ import com.hurence.logisland.serializer.{RecordSerializer, SerializerProvider} import com.hurence.logisland.stream.StreamContext @@ -68,13 +67,11 @@ trait StructuredStreamProviderService extends ControllerService { val df = read(spark, streamContext) + df.mapPartitions(iterator => { val controllerServiceLookup = controllerServiceLookupSink.value.getControllerServiceLookup() - streamContext.getProcessContexts().clear(); - streamContext.getProcessContexts().addAll( - PipelineConfigurationBroadcastWrapper.getInstance().get(streamContext.getIdentifier)) /** * create serializers diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index 5b6690a62..6cb92e54d 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -221,6 +221,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" "alert.criticity", "from 0 to ...", "", "0", "", "" @@ -314,6 +315,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" @@ -408,6 +410,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" Dynamic Properties diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/connectors.rst b/logisland-framework/logisland-resources/src/main/resources/docs/connectors.rst index 8159c69f0..394b88c5e 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/connectors.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/connectors.rst @@ -45,6 +45,8 @@ Please refer to the following table for the details: +--------------------------+-------------------------+--------------------------------------------------------+------------------------------+ | OPC-DA (IIoT) | https://github.com/Hurence/logisland | None (Built in) | +--------------------------+-------------------------+--------------------------------------------------------+------------------------------+ +| OPC-UA (IIoT) | https://github.com/Hurence/logisland | None (Built in) | ++--------------------------+-------------------------+--------------------------------------------------------+------------------------------+ | FTP | https://github.com/Eneco/kafka-connect-ftp | -DwithConnectFtp | +--------------------------+----------------------------------------------------------------------------------+------------------------------+ | Blockchain | https://github.com/Landoop/stream-reactor/tree/master/kafka-connect-blockchain | -DwithConnectBlockchain | From f8c6588c7ca6f77702fb70f765a1dd20de180cdc Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 29 Jun 2018 16:10:10 +0200 Subject: [PATCH 44/63] Use opc-simple stable release --- .../logisland-connectors/logisland-connector-opc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index 33f891817..49d320070 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -16,7 +16,7 @@ com.github.Hurence opc-simple - develop-1.1.2-g6c92e27-20 + 1.2.0 com.hurence.logisland From 39719dfd55163342a6a32ba739c64feff82e74e7 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 2 Jul 2018 11:35:49 +0200 Subject: [PATCH 45/63] Shutting up silly info logging --- .../logisland/connect/source/SharedSourceTaskContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java index 51ff13f8d..300fbbd65 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java @@ -143,7 +143,7 @@ public void commit(Offset endOffset) { if (offsetStorageWriter.beginFlush()) { offsetStorageWriter.doFlush((error, result) -> { if (error == null) { - LOGGER.info("Flushing till offset {} with result {}", endOffset, result); + LOGGER.debug("Flushing till offset {} with result {}", endOffset, result); } else { LOGGER.error("Unable to commit records till source offset " + endOffset, error); From db46b07fa6c563f81d2136ddd1170a779ba456b5 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 2 Jul 2018 11:36:41 +0200 Subject: [PATCH 46/63] Add proper kafka structured streaming sink. Now supports microbatching --- .../provider/KafkaStreamWriter.scala | 48 +++++++ ...KafkaStructuredStreamProviderService.scala | 125 +++++++++++++++--- .../logisland/engine/StreamDebuggerTest.java | 1 + 3 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala new file mode 100644 index 000000000..26807feb0 --- /dev/null +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala @@ -0,0 +1,48 @@ +/* + * * Copyright (C) 2018 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.hurence.logisland.stream.spark.structured.provider + +import com.hurence.logisland.util.kafka.KafkaSink +import org.apache.spark.broadcast.Broadcast +import org.apache.spark.sql.execution.streaming.Sink +import org.apache.spark.sql.sources.StreamSinkProvider +import org.apache.spark.sql.streaming.OutputMode +import org.apache.spark.sql.{DataFrame, SQLContext} + +/** + * A Kafka Sink + * @param kafkaSink the broadcasted sink + * @param topics the topics to send to (comma separated) + */ +class KafkaStreamWriter(kafkaSink: Broadcast[KafkaSink], topics: String) extends Sink with Serializable { + + override def addBatch(batchId: Long, data: DataFrame): Unit = { + + data.sparkSession.createDataFrame( + data.sparkSession.sparkContext.parallelize(data.collect()), data.schema) + .foreach(row => kafkaSink.value.send(topics, row.getAs[Array[Byte]](0), row.getAs[Array[Byte]](1))) + } +} + +class KafkaStreamWriterProvider extends StreamSinkProvider { + override def createSink(sqlContext: SQLContext, parameters: Map[String, String], + partitionColumns: Seq[String], outputMode: OutputMode): Sink = { + new KafkaStreamWriter(sqlContext.sparkContext.broadcast(KafkaSink(parameters)), + parameters.getOrElse("path", throw new IllegalArgumentException("path argument must indicate the output topics"))) + } +} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala index 489c43cd6..02d44fcb1 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala @@ -17,8 +17,10 @@ package com.hurence.logisland.stream.spark.structured.provider +import java.io.{File, IOException} +import java.nio.file.{Files, Paths} import java.util -import java.util.Collections +import java.util.{Collections, UUID} import com.hurence.logisland.annotation.lifecycle.OnEnabled import com.hurence.logisland.component.{InitializationException, PropertyDescriptor} @@ -26,15 +28,13 @@ import com.hurence.logisland.controller.{AbstractControllerService, ControllerSe import com.hurence.logisland.record.{FieldDictionary, FieldType, Record, StandardRecord} import com.hurence.logisland.stream.StreamContext import com.hurence.logisland.stream.StreamProperties._ -import com.hurence.logisland.util.kafka.KafkaSink import kafka.admin.AdminUtils import kafka.utils.ZkUtils import org.apache.kafka.clients.consumer.ConsumerConfig import org.apache.kafka.clients.producer.ProducerConfig import org.apache.kafka.common.security.JaasUtils import org.apache.kafka.common.serialization.{ByteArrayDeserializer, ByteArraySerializer} -import org.apache.spark.sql.{Dataset, ForeachWriter, SparkSession} - +import org.apache.spark.sql.{Dataset, Encoders, SparkSession} class KafkaStructuredStreamProviderService() extends AbstractControllerService with StructuredStreamProviderService { @@ -222,31 +222,114 @@ class KafkaStructuredStreamProviderService() extends AbstractControllerService w * @return DataFrame currently loaded */ override def write(df: Dataset[Record], streamContext: StreamContext) = { - //implicit val myObjEncoder = org.apache.spark.sql.Encoders.tuple(Encoders.BINARY, Encoders.BINARY) - val writer = new ForeachWriter[Record] { - var sender: KafkaSink = _ + implicit val myObjEncoder = org.apache.spark.sql.Encoders.tuple(Encoders.BINARY, Encoders.BINARY) + + //val sender = KafkaSink(kafkaSinkParams) + df.map(record => (getOrElse(record, FieldDictionary.RECORD_KEY, new Array[Byte](0)), + getOrElse(record, FieldDictionary.RECORD_VALUE, new Array[Byte](0)))) + .writeStream + .format("com.hurence.logisland.stream.spark.structured.provider.KafkaStreamWriterProvider") + .options(kafkaSinkParams.mapValues(value => value.toString)) + .option("checkpointLocation", createTempDir(namePrefix = s"temporary").getCanonicalPath) + .start(outputTopics.mkString(",")) + } + + private def getOrElse[T](record: Record, field: String, defaultValue: T): T = { + val value = record.getField(field) + if (value != null && value.isSet) { + return value.getRawValue.asInstanceOf[T] + } + defaultValue + } + + /** + * Create a temporary directory inside the given parent directory. The directory will be + * automatically deleted when the VM shuts down. + */ + private def createTempDir(root: String = System.getProperty("java.io.tmpdir"), + namePrefix: String = "spark"): File = { + val dir = createDirectory(root, namePrefix) + sys.addShutdownHook(() => deleteRecursively(dir.getAbsoluteFile)) + dir + } - override def open(partitionId: Long, version: Long) = { - sender = KafkaSink(kafkaSinkParams) - true + /** + * Create a directory inside the given parent directory. The directory is guaranteed to be + * newly created, and is not marked for automatic deletion. + */ + private def createDirectory(root: String, namePrefix: String = "spark"): File = { + var attempts = 0 + val maxAttempts = 3 + var dir: File = null + while (dir == null) { + attempts += 1 + if (attempts > maxAttempts) { + throw new IOException("Failed to create a temp directory (under " + root + ") after " + + maxAttempts + " attempts!") + } + try { + dir = new File(root, namePrefix + "-" + UUID.randomUUID.toString) + if (dir.exists() || !dir.mkdirs()) { + dir = null + } + } catch { + case e: SecurityException => dir = null; } + } + + dir.getCanonicalFile + } - override def process(value: Record) = { - sender.send(outputTopics.mkString(","), - value.getField(FieldDictionary.RECORD_KEY).getRawValue().asInstanceOf[Array[Byte]], - value.getField(FieldDictionary.RECORD_VALUE).getRawValue().asInstanceOf[Array[Byte]]) + private def listFilesSafely(file: File): Seq[File] = { + if (file.exists()) { + val files = file.listFiles() + if (files == null) { + throw new IOException("Failed to list files for dir: " + file) } + files + } else { + List() + } + } - override def close(errorOrNull: Throwable) = { - if (errorOrNull != null) { - logger.error("Error occurred", errorOrNull) + /** + * Delete a file or directory and its contents recursively. + * Don't follow directories if they are symlinks. + * Throws an exception if deletion is unsuccessful. + */ + private def deleteRecursively(file: File) { + if (file != null) { + try { + if (file.isDirectory && !isSymlink(file)) { + var savedIOException: IOException = null + for (child <- listFilesSafely(file)) { + try { + deleteRecursively(child) + } catch { + // In case of multiple exceptions, only last one will be thrown + case ioe: IOException => savedIOException = ioe + } + } + if (savedIOException != null) { + throw savedIOException + } + } + } finally { + if (!file.delete()) { + // Delete can also fail if the file simply did not exist + if (file.exists()) { + throw new IOException("Failed to delete: " + file.getAbsolutePath) + } } - sender.producer.close(); } } + } - df.writeStream - .foreach(writer) - .start() + /** + * Check to see if file is a symbolic link. + */ + private def isSymlink(file: File): Boolean = { + return Files.isSymbolicLink(Paths.get(file.toURI)) } + } diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/StreamDebuggerTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/StreamDebuggerTest.java index 89c58d565..562ccdbf7 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/StreamDebuggerTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/StreamDebuggerTest.java @@ -75,6 +75,7 @@ public void remoteTest() { EngineContext engineContext = engineInstance.get(); engineInstance.get().getEngine().start(engineContext); SparkUtils.customizeLogLevels(); + engineContext.getEngine().awaitTermination(engineContext); } catch (Exception e) { logger.error("something went bad while running the job : {}", e); System.exit(-1); From 3487f2330a362a32ff384daf2c54c0cd539f50f0 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 2 Jul 2018 12:35:32 +0200 Subject: [PATCH 47/63] Add documentation --- .../tutorials/opc-iiot.yml | 106 +++++++++ .../src/main/resources/conf/opc-iiot.yml | 106 +++++++++ .../resources/docs/tutorials/iiot-opc-ua.rst | 206 ++++++++++++++++++ 3 files changed, 418 insertions(+) create mode 100644 logisland-documentation/tutorials/opc-iiot.yml create mode 100644 logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml create mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst diff --git a/logisland-documentation/tutorials/opc-iiot.yml b/logisland-documentation/tutorials/opc-iiot.yml new file mode 100644 index 000000000..9a48cfe0e --- /dev/null +++ b/logisland-documentation/tutorials/opc-iiot.yml @@ -0,0 +1,106 @@ +version: 0.13.0 +documentation: LogIsland IIoT OPC-UA Job + +engine: + component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine + type: engine + documentation: Index some OPC-UA tagw with Logisland + configuration: + spark.app.name: OpcUaLogisland + spark.master: local[*] + spark.driver.memory: 512M + spark.driver.cores: 1 + spark.executor.memory: 512M + spark.executor.instances: 4 + spark.executor.cores: 1 + spark.yarn.queue: default + spark.yarn.maxAppAttempts: 4 + spark.yarn.am.attemptFailuresValidityInterval: 1h + spark.yarn.max.executor.failures: 20 + spark.yarn.executor.failuresValidityInterval: 1h + spark.task.maxFailures: 8 + spark.serializer: org.apache.spark.serializer.KryoSerializer + spark.streaming.batchDuration: 3000 + spark.streaming.backpressure.enabled: false + spark.streaming.blockInterval: 500 + spark.streaming.kafka.maxRatePerPartition: 10000 + spark.streaming.timeout: -1 + spark.streaming.unpersist: false + spark.streaming.kafka.maxRetries: 3 + spark.streaming.ui.retainedBatches: 200 + spark.streaming.receiver.writeAheadLog.enable: false + spark.ui.port: 4040 + + controllerServiceConfigurations: + + - controllerService: elasticsearch_service + component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService + type: service + documentation: elasticsearch service + configuration: + hosts: sandbox:9300 + cluster.name: es-logisland + + - controllerService: console_service + component: com.hurence.logisland.stream.spark.structured.provider.ConsoleStructuredStreamProviderService + + - controllerService: kc_source_service + component: com.hurence.logisland.stream.spark.provider.KafkaConnectStructuredProviderService + documentation: Kafka connect OPC-UA source service + type: service + configuration: + kc.connector.class: com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector + kc.data.value.converter: com.hurence.logisland.connect.converter.LogIslandRecordConverter + kc.data.value.converter.properties: | + record.serializer=com.hurence.logisland.serializer.KryoSerializer + kc.data.key.converter.properties: | + schemas.enable=false + kc.data.key.converter: org.apache.kafka.connect.storage.StringConverter + kc.worker.tasks.max: 1 + kc.connector.offset.backing.store: memory + kc.connector.properties: | + defaultRefreshPeriodMillis=500 + dataPublicationPeriodMillis=1000 + defaultSocketTimeoutMillis=10000 + server.uri=opc.tcp://sandbox:53530/OPCUA/SimulationServer + tags=ns=5;s=Sawtooth1 + + streamConfigurations: + + - stream: ingest_stream + component: com.hurence.logisland.stream.spark.structured.StructuredStream + configuration: + read.topics: /a/in + read.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + read.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + read.topics.client.service: kc_source_service + write.topics: logisland_parsed + write.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + write.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + write.topics.client.service: console_service + processorConfigurations: + - processor: flatten + component: com.hurence.logisland.processor.FlatMap + type: processor + documentation: "extract from root record" + configuration: + keep.root.record: false + copy.root.record.fields: true + - processor: rename_fields + component: com.hurence.logisland.processor.NormalizeFields + type: processor + documentation: "set record time to tag server generation time" + configuration: + conflict.resolution.policy: overwrite_existing + record_time: tag_timestamp + - processor: es_publisher + component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch + type: processor + documentation: a processor that indexes processed events in elasticsearch + configuration: + elasticsearch.client.service: elasticsearch_service + default.index: logisland + default.type: event + timebased.index: yesterday + es.index.field: search_index + es.type.field: record_type diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml b/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml new file mode 100644 index 000000000..9a48cfe0e --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml @@ -0,0 +1,106 @@ +version: 0.13.0 +documentation: LogIsland IIoT OPC-UA Job + +engine: + component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine + type: engine + documentation: Index some OPC-UA tagw with Logisland + configuration: + spark.app.name: OpcUaLogisland + spark.master: local[*] + spark.driver.memory: 512M + spark.driver.cores: 1 + spark.executor.memory: 512M + spark.executor.instances: 4 + spark.executor.cores: 1 + spark.yarn.queue: default + spark.yarn.maxAppAttempts: 4 + spark.yarn.am.attemptFailuresValidityInterval: 1h + spark.yarn.max.executor.failures: 20 + spark.yarn.executor.failuresValidityInterval: 1h + spark.task.maxFailures: 8 + spark.serializer: org.apache.spark.serializer.KryoSerializer + spark.streaming.batchDuration: 3000 + spark.streaming.backpressure.enabled: false + spark.streaming.blockInterval: 500 + spark.streaming.kafka.maxRatePerPartition: 10000 + spark.streaming.timeout: -1 + spark.streaming.unpersist: false + spark.streaming.kafka.maxRetries: 3 + spark.streaming.ui.retainedBatches: 200 + spark.streaming.receiver.writeAheadLog.enable: false + spark.ui.port: 4040 + + controllerServiceConfigurations: + + - controllerService: elasticsearch_service + component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService + type: service + documentation: elasticsearch service + configuration: + hosts: sandbox:9300 + cluster.name: es-logisland + + - controllerService: console_service + component: com.hurence.logisland.stream.spark.structured.provider.ConsoleStructuredStreamProviderService + + - controllerService: kc_source_service + component: com.hurence.logisland.stream.spark.provider.KafkaConnectStructuredProviderService + documentation: Kafka connect OPC-UA source service + type: service + configuration: + kc.connector.class: com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector + kc.data.value.converter: com.hurence.logisland.connect.converter.LogIslandRecordConverter + kc.data.value.converter.properties: | + record.serializer=com.hurence.logisland.serializer.KryoSerializer + kc.data.key.converter.properties: | + schemas.enable=false + kc.data.key.converter: org.apache.kafka.connect.storage.StringConverter + kc.worker.tasks.max: 1 + kc.connector.offset.backing.store: memory + kc.connector.properties: | + defaultRefreshPeriodMillis=500 + dataPublicationPeriodMillis=1000 + defaultSocketTimeoutMillis=10000 + server.uri=opc.tcp://sandbox:53530/OPCUA/SimulationServer + tags=ns=5;s=Sawtooth1 + + streamConfigurations: + + - stream: ingest_stream + component: com.hurence.logisland.stream.spark.structured.StructuredStream + configuration: + read.topics: /a/in + read.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + read.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + read.topics.client.service: kc_source_service + write.topics: logisland_parsed + write.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + write.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + write.topics.client.service: console_service + processorConfigurations: + - processor: flatten + component: com.hurence.logisland.processor.FlatMap + type: processor + documentation: "extract from root record" + configuration: + keep.root.record: false + copy.root.record.fields: true + - processor: rename_fields + component: com.hurence.logisland.processor.NormalizeFields + type: processor + documentation: "set record time to tag server generation time" + configuration: + conflict.resolution.policy: overwrite_existing + record_time: tag_timestamp + - processor: es_publisher + component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch + type: processor + documentation: a processor that indexes processed events in elasticsearch + configuration: + elasticsearch.client.service: elasticsearch_service + default.index: logisland + default.type: event + timebased.index: yesterday + es.index.field: search_index + es.type.field: record_type diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst new file mode 100644 index 000000000..c41e9762e --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst @@ -0,0 +1,206 @@ +IIoT with OOPC and Logisland +============================ + +In this tutorial we'll show you how to ingest IIoT data from an OPC-UA server and process it with Logisland, storing everything into an elasticsearch database. + +In particular, we'll use the Prosys OPC-UA simulation server you can download for free `here `_ + + +.. note:: + + You will need to have a logisland Docker environment. Please follow the `prerequisites <./prerequisites.html>`_ section for more information. + + +Please also remember to always turn on the simulation server before running the logisland job. + + + +1. Logisland job setup +---------------------- +The logisland job for this tutorial is already packaged in the tar.gz assembly and you can find it here for ElasticSearch : + +.. code-block:: sh + + docker exec -i -t logisland vim conf/opc-iiot.yml + + +We will start by explaining each part of the config file. + +The first section configures the Spark engine (we will use a `KafkaStreamProcessingEngine <../plugins.html#kafkastreamprocessingengine>`_) to run in local mode with 1 cpu cores and 512M of RAM. + +.. code-block:: yaml + + engine: + component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine + type: engine + documentation: Index some OPC-UA tagw with Logisland + configuration: + spark.app.name: OpcUaLogisland + spark.master: local[*] + spark.driver.memory: 512M + spark.driver.cores: 1 + spark.executor.memory: 512M + spark.executor.instances: 4 + spark.executor.cores: 1 + spark.yarn.queue: default + spark.yarn.maxAppAttempts: 4 + spark.yarn.am.attemptFailuresValidityInterval: 1h + spark.yarn.max.executor.failures: 20 + spark.yarn.executor.failuresValidityInterval: 1h + spark.task.maxFailures: 8 + spark.serializer: org.apache.spark.serializer.KryoSerializer + spark.streaming.batchDuration: 3000 + spark.streaming.backpressure.enabled: false + spark.streaming.blockInterval: 500 + spark.streaming.kafka.maxRatePerPartition: 10000 + spark.streaming.timeout: -1 + spark.streaming.unpersist: false + spark.streaming.kafka.maxRetries: 3 + spark.streaming.ui.retainedBatches: 200 + spark.streaming.receiver.writeAheadLog.enable: false + spark.ui.port: 4040 + +The `controllerServiceConfigurations` part is here to define all services that be shared by processors within the whole job. + +Here we have the OPC-UA source with all the connection parameters. + +.. code-block:: yaml + + - controllerService: kc_source_service + component: com.hurence.logisland.stream.spark.provider.KafkaConnectStructuredProviderService + documentation: Kafka connect OPC-UA source service + type: service + configuration: + kc.connector.class: com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector + kc.data.value.converter: com.hurence.logisland.connect.converter.LogIslandRecordConverter + kc.data.value.converter.properties: | + record.serializer=com.hurence.logisland.serializer.KryoSerializer + kc.data.key.converter.properties: | + schemas.enable=false + kc.data.key.converter: org.apache.kafka.connect.storage.StringConverter + kc.worker.tasks.max: 1 + kc.connector.offset.backing.store: memory + kc.connector.properties: | + defaultRefreshPeriodMillis=500 + dataPublicationPeriodMillis=1000 + defaultSocketTimeoutMillis=10000 + server.uri=opc.tcp://sandbox:53530/OPCUA/SimulationServer + tags=ns=5;s=Sawtooth1 + +In particular, we have + +* A tag to be read: *"ns=5;s=Sawtooth1"* +* The tag will be subscribed to be refreshed each 0.5s *(defaultRefreshPeriodMillis)* +* The data will be published by the opc server each second (*dataPublicationPeriodMillis*) + +Full connector documentation is on javadoc of class ``com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector`` + + +Then we also define her Elasticsearch service that will be used later in the ``BulkAddElasticsearch`` processor. + +.. code-block:: yaml + + - controllerService: elasticsearch_service + component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService + type: service + documentation: elasticsearch service + configuration: + hosts: sandbox:9300 + cluster.name: es-logisland + batch.size: 5000 + + +Inside this engine you will run a spark structured stream, taking records from the previously defined source and letting data flow through the processing pipeline till the console output. + +.. code-block:: yaml + + - stream: ingest_stream + component: com.hurence.logisland.stream.spark.structured.StructuredStream + configuration: + read.topics: /a/in + read.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + read.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + read.topics.client.service: kc_source_service + write.topics: logisland_parsed + write.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + write.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + write.topics.client.service: console_service + + +And now it's time to describe the parsing pipeline. + +First, we need to extract the record thanks to a ``FlatMap`` processor + +.. code-block:: yaml + + - processor: flatten + component: com.hurence.logisland.processor.FlatMap + type: processor + documentation: "extract from root record" + configuration: + keep.root.record: false + copy.root.record.fields: true + +Now that the record is well-formed, we want to set the record time to be the same of the one given by the source (and stored on the field *tag_timestamp*). + +For this, we use a ``NormalizeFields`` processor. + +.. code-block:: yaml + + - processor: rename_fields + component: com.hurence.logisland.processor.NormalizeFields + type: processor + documentation: "set record time to tag server generation time" + configuration: + conflict.resolution.policy: overwrite_existing + record_time: tag_timestamp + +Then, the last processor will index our records into elasticsearch + +.. code-block:: yaml + + # add to elasticsearch + - processor: es_publisher + component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch + type: processor + documentation: a processor that trace the processed events + configuration: + elasticsearch.client.service: elasticsearch_service + default.index: logisland + default.type: event + timebased.index: yesterday + es.index.field: search_index + es.type.field: record_type + + +2. Launch the script +-------------------- +Just ensure the Prosys OPC-UA server is up and running and that on the *Simulation* tab the simulation is ticked. + +Then you can execute: + +.. code-block:: sh + + docker exec -i -t logisland bin/logisland.sh --conf conf/opc-iiot.yml + + + + +3. Inspect the records +---------------------- + + +With ElasticSearch, you can use Kibana. + +Open up your browser and go to `http://sandbox:5601/ `_ and you should be able to explore your apache logs. + + +Configure a new index pattern with ``logisland.*`` as the pattern name and ``@timestamp`` as the time value field. + +.. image:: /_static/kibana-configure-index.png + +Then if you go to Explore panel for the latest 15' time window you'll only see logisland process_metrics events which give you +insights about the processing bandwidth of your streams. + + + From d023db3a343761571fbb1223f8ddedc8ffc751e0 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 2 Jul 2018 16:20:44 +0200 Subject: [PATCH 48/63] Add WARN loglevel for jinterop --- .../src/main/resources/conf/log4j.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/log4j.properties b/logisland-framework/logisland-resources/src/main/resources/conf/log4j.properties index 1c185d567..ffaddcf2a 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/log4j.properties +++ b/logisland-framework/logisland-resources/src/main/resources/conf/log4j.properties @@ -47,7 +47,8 @@ log4j.logger.kafka=WARN log4j.logger.org.elasticsearch=WARN log4j.logger.com.hurence=DEBUG +log4j.logger.org.jinterop=WARN -# log4j.logger.org.apache.spark.deploy.yarn.Client=DEBUG \ No newline at end of file +# log4j.logger.org.apache.spark.deploy.yarn.Client=DEBUG From 1f013286ae60e57f914489ed051b064841174bc5 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 3 Jul 2018 16:26:03 +0200 Subject: [PATCH 49/63] Fix typos in doc --- .../tutorials/iiot-opc-ua.rst | 206 ++++++++++++++++++ logisland-documentation/tutorials/index.rst | 1 + .../tutorials/opc-iiot.yml | 106 --------- .../resources/docs/tutorials/iiot-opc-ua.rst | 4 +- 4 files changed, 209 insertions(+), 108 deletions(-) create mode 100644 logisland-documentation/tutorials/iiot-opc-ua.rst delete mode 100644 logisland-documentation/tutorials/opc-iiot.yml diff --git a/logisland-documentation/tutorials/iiot-opc-ua.rst b/logisland-documentation/tutorials/iiot-opc-ua.rst new file mode 100644 index 000000000..e8bcf1106 --- /dev/null +++ b/logisland-documentation/tutorials/iiot-opc-ua.rst @@ -0,0 +1,206 @@ +IIoT with OPC and Logisland +=========================== + +In this tutorial we'll show you how to ingest IIoT data from an OPC-UA server and process it with Logisland, storing everything into an elasticsearch database. + +In particular, we'll use the Prosys OPC-UA simulation server you can download for free `here `_ + + +.. note:: + + You will need to have a logisland Docker environment. Please follow the `prerequisites <./prerequisites.html>`_ section for more information. + + +Please also remember to always turn on the simulation server before running the logisland job. + + + +1. Logisland job setup +---------------------- +The logisland job for this tutorial is already packaged in the tar.gz assembly and you can find it here for ElasticSearch : + +.. code-block:: sh + + docker exec -i -t logisland vim conf/opc-iiot.yml + + +We will start by explaining each part of the config file. + +The first section configures the Spark engine (we will use a `KafkaStreamProcessingEngine <../plugins.html#kafkastreamprocessingengine>`_) to run in local mode with 1 cpu cores and 512M of RAM. + +.. code-block:: yaml + + engine: + component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine + type: engine + documentation: Index some OPC-UA tagw with Logisland + configuration: + spark.app.name: OpcUaLogisland + spark.master: local[*] + spark.driver.memory: 512M + spark.driver.cores: 1 + spark.executor.memory: 512M + spark.executor.instances: 4 + spark.executor.cores: 1 + spark.yarn.queue: default + spark.yarn.maxAppAttempts: 4 + spark.yarn.am.attemptFailuresValidityInterval: 1h + spark.yarn.max.executor.failures: 20 + spark.yarn.executor.failuresValidityInterval: 1h + spark.task.maxFailures: 8 + spark.serializer: org.apache.spark.serializer.KryoSerializer + spark.streaming.batchDuration: 3000 + spark.streaming.backpressure.enabled: false + spark.streaming.blockInterval: 500 + spark.streaming.kafka.maxRatePerPartition: 10000 + spark.streaming.timeout: -1 + spark.streaming.unpersist: false + spark.streaming.kafka.maxRetries: 3 + spark.streaming.ui.retainedBatches: 200 + spark.streaming.receiver.writeAheadLog.enable: false + spark.ui.port: 4040 + +The `controllerServiceConfigurations` part is here to define all services that be shared by processors within the whole job. + +Here we have the OPC-UA source with all the connection parameters. + +.. code-block:: yaml + + - controllerService: kc_source_service + component: com.hurence.logisland.stream.spark.provider.KafkaConnectStructuredProviderService + documentation: Kafka connect OPC-UA source service + type: service + configuration: + kc.connector.class: com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector + kc.data.value.converter: com.hurence.logisland.connect.converter.LogIslandRecordConverter + kc.data.value.converter.properties: | + record.serializer=com.hurence.logisland.serializer.KryoSerializer + kc.data.key.converter.properties: | + schemas.enable=false + kc.data.key.converter: org.apache.kafka.connect.storage.StringConverter + kc.worker.tasks.max: 1 + kc.connector.offset.backing.store: memory + kc.connector.properties: | + defaultRefreshPeriodMillis=500 + dataPublicationPeriodMillis=1000 + defaultSocketTimeoutMillis=10000 + server.uri=opc.tcp://sandbox:53530/OPCUA/SimulationServer + tags=ns=5;s=Sawtooth1 + +In particular, we have + +* A tag to be read: *"ns=5;s=Sawtooth1"* +* The tag will be subscribed to be refreshed each 0.5s *(defaultRefreshPeriodMillis)* +* The data will be published by the opc server each second (*dataPublicationPeriodMillis*) + +Full connector documentation is on javadoc of class ``com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector`` + + +Then we also define her Elasticsearch service that will be used later in the ``BulkAddElasticsearch`` processor. + +.. code-block:: yaml + + - controllerService: elasticsearch_service + component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService + type: service + documentation: elasticsearch service + configuration: + hosts: sandbox:9300 + cluster.name: es-logisland + batch.size: 5000 + + +Inside this engine you will run a spark structured stream, taking records from the previously defined source and letting data flow through the processing pipeline till the console output. + +.. code-block:: yaml + + - stream: ingest_stream + component: com.hurence.logisland.stream.spark.structured.StructuredStream + configuration: + read.topics: /a/in + read.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + read.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + read.topics.client.service: kc_source_service + write.topics: logisland_parsed + write.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + write.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer + write.topics.client.service: console_service + + +And now it's time to describe the parsing pipeline. + +First, we need to extract the record thanks to a ``FlatMap`` processor + +.. code-block:: yaml + + - processor: flatten + component: com.hurence.logisland.processor.FlatMap + type: processor + documentation: "extract from root record" + configuration: + keep.root.record: false + copy.root.record.fields: true + +Now that the record is well-formed, we want to set the record time to be the same of the one given by the source (and stored on the field *tag_timestamp*). + +For this, we use a ``NormalizeFields`` processor. + +.. code-block:: yaml + + - processor: rename_fields + component: com.hurence.logisland.processor.NormalizeFields + type: processor + documentation: "set record time to tag server generation time" + configuration: + conflict.resolution.policy: overwrite_existing + record_time: tag_timestamp + +Then, the last processor will index our records into elasticsearch + +.. code-block:: yaml + + # add to elasticsearch + - processor: es_publisher + component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch + type: processor + documentation: a processor that trace the processed events + configuration: + elasticsearch.client.service: elasticsearch_service + default.index: logisland + default.type: event + timebased.index: yesterday + es.index.field: search_index + es.type.field: record_type + + +2. Launch the script +-------------------- +Just ensure the Prosys OPC-UA server is up and running and that on the *Simulation* tab the simulation is ticked. + +Then you can execute: + +.. code-block:: sh + + docker exec -i -t logisland bin/logisland.sh --conf conf/opc-iiot.yml + + + + +3. Inspect the records +---------------------- + + +With ElasticSearch, you can use Kibana. + +Open up your browser and go to `http://sandbox:5601/ `_ and you should be able to explore your apache logs. + + +Configure a new index pattern with ``logisland.*`` as the pattern name and ``@timestamp`` as the time value field. + +.. image:: /_static/kibana-configure-index.png + +Then if you go to Explore panel for the latest 15' time window you'll only see logisland process_metrics events which give you +insights about the processing bandwidth of your streams. + + + diff --git a/logisland-documentation/tutorials/index.rst b/logisland-documentation/tutorials/index.rst index a9f443d19..e13f859b1 100644 --- a/logisland-documentation/tutorials/index.rst +++ b/logisland-documentation/tutorials/index.rst @@ -36,4 +36,5 @@ Contents: index-blockchain-transactions index-excel-spreadsheets mqtt-to-historian + iiot-opc-ua integrate-kafka-connect diff --git a/logisland-documentation/tutorials/opc-iiot.yml b/logisland-documentation/tutorials/opc-iiot.yml deleted file mode 100644 index 9a48cfe0e..000000000 --- a/logisland-documentation/tutorials/opc-iiot.yml +++ /dev/null @@ -1,106 +0,0 @@ -version: 0.13.0 -documentation: LogIsland IIoT OPC-UA Job - -engine: - component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine - type: engine - documentation: Index some OPC-UA tagw with Logisland - configuration: - spark.app.name: OpcUaLogisland - spark.master: local[*] - spark.driver.memory: 512M - spark.driver.cores: 1 - spark.executor.memory: 512M - spark.executor.instances: 4 - spark.executor.cores: 1 - spark.yarn.queue: default - spark.yarn.maxAppAttempts: 4 - spark.yarn.am.attemptFailuresValidityInterval: 1h - spark.yarn.max.executor.failures: 20 - spark.yarn.executor.failuresValidityInterval: 1h - spark.task.maxFailures: 8 - spark.serializer: org.apache.spark.serializer.KryoSerializer - spark.streaming.batchDuration: 3000 - spark.streaming.backpressure.enabled: false - spark.streaming.blockInterval: 500 - spark.streaming.kafka.maxRatePerPartition: 10000 - spark.streaming.timeout: -1 - spark.streaming.unpersist: false - spark.streaming.kafka.maxRetries: 3 - spark.streaming.ui.retainedBatches: 200 - spark.streaming.receiver.writeAheadLog.enable: false - spark.ui.port: 4040 - - controllerServiceConfigurations: - - - controllerService: elasticsearch_service - component: com.hurence.logisland.service.elasticsearch.Elasticsearch_5_4_0_ClientService - type: service - documentation: elasticsearch service - configuration: - hosts: sandbox:9300 - cluster.name: es-logisland - - - controllerService: console_service - component: com.hurence.logisland.stream.spark.structured.provider.ConsoleStructuredStreamProviderService - - - controllerService: kc_source_service - component: com.hurence.logisland.stream.spark.provider.KafkaConnectStructuredProviderService - documentation: Kafka connect OPC-UA source service - type: service - configuration: - kc.connector.class: com.hurence.logisland.connect.opc.ua.OpcUaSourceConnector - kc.data.value.converter: com.hurence.logisland.connect.converter.LogIslandRecordConverter - kc.data.value.converter.properties: | - record.serializer=com.hurence.logisland.serializer.KryoSerializer - kc.data.key.converter.properties: | - schemas.enable=false - kc.data.key.converter: org.apache.kafka.connect.storage.StringConverter - kc.worker.tasks.max: 1 - kc.connector.offset.backing.store: memory - kc.connector.properties: | - defaultRefreshPeriodMillis=500 - dataPublicationPeriodMillis=1000 - defaultSocketTimeoutMillis=10000 - server.uri=opc.tcp://sandbox:53530/OPCUA/SimulationServer - tags=ns=5;s=Sawtooth1 - - streamConfigurations: - - - stream: ingest_stream - component: com.hurence.logisland.stream.spark.structured.StructuredStream - configuration: - read.topics: /a/in - read.topics.serializer: com.hurence.logisland.serializer.KryoSerializer - read.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer - read.topics.client.service: kc_source_service - write.topics: logisland_parsed - write.topics.serializer: com.hurence.logisland.serializer.JsonSerializer - write.topics.key.serializer: com.hurence.logisland.serializer.StringSerializer - write.topics.client.service: console_service - processorConfigurations: - - processor: flatten - component: com.hurence.logisland.processor.FlatMap - type: processor - documentation: "extract from root record" - configuration: - keep.root.record: false - copy.root.record.fields: true - - processor: rename_fields - component: com.hurence.logisland.processor.NormalizeFields - type: processor - documentation: "set record time to tag server generation time" - configuration: - conflict.resolution.policy: overwrite_existing - record_time: tag_timestamp - - processor: es_publisher - component: com.hurence.logisland.processor.elasticsearch.BulkAddElasticsearch - type: processor - documentation: a processor that indexes processed events in elasticsearch - configuration: - elasticsearch.client.service: elasticsearch_service - default.index: logisland - default.type: event - timebased.index: yesterday - es.index.field: search_index - es.type.field: record_type diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst index c41e9762e..e8bcf1106 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/iiot-opc-ua.rst @@ -1,5 +1,5 @@ -IIoT with OOPC and Logisland -============================ +IIoT with OPC and Logisland +=========================== In this tutorial we'll show you how to ingest IIoT data from an OPC-UA server and process it with Logisland, storing everything into an elasticsearch database. From 3bb275cb8937ccadef673b03a95f21e58cf4207b Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 3 Jul 2018 16:29:14 +0200 Subject: [PATCH 50/63] Remove old files in logisland-assembly --- logisland-assembly/LICENSE | 1152 ---------------------------------- logisland-assembly/NOTICE | 926 --------------------------- logisland-assembly/README.md | 63 -- 3 files changed, 2141 deletions(-) delete mode 100644 logisland-assembly/LICENSE delete mode 100644 logisland-assembly/NOTICE delete mode 100644 logisland-assembly/README.md diff --git a/logisland-assembly/LICENSE b/logisland-assembly/LICENSE deleted file mode 100644 index 68af30331..000000000 --- a/logisland-assembly/LICENSE +++ /dev/null @@ -1,1152 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -APACHE NIFI SUBCOMPONENTS: - -The Apache NiFi project contains subcomponents with separate copyright -notices and license terms. Your use of the source code for the these -subcomponents is subject to the terms and conditions of the following -licenses. - -This product bundles source from 'Asciidoctor'. Specifically the 'asciidoc-mod.css'. -The source is available under an MIT LICENSE. - - Copyright (C) 2012-2015 Dan Allen, Ryan Waldron and the Asciidoctor Project - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -This product bundles 'Javascript D3 Library' which is available under a -"3-clause BSD" license. - - Copyright (c) 2010-2014, Michael Bostock - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * The name Michael Bostock may not be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -This product bundles 'CodeMirror' which is available under an MIT style license. - - Copyright (C) 2014 by Marijn Haverbeke and others - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -This product bundles 'JQuery' which is available under and MIT style license. - (c) 2005, 2014 jQuery Foundation, Inc. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -This product bundles 'JQuery Event Drag' which is available under an MIT style -license. - Copyright (c) 2008-2015 ThreeDubMedia - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -This product bundles 'jQuery Form Plugin' which is available under either the MIT -or GPL license. The license in effect here is the MIT license - - Copyright 2006-2013 (c) M. Alsup - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -This product bundles 'jQuery UI' which is available under an MIT style license. -For details see http://jqueryui.com - - Copyright 2014 jQuery Foundation and other contributors - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -This product bundles 'jquery.base64.js' which is available under an MIT style license. - - Copyright (c) 2013 Yannick Albert (http://yckart.com/) - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the "Software"), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -This product bundles 'SlickGrid v2.2' which is available under an MIT style license. - - Copyright (c) 2010 Michael Leibman, http://github.com/mleibman/slickgrid - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -This product bundles 'qTip2' which is available under an MIT style license. -For details see http://qtip2.com - - Copyright (c) 2012 Craig Michael Thompson - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - -This product bundles 'jQuery MiniColors' which is available under the MIT License. -For details see http://www.abeautifulsite.net/ - - Copyright Cory LaViska for A Beautiful Site, LLC. (http://www.abeautifulsite.net/) - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - -This product bundles 'jsoup' which is available under the MIT License. -For details see http://jsoup.org/ - - jsoup License - The jsoup code-com.hurence.logisland.logisland.parser.base (include source and compiled packages) are distributed under the open source MIT license as described below. - - The MIT License - Copyright © 2009 - 2013 Jonathan Hedley (jonathan@hedley.net) - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without - restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following - conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - - - -This product bundles 'json2.js' which is available in the 'public domain'. - For details see https://github.com/douglascrockford/JSON-js - -This product bundles 'reset.css' which is available in the 'public domain'. - For details see http://meyerweb.com/eric/tools/css/reset/ - -This product bundles HexViewJS available under an MIT License - - Copyright (c) 2010 Nick McVeity - - Permission is hereby granted, free of charge, to any person - obtaining a copy of this software and associated documentation - files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, merge, - publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, - subject to the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - The binary distribution of this product bundles 'Slf4j' which is available - under a "3-clause BSD" license. For details see http://www.slf4j.org/ - - Copyright (c) 2004-2013 QOS.ch - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - The binary distribution of this product bundles 'Antlr 3' which is available - under a "3-clause BSD" license. For details see http://www.antlr3.org/license.html - - Copyright (c) 2010 Terence Parr - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - Neither the name of the author nor the names of its contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'Bouncy Castle JDK 1.5 Provider' - under an MIT style license. - - Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - - The binary distribution of this product bundles 'XMLENC' which is available - under a BSD license. More details found here: http://xmlenc.sourceforge.net. - - Copyright 2003-2005, Ernst de Haan - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - 3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'Slf4j' which is available under - an MIT license. - - Copyright (c) 2004-2013 QOS.ch - All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - The binary distribution of this product bundles 'ParaNamer' and 'Paranamer Core' - which is available under a BSD style license. - - Copyright (c) 2006 Paul Hammant & ThoughtWorks Inc - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - 3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'Protocol Buffers - Google's data interchange format' - which is available under a BSD style license. - - Copyright 2008 Google Inc. All rights reserved. - http://code.google.com/p/protobuf/ - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following disclaimer - in the documentation and/or other materials provided with the - distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'JCraft Jsch' which is available - under a BSD style license. - Copyright (c) 2002-2015 Atsuhiko Yamanaka, JCraft,Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the distribution. - - 3. The names of the authors may not be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, - INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, - INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'Scala Library' under a BSD - style license. - - Copyright (c) 2002-2015 EPFL - Copyright (c) 2011-2015 Typesafe, Inc. - - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this list of - conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, this list of - conditions and the following disclaimer in the documentation and/or other materials - provided with the distribution. - - Neither the name of the EPFL nor the names of its contributors may be used to endorse - or promote products derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS - OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER - IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT - OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'JLine' under a BSD - style license. - - Copyright (c) 2002-2006, Marc Prud'hommeaux - All rights reserved. - - Redistribution and use in source and binary forms, with or - without modification, are permitted provided that the following - conditions are met: - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer - in the documentation and/or other materials provided with - the distribution. - - Neither the name of JLine nor the names of its contributors - may be used to endorse or promote products derived from this - software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, - BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, - OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED - AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED - OF THE POSSIBILITY OF SUCH DAMAGE. - - The binary distribution of this product bundles 'JOpt Simple' under an MIT - style license. - - Copyright (c) 2009 Paul R. Holser, Jr. - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - The binary distribution of this product bundles 'Jcodings' under an MIT style - license. - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is furnished to do - so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - The binary distribution of this product bundles 'Joni' under an MIT style - license. - - Permission is hereby granted, free of charge, to any person obtaining a copy of - this software and associated documentation files (the "Software"), to deal in - the Software without restriction, including without limitation the rights to - use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - of the Software, and to permit persons to whom the Software is furnished to do - so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - -The binary distribution of this product bundles 'Google Protocol Buffers Java 2.5.0' -which is licensed under a BSD license. - - This license applies to all parts of Protocol Buffers except the following: - - - Atomicops support for generic gcc, located in - src/google/protobuf/stubs/atomicops_internals_generic_gcc.h. - This file is copyrighted by Red Hat Inc. - - - Atomicops support for AIX/POWER, located in - src/google/protobuf/stubs/atomicops_internals_aix.h. - This file is copyrighted by Bloomberg Finance LP. - - Copyright 2014, Google Inc. All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following disclaimer - in the documentation and/or other materials provided with the - distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - Code generated by the Protocol Buffer compiler is owned by the owner - of the input file used when generating it. This code is not - standalone and requires a support library to be linked with it. This - support library is itself covered by the above license. - -This product bundles 'JCraft Jzlib' which is available under a 3-Clause BSD License. - - Copyright (c) 2002-2014 Atsuhiko Yamanaka, JCraft,Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the distribution. - - 3. The names of the authors may not be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, - INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND - FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, - INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -This product bundles 'asm' which is available under a 3-Clause BSD style license. -For details see http://asm.ow2.org/asmdex-license.html - - Copyright (c) 2012 France Télécom - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions - are met: - 1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - 3. Neither the name of the copyright holders nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - THE POSSIBILITY OF SUCH DAMAGE. - -The binary distribution of this product bundles 'Hamcrest' which is available -under a BSD license. More details found here: http://hamcrest.org. - - Copyright (c) 2000-2006, www.hamcrest.org - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this list of - conditions and the following disclaimer. Redistributions in binary form must reproduce - the above copyright notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the distribution. - - Neither the name of Hamcrest nor the names of its contributors may be used to endorse - or promote products derived from this software without specific prior written - permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY - EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT - SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, - INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED - TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY - WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH - DAMAGE. - -The binary distribution of this product bundles 'leveldbjni-all-1.8.jar' which is available - under a BSD style license - - Copyright (c) 2011 FuseSource Corp. All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are - met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following disclaimer - in the documentation and/or other materials provided with the - distribution. - * Neither the name of FuseSource Corp. nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The binary distribution of this product bundles 'Woodstox StAX 2 API' which is - "licensed under standard BSD license" - -This product bundles 'Adobe XMPCore' which is available under "The BSD license". More -information can be found here: http://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html - - Copyright (c) 2009, Adobe Systems Incorporated All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - * Neither the name of Adobe Systems Incorporated, nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANT ABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - THE POSSIBILITY OF SUCH DAMAGE. - -This product bundles 'Jsoup' which is available under "The MIT license". More -information can be found here: http://jsoup.org/license - - The MIT License - - Copyright (c) 2009-2015, Jonathan Hedley - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -This product bundles 'Luaj' which is available under an MIT style license. More -information can be found here: - - Copyright (c) 2009 Luaj.org. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -This product bundles 'jBCrypt' which is available under a BSD license. -For details see https://github.com/svenkubiak/jBCrypt/blob/0.4.1/LICENSE - - Copyright (c) 2006 Damien Miller - - Permission to use, copy, modify, and distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/logisland-assembly/NOTICE b/logisland-assembly/NOTICE deleted file mode 100644 index 06c5be409..000000000 --- a/logisland-assembly/NOTICE +++ /dev/null @@ -1,926 +0,0 @@ -Logisland -Copyright 2014-2016 Hurence - -This product includes software developed at -The Apache Software Foundation (http://www.apache.org/). - -This product includes the following work from the Apache Hadoop project: - -BoundedByteArrayOutputStream.java which was adapted to SoftLimitBoundedByteArrayOutputStream.java - -=========================================== -Apache Software License v2 -=========================================== - -The following binary components are provided under the Apache Software License v2 - - (ASLv2) Apache Commons IO - The following NOTICE information applies: - Apache Commons IO - Copyright 2002-2012 The Apache Software Foundation - - (ASLv2) Apache Commons Net - The following NOTICE information applies: - Apache Commons Net - Copyright 2001-2013 The Apache Software Foundation - - (ASLv2) Apache Commons Collections - The following NOTICE information applies: - Apache Commons Collections - Copyright 2001-2013 The Apache Software Foundation - - (ASLv2) Apache Commons Compress - The following NOTICE information applies: - Apache Commons Compress - Copyright 2002-2014 The Apache Software Foundation - - The files in the package org.apache.commons.compress.archivers.sevenz - were derived from the LZMA SDK, version 9.20 (C/ and CPP/7zip/), - which has been placed in the public domain: - - "LZMA SDK is placed in the public domain." (http://www.7-zip.org/sdk.html) - - (ASLv2) Jettison - The following NOTICE information applies: - Copyright 2006 Envoi Solutions LLC - - (ASLv2) Jasypt - The following NOTICE information applies: - Copyright (c) 2007-2010, The JASYPT team (http://www.jasypt.org) - - (ASLv2) Apache Commons Codec - The following NOTICE information applies: - Apache Commons Codec - Copyright 2002-2014 The Apache Software Foundation - - src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java - contains test data from http://aspell.net/test/orig/batch0.tab. - Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org) - - =============================================================================== - - The content of package org.apache.commons.codec.language.bm has been translated - from the original php source code available at http://stevemorse.org/phoneticinfo.htm - with permission from the original authors. - Original source copyright: - Copyright (c) 2008 Alexander Beider & Stephen P. Morse. - - (ASLv2) Apache HttpComponents - The following NOTICE information applies: - Apache HttpClient - Copyright 1999-2015 The Apache Software Foundation - - Apache HttpCore - Copyright 2005-2015 The Apache Software Foundation - - Apache HttpMime - Copyright 1999-2013 The Apache Software Foundation - - This project contains annotations derived from JCIP-ANNOTATIONS - Copyright (c) 2005 Brian Goetz and Tim Peierls. See http://www.jcip.net - - (ASLv2) Apache Jakarta HttpClient - The following NOTICE information applies: - Apache Jakarta HttpClient - Copyright 1999-2007 The Apache Software Foundation - - (ASLv2) Apache Commons Logging - The following NOTICE information applies: - Apache Commons Logging - Copyright 2003-2014 The Apache Software Foundation - - (ASLv2) Apache Commons Lang - The following NOTICE information applies: - Apache Commons Lang - Copyright 2001-2015 The Apache Software Foundation - - This product includes software from the Spring Framework, - under the Apache License 2.0 (see: StringUtils.containsWhitespace()) - - (ASLv2) Apache Commons Configuration - The following NOTICE information applies: - Apache Commons Configuration - Copyright 2001-2008 The Apache Software Foundation - - (ASLv2) Apache Commons JEXL - The following NOTICE information applies: - Apache Commons JEXL - Copyright 2001-2011 The Apache Software Foundation - - (ASLv2) Spring Framework - The following NOTICE information applies: - Spring Framework 4.1.4.RELEASE - Copyright (c) 2002-2015 Pivotal, Inc. - - (ASLv2) Spring Security - The following NOTICE information applies: - Spring Framework 4.0.3.RELEASE - Copyright (c) 2002-2015 Pivotal, Inc. - - (ASLv2) Apache Flume - The following NOTICE information applies: - Apache Flume - Copyright 2011-2015 Apache Software Foundation - - asynchbase is BSD-licensed software (https://github.com/OpenTSDB/asynchbase) - - async is BSD-licensed software (https://github.com/stumbleupon/async) - - jopt-simple is MIT licensed software (http://pholser.github.io/jopt-simple/license.html) - - scala-library is BSD-like licensed software (http://www.scala-lang.org/license.html) - - (ASLv2) Xalan - This product includes software developed by - The Apache Software Foundation (http://www.apache.org/). - - Portions of this software was originally based on the following: - - - software copyright (c) 1999-2002, Lotus Development Corporation., http://www.lotus.com. - - software copyright (c) 2001-2002, Sun Microsystems., http://www.sun.com. - - software copyright (c) 2003, IBM Corporation., http://www.ibm.com. - - voluntary contributions made by Ovidiu Predescu (ovidiu@cup.hp.com) on behalf of the - Apache Software Foundation and was originally developed at Hewlett Packard Company. - - (ASLv2) Apache XML Commons XML APIs - Copyright 2006 The Apache Software Foundation. - - This product includes software developed at - The Apache Software Foundation (http://www.apache.org/). - - Portions of this software were originally based on the following: - - software copyright (c) 1999, IBM Corporation., http://www.ibm.com. - - software copyright (c) 1999, Sun Microsystems., http://www.sun.com. - - software copyright (c) 2000 World Wide Web Consortium, http://www.w3.org - - (ASLv2) IRClib - The following NOTICE information applies: - IRClib -- A Java Internet Relay Chat library -- - Copyright (C) 2002 - 2006 Christoph Schwering - - (ASLv2) Jackson JSON component - The following NOTICE information applies: - # Jackson JSON component - - Jackson is a high-performance, Free/Open Source JSON processing library. - It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has - been in development since 2007. - It is currently developed by a community of developers, as well as supported - commercially by FasterXML.com. - - ## Licensing - - Jackson core and extension components may licensed under different licenses. - To find the details that apply to this artifact see the accompanying LICENSE file. - For more information, including possible other licensing options, contact - FasterXML.com (http://fasterxml.com). - - ## Credits - - A list of contributors may be found from CREDITS file, which is included - in some artifacts (usually source distributions); but is always available - from the source code management (SCM) system project uses. - - (ASLv2) Apache Thrift - The following NOTICE information applies: - Apache Thrift - Copyright 2006-2010 The Apache Software Foundation. - - (ASLv2) Apache MINA - The following NOTICE information applies: - Apache MINA Core - Copyright 2004-2011 Apache MINA Project - - (ASLv2) opencsv (net.sf.opencsv:opencsv:2.3) - - (ASLv2) Apache Velocity - The following NOTICE information applies: - Apache Velocity - Copyright (C) 2000-2007 The Apache Software Foundation - - (ASLv2) ZkClient - The following NOTICE information applies: - ZkClient - Copyright 2009 Stefan Groschupf - - (ASLv2) Apache Commons CLI - The following NOTICE information applies: - Apache Commons CLI - Copyright 2001-2009 The Apache Software Foundation - - (ASLv2) Apache Commons Math - The following NOTICE information applies: - Apache Commons Math - Copyright 2001-2012 The Apache Software Foundation - - This product includes software developed by - The Apache Software Foundation (http://www.apache.org/). - - =============================================================================== - - The BracketFinder (package org.apache.commons.math3.optimization.univariate) - and PowellOptimizer (package org.apache.commons.math3.optimization.general) - classes are based on the Python code in module "optimize.py" (version 0.5) - developed by Travis E. Oliphant for the SciPy library (http://www.scipy.org/) - Copyright © 2003-2009 SciPy Developers. - =============================================================================== - - The LinearConstraint, LinearObjectiveFunction, LinearOptimizer, - RelationShip, SimplexSolver and SimplexTableau classes in package - org.apache.commons.math3.optimization.linear include software developed by - Benjamin McCann (http://www.benmccann.com) and distributed with - the following copyright: Copyright 2009 Google Inc. - =============================================================================== - - This product includes software developed by the - University of Chicago, as Operator of Argonne National - Laboratory. - The LevenbergMarquardtOptimizer class in package - org.apache.commons.math3.optimization.general includes software - translated from the lmder, lmpar and qrsolv Fortran routines - from the Minpack package - Minpack Copyright Notice (1999) University of Chicago. All rights reserved - =============================================================================== - - The GraggBulirschStoerIntegrator class in package - org.apache.commons.math3.ode.nonstiff includes software translated - from the odex Fortran routine developed by E. Hairer and G. Wanner. - Original source copyright: - Copyright (c) 2004, Ernst Hairer - =============================================================================== - - The EigenDecompositionImpl class in package - org.apache.commons.math3.linear includes software translated - from some LAPACK Fortran routines. Original source copyright: - Copyright (c) 1992-2008 The University of Tennessee. All rights reserved. - =============================================================================== - - The MersenneTwister class in package org.apache.commons.math3.random - includes software translated from the 2002-01-26 version of - the Mersenne-Twister generator written in C by Makoto Matsumoto and Takuji - Nishimura. Original source copyright: - Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, - All rights reserved - =============================================================================== - - The LocalizedFormatsTest class in the unit tests is an adapted version of - the OrekitMessagesTest class from the orekit library distributed under the - terms of the Apache 2 licence. Original source copyright: - Copyright 2010 CS Systèmes d'Information - =============================================================================== - - The HermiteInterpolator class and its corresponding test have been imported from - the orekit library distributed under the terms of the Apache 2 licence. Original - source copyright: - Copyright 2010-2012 CS Systèmes d'Information - =============================================================================== - - The creation of the package "o.a.c.m.analysis.integration.gauss" was inspired - by an original code donated by Sébastien Brisard. - =============================================================================== - - (ASLv2) Apache log4j - The following NOTICE information applies: - Apache log4j - Copyright 2007 The Apache Software Foundation - - (ASLv2) Apache Tika - The following NOTICE information applies: - Apache Tika Core - Copyright 2007-2015 The Apache Software Foundation - - (ASLv2) Apache Jakarta Commons Digester - The following NOTICE information applies: - Apache Jakarta Commons Digester - Copyright 2001-2006 The Apache Software Foundation - - (ASLv2) Apache Commons BeanUtils - The following NOTICE information applies: - Apache Commons BeanUtils - Copyright 2000-2008 The Apache Software Foundation - - (ASLv2) Apache Avro - The following NOTICE information applies: - Apache Avro - Copyright 2009-2013 The Apache Software Foundation - - (ASLv2) Snappy Java - The following NOTICE information applies: - This product includes software developed by Google - Snappy: http://code.google.com/p/snappy/ (New BSD License) - - This product includes software developed by Apache - PureJavaCrc32C from apache-hadoop-common http://hadoop.apache.org/ - (Apache 2.0 license) - - This library containd statically linked libstdc++. This inclusion is allowed by - "GCC RUntime Library Exception" - http://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html - - (ASLv2) ApacheDS - The following NOTICE information applies: - ApacheDS - Copyright 2003-2013 The Apache Software Foundation - - (ASLv2) Apache ZooKeeper - The following NOTICE information applies: - Apache ZooKeeper - Copyright 2009-2012 The Apache Software Foundation - - (ASLv2) Apache Commons Daemon - The following NOTICE information applies: - Apache Commons Daemon - Copyright 1999-2013 The Apache Software Foundation - - (ASLv2) Apache Commons EL - The following NOTICE information applies: - Apache Commons EL - Copyright 1999-2007 The Apache Software Foundation - - EL-8 patch - Copyright 2004-2007 Jamie Taylor - http://issues.apache.org/jira/browse/EL-8 - - (ASLv2) Jetty - The following NOTICE information applies: - Jetty Web Container - Copyright 1995-2015 Mort Bay Consulting Pty Ltd. - - (ASLv2) Apache Tomcat - The following NOTICE information applies: - Apache Tomcat - Copyright 2007 The Apache Software Foundation - - Java Management Extensions (JMX) support is provided by - the MX4J package, which is open source software. The - original software and related information is available - at http://mx4j.sourceforge.net. - - Java compilation software for JSP pages is provided by Eclipse, - which is open source software. The orginal software and - related infomation is available at - http://www.eclipse.org. - - (ASLv2) Apache Kafka - The following NOTICE information applies: - Apache Kafka - Copyright 2012 The Apache Software Foundation. - - (ASLv2) Yammer Metrics - The following NOTICE information applies: - Metrics - Copyright 2010-2012 Coda Hale and Yammer, Inc. - - This product includes software developed by Coda Hale and Yammer, Inc. - - This product includes code derived from the JSR-166 project (ThreadLocalRandom), which was released - with the following comments: - - Written by Doug Lea with assistance from members of JCP JSR-166 - Expert Group and released to the public domain, as explained at - http://creativecommons.org/publicdomain/zero/1.0/ - - (ASLv2) Apache Lucene - The following NOTICE information applies: - Apache Lucene - Copyright 2014 The Apache Software Foundation - - Includes software from other Apache Software Foundation projects, - including, but not limited to: - - Apache Ant - - Apache Jakarta Regexp - - Apache Commons - - Apache Xerces - - ICU4J, (under analysis/icu) is licensed under an MIT styles license - and Copyright (c) 1995-2008 International Business Machines Corporation and others - - Some data files (under analysis/icu/src/data) are derived from Unicode data such - as the Unicode Character Database. See http://unicode.org/copyright.html for more - details. - - Brics Automaton (under core/src/java/org/apache/lucene/util/automaton) is - BSD-licensed, created by Anders Møller. See http://www.brics.dk/automaton/ - - The levenshtein automata tables (under core/src/java/org/apache/lucene/util/automaton) were - automatically generated with the moman/finenight FSA library, created by - Jean-Philippe Barrette-LaPierre. This library is available under an MIT license, - see http://sites.google.com/site/rrettesite/moman and - http://bitbucket.org/jpbarrette/moman/overview/ - - The class org.apache.lucene.util.WeakIdentityMap was derived from - the Apache CXF project and is Apache License 2.0. - - The Google Code Prettify is Apache License 2.0. - See http://code.google.com/p/google-code-prettify/ - - JUnit (junit-4.10) is licensed under the Common Public License v. 1.0 - See http://junit.sourceforge.net/cpl-v10.html - - This product includes code (JaspellTernarySearchTrie) from Java Spelling Checkin - g Package (jaspell): http://jaspell.sourceforge.net/ - License: The BSD License (http://www.opensource.org/licenses/bsd-license.php) - - The snowball stemmers in - analysis/common/src/java/net/sf/snowball - were developed by Martin Porter and Richard Boulton. - The snowball stopword lists in - analysis/common/src/resources/org/apache/lucene/analysis/snowball - were developed by Martin Porter and Richard Boulton. - The full snowball package is available from - http://snowball.tartarus.org/ - - The KStem stemmer in - analysis/common/src/org/apache/lucene/analysis/en - was developed by Bob Krovetz and Sergio Guzman-Lara (CIIR-UMass Amherst) - under the BSD-license. - - The Arabic,Persian,Romanian,Bulgarian, and Hindi analyzers (common) come with a default - stopword list that is BSD-licensed created by Jacques Savoy. These files reside in: - analysis/common/src/resources/org/apache/lucene/analysis/ar/stopwords.txt, - analysis/common/src/resources/org/apache/lucene/analysis/fa/stopwords.txt, - analysis/common/src/resources/org/apache/lucene/analysis/ro/stopwords.txt, - analysis/common/src/resources/org/apache/lucene/analysis/bg/stopwords.txt, - analysis/common/src/resources/org/apache/lucene/analysis/hi/stopwords.txt - See http://members.unine.ch/jacques.savoy/clef/index.html. - - The German,Spanish,Finnish,French,Hungarian,Italian,Portuguese,Russian and Swedish light stemmers - (common) are based on BSD-licensed reference implementations created by Jacques Savoy and - Ljiljana Dolamic. These files reside in: - analysis/common/src/java/org/apache/lucene/analysis/de/GermanLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/de/GermanMinimalStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/es/SpanishLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/fi/FinnishLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/fr/FrenchLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/fr/FrenchMinimalStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/hu/HungarianLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/it/ItalianLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/pt/PortugueseLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/ru/RussianLightStemmer.java - analysis/common/src/java/org/apache/lucene/analysis/sv/SwedishLightStemmer.java - - The Stempel analyzer (stempel) includes BSD-licensed software developed - by the Egothor project http://egothor.sf.net/, created by Leo Galambos, Martin Kvapil, - and Edmond Nolan. - - The Polish analyzer (stempel) comes with a default - stopword list that is BSD-licensed created by the Carrot2 project. The file resides - in stempel/src/resources/org/apache/lucene/analysis/pl/stopwords.txt. - See http://project.carrot2.org/license.html. - - The SmartChineseAnalyzer source code (smartcn) was - provided by Xiaoping Gao and copyright 2009 by www.imdict.net. - - WordBreakTestUnicode_*.java (under modules/analysis/common/src/test/) - is derived from Unicode data such as the Unicode Character Database. - See http://unicode.org/copyright.html for more details. - - The Morfologik analyzer (morfologik) includes BSD-licensed software - developed by Dawid Weiss and Marcin Miłkowski (http://morfologik.blogspot.com/). - - Morfologik uses data from Polish ispell/myspell dictionary - (http://www.sjp.pl/slownik/en/) licenced on the terms of (inter alia) - LGPL and Creative Commons ShareAlike. - - Morfologic includes data from BSD-licensed dictionary of Polish (SGJP) - (http://sgjp.pl/morfeusz/) - - Servlet-api.jar and javax.servlet-*.jar are under the CDDL license, the original - source code for this can be found at http://www.eclipse.org/jetty/downloads.php - - =========================================================================== - Kuromoji Japanese Morphological Analyzer - Apache Lucene Integration - =========================================================================== - - This software includes a binary and/or source version of data from - - mecab-ipadic-2.7.0-20070801 - - which can be obtained from - - http://atilika.com/releases/mecab-ipadic/mecab-ipadic-2.7.0-20070801.tar.gz - - or - - http://jaist.dl.sourceforge.net/project/mecab/mecab-ipadic/2.7.0-20070801/mecab-ipadic-2.7.0-20070801.tar.gz - - =========================================================================== - mecab-ipadic-2.7.0-20070801 Notice - =========================================================================== - - Nara Institute of Science and Technology (NAIST), - the copyright holders, disclaims all warranties with regard to this - software, including all implied warranties of merchantability and - fitness, in no event shall NAIST be liable for - any special, indirect or consequential damages or any damages - whatsoever resulting from loss of use, data or profits, whether in an - action of contract, negligence or other tortuous action, arising out - of or in connection with the use or performance of this software. - - A large portion of the dictionary entries - originate from ICOT Free Software. The following conditions for ICOT - Free Software applies to the current dictionary as well. - - Each User may also freely distribute the Program, whether in its - original form or modified, to any third party or parties, PROVIDED - that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear - on, or be attached to, the Program, which is distributed substantially - in the same form as set out herein and that such intended - distribution, if actually made, will neither violate or otherwise - contravene any of the laws and regulations of the countries having - jurisdiction over the User or the intended distribution itself. - - NO WARRANTY - - The program was produced on an experimental basis in the course of the - research and development conducted during the project and is provided - to users as so produced on an experimental basis. Accordingly, the - program is provided without any warranty whatsoever, whether express, - implied, statutory or otherwise. The term "warranty" used herein - includes, but is not limited to, any warranty of the quality, - performance, merchantability and fitness for a particular purpose of - the program and the nonexistence of any infringement or violation of - any right of any third party. - - Each user of the program will agree and understand, and be deemed to - have agreed and understood, that there is no warranty whatsoever for - the program and, accordingly, the entire risk arising from or - otherwise connected with the program is assumed by the user. - - Therefore, neither ICOT, the copyright holder, or any other - organization that participated in or was otherwise related to the - development of the program and their respective officials, directors, - officers and other employees shall be held liable for any and all - damages, including, without limitation, general, special, incidental - and consequential damages, arising out of or otherwise in connection - with the use or inability to use the program or any product, material - or result produced or otherwise obtained by using the program, - regardless of whether they have been advised of, or otherwise had - knowledge of, the possibility of such damages at any time during the - project or thereafter. Each user will be deemed to have agreed to the - foregoing by his or her commencement of use of the program. The term - "use" as used herein includes, but is not limited to, the use, - modification, copying and distribution of the program and the - production of secondary products from the program. - - In the case where the program, whether in its original form or - modified, was distributed or delivered to or received by a user from - any person, organization or entity other than ICOT, unless it makes or - grants independently of ICOT any specific warranty to the user in - writing, such person, organization or entity, will also be exempted - from and not be held liable to the user for any such damages as noted - above as far as the program is concerned. - - (ASLv2) Joda Time - The following NOTICE information applies: - This product includes software developed by - Joda.org (http://www.joda.org/). - - (ASLv2) Apache ActiveMQ - The following NOTICE information applies: - ActiveMQ :: Client - Copyright 2005-2015 The Apache Software Foundation - - (ASLv2) Apache Geronimo - The following NOTICE information applies: - Apache Geronimo - Copyright 2003-2008 The Apache Software Foundation - - (ASLv2) Swagger Core - The following NOTICE information applies: - Swagger Core 1.5.3-M1 - Copyright 2015 Reverb Technologies, Inc. - - (ASLv2) Google GSON - The following NOTICE information applies: - Copyright 2008 Google Inc. - - (ASLv2) JSON-SMART - The following NOTICE information applies: - Copyright 2011 JSON-SMART authors - - (ASLv2) JsonPath - The following NOTICE information applies: - Copyright 2011 JsonPath authors - - (ASLv2) Kite SDK - The following NOTICE information applies: - This product includes software developed by Cloudera, Inc. - (http://www.cloudera.com/). - - This product includes software developed at - The Apache Software Foundation (http://www.apache.org/). - - This product includes software developed by - Saxonica (http://www.saxonica.com/). - - (ASLv2) MongoDB Java Driver - The following NOTICE information applies: - Copyright (C) 2008-2013 10gen, Inc. - - (ASLv2) Parquet MR - The following NOTICE information applies: - Parquet MR - Copyright 2012 Twitter, Inc. - - This project includes code from https://github.com/lemire/JavaFastPFOR - parquet-column/src/main/java/parquet/column/values/bitpacking/LemireBitPacking.java - Apache License Version 2.0 http://www.apache.org/licenses/. - (c) Daniel Lemire, http://lemire.me/en/ - - (ASLv2) Twitter4J - The following NOTICE information applies: - Copyright 2007 Yusuke Yamamoto - - Twitter4J includes software from JSON.org to parse JSON response from the Twitter API. You can see the license term at http://www.JSON.org/license.html - - (ASLv2) JOAuth - The following NOTICE information applies: - JOAuth - Copyright 2010-2013 Twitter, Inc - - (ASLv2) Hosebird Client - The following NOTICE information applies: - Hosebird Client (hbc) - Copyright 2013 Twitter, Inc. - - (ASLv2) GeoIP2 Java API - The following NOTICE information applies: - GeoIP2 Java API - This software is Copyright (c) 2013 by MaxMind, Inc. - - (ASLv2) Woodstox Core ASL - The following NOTICE information applies: - This product currently only contains code developed by authors - of specific components, as identified by the source code files. - - Since product implements StAX API, it has dependencies to StAX API - classes. - - (ASLv2) Amazon Web Services SDK - The following NOTICE information applies: - Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. - - This product includes software developed by - Amazon Technologies, Inc (http://www.amazon.com/). - - ********************** - THIRD PARTY COMPONENTS - ********************** - This software includes third party software subject to the following copyrights: - - XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty. - - JSON parsing and utility functions from JSON.org - Copyright 2002 JSON.org. - - PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. - - (ASLv2) Apache Commons DBCP - The following NOTICE information applies: - Apache Commons DBCP - Copyright 2001-2015 The Apache Software Foundation. - - (ASLv2) Apache Commons Pool - The following NOTICE information applies: - Apache Commons Pool - Copyright 1999-2009 The Apache Software Foundation. - - (ASLv2) Apache Derby - The following NOTICE information applies: - Apache Derby - Copyright 2004-2014 Apache, Apache DB, Apache Derby, Apache Torque, Apache JDO, Apache DDLUtils, - the Derby hat logo, the Apache JDO logo, and the Apache feather logo are trademarks of The Apache Software Foundation. - - (ASLv2) Apache Directory Server - The following NOTICE information applies: - ApacheDS Protocol Kerberos Codec - Copyright 2003-2013 The Apache Software Foundation - - ApacheDS I18n - Copyright 2003-2013 The Apache Software Foundation - - Apache Directory API ASN.1 API - Copyright 2003-2013 The Apache Software Foundation - - Apache Directory LDAP API Utilities - Copyright 2003-2013 The Apache Software Foundation - - (ASLv2) Apache Curator - The following NOTICE information applies: - Curator Framework - Copyright 2011-2014 The Apache Software Foundation - - Curator Client - Copyright 2011-2014 The Apache Software Foundation - - Curator Recipes - Copyright 2011-2014 The Apache Software Foundation - - (ASLv2) The Netty Project - The following NOTICE information applies: - The Netty Project - Copyright 2011 The Netty Project - - (ASLv2) Apache Xerces Java - The following NOTICE information applies: - Apache Xerces Java - Copyright 1999-2007 The Apache Software Foundation - - This product includes software developed at - The Apache Software Foundation (http://www.apache.org/). - - Portions of this software were originally based on the following: - - software copyright (c) 1999, IBM Corporation., http://www.ibm.com. - - software copyright (c) 1999, Sun Microsystems., http://www.sun.com. - - voluntary contributions made by Paul Eng on behalf of the - Apache Software Foundation that were originally developed at iClick, Inc., - software copyright (c) 1999. - - (ASLv2) Metadata-Extractor - The following NOTICE information applies: - Metadata-Extractor - Copyright 2002-2015 Drew Noakes - - (ASLv2) Couchbase Java SDK - The following NOTICE information applies: - Couchbase Java SDK - Copyright 2014 Couchbase, Inc. - - (ASLv2) RxJava - The following NOTICE information applies: - Couchbase Java SDK - Copyright 2012 Netflix, Inc. - - (ASLv2) HBase Common - The following NOTICE information applies: - This product includes portions of the Guava project v14, specifically - 'hbase-common/src/main/java/org/apache/hadoop/hbase/io/LimitInputStream.java' - - Copyright (C) 2007 The Guava Authors - - Licensed under the Apache License, Version 2.0 - - (ASLv2) HTrace Core - The following NOTICE information applies: - In addition, this product includes software dependencies. See - the accompanying LICENSE.txt for a listing of dependencies - that are NOT Apache licensed (with pointers to their licensing) - - Apache HTrace includes an Apache Thrift connector to Zipkin. Zipkin - is a distributed tracing system that is Apache 2.0 Licensed. - Copyright 2012 Twitter, Inc. - - (ASLv2) Apache Qpid AMQP 1.0 Client - The following NOTICE information applies: - Copyright 2006-2015 The Apache Software Foundation - - (ASLv2) EventHubs Client (com.microsoft.eventhubs.client:eventhubs-client:0.9.1 - https://github.com/hdinsight/eventhubs-client/) - - (ASLv2) Groovy (org.codehaus.groovy:groovy:jar:2.4.5 - http://www.groovy-lang.org) - The following NOTICE information applies: - Groovy Language - Copyright 2003-2015 The respective authors and developers - Developers and Contributors are listed in the project POM file - and Gradle build file - - This product includes software developed by - The Groovy community (http://groovy.codehaus.org/). - -(ASLv2) Carrotsearch HPPC - The following NOTICE information applies: - HPPC borrowed code, ideas or both from: - - * Apache Lucene, http://lucene.apache.org/ - (Apache license) - * Fastutil, http://fastutil.di.unimi.it/ - (Apache license) - * Koloboke, https://github.com/OpenHFT/Koloboke - (Apache license) - - (ASLv2) t-digest - The following NOTICE information applies: - The code for the t-digest was originally authored by Ted Dunning - A number of small but very helpful changes have been contributed by Adrien Grand (https://github.com/jpountz) - -************************ -Common Development and Distribution License 1.1 -************************ - -The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details. - - (CDDL 1.1) (GPL2 w/ CPE) jersey-core-client (org.glassfish.jersey.core:jersey-client:jar:2.19 - https://jersey.java.net/jersey-client/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-client (com.sun.jersey:jersey-client:jar:1.19 - https://jersey.java.net/jersey-client/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-core-common (org.glassfish.jersey.core:jersey-common:jar:2.19 - https://jersey.java.net/jersey-common/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-core (com.sun.jersey:jersey-core:jar:1.19 - https://jersey.java.net/jersey-core/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-spring (com.sun.jersey:jersey-spring:jar:1.19 - https://jersey.java.net/jersey-spring/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-servlet (com.sun.jersey:jersey-servlet:jar:1.19 - https://jersey.java.net/jersey-servlet/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-multipart (com.sun.jersey:jersey-multipart:jar:1.19 - https://jersey.java.net/jersey-multipart/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-server (com.sun.jersey:jersey-server:jar:1.19 - https://jersey.java.net/jersey-server/) - (CDDL 1.1) (GPL2 w/ CPE) jersey-json (com.sun.jersey:jersey-json:jar:1.19 - https://jersey.java.net/jersey-json/) - (CDDL 1.1) (GPL2 w/ CPE) Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:jar:2.2.3-1 - http://jaxb.java.net/) - (CDDL 1.1) (GPL2 w/ CPE) Java Architecture For XML Binding (javax.xml.bind:jaxb-api:jar:2.2.2 - https://jaxb.dev.java.net/) - (CDDL 1.1) (GPL2 w/ CPE) MIME Streaming Extension (org.jvnet.mimepull:mimepull:jar:1.9.3 - http://mimepull.java.net) - (CDDL 1.1) (GPL2 w/ CPE) JavaMail API (compat) (javax.mail:mail:jar:1.4.7 - http://kenai.com/projects/javamail/mail) - (CDDL 1.1) (GPL2 w/ CPE) JSP Implementation (org.glassfish.web:javax.servlet.jsp:jar:2.3.2 - http://jsp.java.net) - (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages (TM) TagLib Implementation (org.glassfish.web:javax.servlet.jsp.jstl:jar:1.2.2 - http://jstl.java.net) - (CDDL 1.1) (GPL2 w/ CPE) Expression Language 3.0 (org.glassfish:javax.el:jar:3.0.0 - http://el-spec.java.net) - (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages(TM) API (javax.servlet.jsp:javax.servlet.jsp-api:jar:2.3.1 - http://jsp.java.net) - (CDDL 1.1) (GPL2 w/ CPE) Expression Language 3.0 API (javax.el:javax.el-api:jar:3.0.0 - http://uel-spec.java.net) - (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages(TM) Standard Tag Library API (javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:jar:1.2.1 - http://jcp.org/en/jsr/detail?id=52) - (CDDL 1.1) (GPL2 w/ CPE) Java Servlet API (javax.servlet:javax.servlet-api:jar:3.1.0 - http://servlet-spec.java.net) - (CDDL 1.1) (GPL2 w/ CPE) Javax JMS Api (javax.jms:javax.jms-api:jar:2.0.1 - http://java.net/projects/jms-spec/pages/Home) - (CDDL 1.1) (GPL2 w/ CPE) JSON Processing API (javax.json:javax.json-api:jar:1.0 - http://json-processing-spec.java.net) - (CDDL 1.1) (GPL2 w/ CPE) JSON Processing Default Provider (org.glassfish:javax.json:jar:1.0.4 - https://jsonp.java.net) - (CDDL 1.1) (GPL2 w/ CPE) OSGi resource locator bundle (org.glassfish.hk2:osgi-resource-locator:jar:1.0.1 - http://glassfish.org/osgi-resource-locator) - (CDDL 1.1) (GPL2 w/ CPE) javax.annotation API (javax.annotation:javax.annotation-api:jar:1.2 - http://jcp.org/en/jsr/detail?id=250) - (CDDL 1.1) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:jar:2.4.0-b25 - https://hk2.java.net/hk2-api) - (CDDL 1.1) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:jar:2.4.0-b25 - https://hk2.java.net/hk2-locator) - (CDDL 1.1) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:jar:2.4.0-b25 - https://hk2.java.net/hk2-utils) - (CDDL 1.1) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:jar:2.4.0-b25 - https://hk2.java.net/external/aopalliance-repackaged) - (CDDL 1.1) (GPL2 w/ CPE) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:jar:2.4.0-b25 - https://hk2.java.net/external/javax.inject) - (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:jar:2.0.1 - http://jax-rs-spec.java.net) - (CDDL 1.1) (GPL2 w/ CPE) jersey-repackaged-guava (org.glassfish.jersey.bundles.repackaged:jersey-guava:bundle:2.19 - https://jersey.java.net/project/project/jersey-guava/) - - -************************ -Common Development and Distribution License 1.0 -************************ - -The following binary components are provided under the Common Development and Distribution License 1.0. See project link for details. - - (CDDL 1.0) JavaServlet(TM) Specification (javax.servlet:servlet-api:jar:2.5 - no url available) - (CDDL 1.0) (GPL3) Streaming API For XML (javax.xml.processor:stax-api:jar:1.0-2 - no url provided) - (CDDL 1.0) JavaBeans Activation Framework (JAF) (javax.activation:activation:jar:1.1 - http://java.sun.com/products/javabeans/jaf/index.jsp) - (CDDL 1.0) JSR311 API (javax.ws.rs:jsr311-api:jar:1.1.1 - https://jsr311.dev.java.net) - -************************ -Creative Commons Attribution-ShareAlike 3.0 -************************ - -The following binary components are provided under the Creative Commons Attribution-ShareAlike 3.0. See project link for details. - - (CCAS 3.0) MaxMind DB (https://github.com/maxmind/MaxMind-DB) - -************************ -Eclipse Public License 1.0 -************************ - -The following binary components are provided under the Eclipse Public License 1.0. See project link for details. - - (EPL 1.0) AspectJ Weaver (org.aspectj:aspectjweaver:jar:1.8.5 - http://www.aspectj.org) - (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:1.3.176 - http://www.h2database.com/html/license.html) - (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.1.3 - http://logback.qos.ch/) - (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.1.3 - http://logback.qos.ch/) - (EPLv1.0) JRuby (org.jruby:jruby-complete:9.0.4.0 - http://jruby.org). - - JRuby is licensed under three licenses - the EPL 1.0, GPL 2 and LGPL 2.1. Apache NiFi uses the EPL v1.0 license. - - The following NOTICE information applies: - Copyright (c) 2007-2015 The JRuby project - -***************** -Mozilla Public License v2.0 -***************** - -The following binary components are provided under the Mozilla Public License v2.0. See project link for details. - - (MPL 2.0) Saxon HE (net.sf.saxon:Saxon-HE:jar:9.6.0-5 - http://www.saxonica.com/) - -***************** -Mozilla Public License v1.1 -***************** - -The following binary components are provided under the Mozilla Public License v1.1. See project link for details. - - (MPL 1.1) HAPI Base (ca.uhn.hapi:hapi-com.hurence.logisland.logisland.parser.base:2.2 - http://hl7api.sourceforge.net/) - (MPL 1.1) HAPI Structures (ca.uhn.hapi:hapi-structures-v*:2.2 - http://hl7api.sourceforge.net/) - -****************** -Python Software Foundation License v2 -****************** - -The following binary components are provided under the Python Software Foundation License v2 - - (PSFLv2) Jython (org.python:jython-standalone:2.7.0 - http://www.jython.org/) - The following NOTICE information applies: - Copyright (c) 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 Jython Developers All rights reserved. - -****************** -MIT License -****************** - -The following binary components are provided under an MIT-style license - - (MIT) Luaj (org.luaj:luaj-jse:3.0.1 - http://www.luaj.org/luaj/3.0/README.html) - The following NOTICE information applies: - Copyright (c) 2009-2011 Luaj.org. All rights reserved. - -***************** -Public Domain -***************** - -The following binary components are provided to the 'Public Domain'. See project link for details. - - (Public Domain) XZ for Java (org.tukaani:xz:jar:1.5 - http://tukaani.org/xz/java.html - (Public Domain) AOP Alliance 1.0 (http://aopalliance.sourceforge.net/) - -The following binary components are provided under the Creative Commons Zero license version 1.0. See project link for details. - - (CC0v1.0) JSR166e for Twitter (com.twitter:jsr166e:jar:1.1.0 - https://github.com/twitter/jsr166e) - diff --git a/logisland-assembly/README.md b/logisland-assembly/README.md deleted file mode 100644 index e5edb2f24..000000000 --- a/logisland-assembly/README.md +++ /dev/null @@ -1,63 +0,0 @@ - -# Apache NiFi - -Apache NiFi is an easy to use, powerful, and reliable system to process and distribute data. - -## Table of Contents - -- [Features](#features) -- [Getting Started](#getting-started) -- [Getting Help](#getting-help) -- [Requirements](#requirements) -- [License](#license) -- [Export Control] (#export-control) - -## Features - -Logisland was made for providing event mining tools on logs streams at scale. - -## Getting Started - -To start Logisland: -- execute bin/logisland.sh start -- Direct your browser to http://localhost:8080/logisland/ - -## Getting Help -If you have questions, you can reach out to our mailing list: contact@hurence.com - -We're also often available in IRC: #logisland on -[irc.freenode.net](http://webchat.freenode.net/?channels=#logisland). - -## Requirements -* JDK 1.7 or higher - -## License - -Except as otherwise noted this software is licensed under the -[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - From c7a3f8648b8e5130ada98fe935da2746711c63e9 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Thu, 5 Jul 2018 15:58:48 +0200 Subject: [PATCH 51/63] Upgrade opc-simple to latest --- .../logisland-connectors/logisland-connector-opc/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index 49d320070..a54829627 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -16,7 +16,7 @@ com.github.Hurence opc-simple - 1.2.0 + 1.2.1 com.hurence.logisland From d66b37c1dc3fd8ccbb6b7eea4d3b2d0be1129900 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 6 Jul 2018 10:27:44 +0200 Subject: [PATCH 52/63] Remove bad dep from documentation pom --- logisland-documentation/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/logisland-documentation/pom.xml b/logisland-documentation/pom.xml index acba32626..a8cf41745 100644 --- a/logisland-documentation/pom.xml +++ b/logisland-documentation/pom.xml @@ -117,10 +117,6 @@ com.hurence.logisland logisland-redis_4-client-service - - com.hurence.logisland - logisland-connector-opcda - From 18d0aa04968e5f8cd842791e9099bcaeda24ccb8 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 6 Jul 2018 10:48:00 +0200 Subject: [PATCH 53/63] Make kafka spark engine 1.6 compliant with new interface --- logisland-documentation/components.rst | 3 ++ .../spark/KafkaStreamProcessingEngine.scala | 36 ++++++++++++++++++- .../src/main/resources/docs/components.rst | 3 ++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index 5b6690a62..6cb92e54d 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -221,6 +221,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" "alert.criticity", "from 0 to ...", "", "0", "", "" @@ -314,6 +315,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" @@ -408,6 +410,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" Dynamic Properties diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index ddfa26520..e916aae81 100644 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -292,6 +292,7 @@ object KafkaStreamProcessingEngine { class KafkaStreamProcessingEngine extends AbstractProcessingEngine { private val logger = LoggerFactory.getLogger(classOf[KafkaStreamProcessingEngine]) + private val conf = new SparkConf() override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { @@ -365,7 +366,6 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { /** * job configuration */ - val conf = new SparkConf() conf.setAppName(appName) conf.setMaster(sparkMaster) @@ -458,6 +458,40 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { } value changed from $oldValue to $newValue") } + /** + * Await for termination. + * + * @param engineContext + */ + override def awaitTermination(engineContext: EngineContext): Unit = { + var timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) + .asInteger().toInt + val sc = SparkContext.getOrCreate(conf) + + while (!sc.isStopped) { + try { + if (timeout < 0) { + Thread.sleep(200) + } else { + val toSleep = Math.min(200, timeout); + Thread.sleep(toSleep) + timeout -= toSleep + } + } catch { + case e: InterruptedException => return + case unknown: Throwable => throw unknown + } + } + } + + /** + * Reset the engine by stopping the streaming context. + * + * @param engineContext + */ + override def reset(engineContext: EngineContext): Unit = { + //not supported + } } diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index 5b6690a62..6cb92e54d 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -221,6 +221,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "profile.activation.condition", "A javascript expression that activates this alerting profile when true", "", "0==0", "", "" "alert.criticity", "from 0 to ...", "", "0", "", "" @@ -314,6 +315,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" @@ -408,6 +410,7 @@ In the list below, the names of required properties appear in **bold**. Any othe

", "", "30", "", "" "**datastore.client.service**", "The instance of the Controller Service to use for accessing datastore.", "", "null", "", "" "datastore.cache.collection", "The collection where to find cached objects", "", "test", "", "" + "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" Dynamic Properties From 4630072b2cad8613e15e5e1e4d92e18cac03e372 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 6 Jul 2018 10:51:14 +0200 Subject: [PATCH 54/63] Temporary restore old assembly documentation --- logisland-assembly/LICENSE | 1152 ++++++++++++++++++++++++++++++++++ logisland-assembly/NOTICE | 926 +++++++++++++++++++++++++++ logisland-assembly/README.md | 63 ++ 3 files changed, 2141 insertions(+) create mode 100644 logisland-assembly/LICENSE create mode 100644 logisland-assembly/NOTICE create mode 100644 logisland-assembly/README.md diff --git a/logisland-assembly/LICENSE b/logisland-assembly/LICENSE new file mode 100644 index 000000000..68af30331 --- /dev/null +++ b/logisland-assembly/LICENSE @@ -0,0 +1,1152 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +APACHE NIFI SUBCOMPONENTS: + +The Apache NiFi project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + +This product bundles source from 'Asciidoctor'. Specifically the 'asciidoc-mod.css'. +The source is available under an MIT LICENSE. + + Copyright (C) 2012-2015 Dan Allen, Ryan Waldron and the Asciidoctor Project + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Javascript D3 Library' which is available under a +"3-clause BSD" license. + + Copyright (c) 2010-2014, Michael Bostock + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * The name Michael Bostock may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'CodeMirror' which is available under an MIT style license. + + Copyright (C) 2014 by Marijn Haverbeke and others + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'JQuery' which is available under and MIT style license. + (c) 2005, 2014 jQuery Foundation, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'JQuery Event Drag' which is available under an MIT style +license. + Copyright (c) 2008-2015 ThreeDubMedia + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'jQuery Form Plugin' which is available under either the MIT +or GPL license. The license in effect here is the MIT license + + Copyright 2006-2013 (c) M. Alsup + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'jQuery UI' which is available under an MIT style license. +For details see http://jqueryui.com + + Copyright 2014 jQuery Foundation and other contributors + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'jquery.base64.js' which is available under an MIT style license. + + Copyright (c) 2013 Yannick Albert (http://yckart.com/) + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'SlickGrid v2.2' which is available under an MIT style license. + + Copyright (c) 2010 Michael Leibman, http://github.com/mleibman/slickgrid + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'qTip2' which is available under an MIT style license. +For details see http://qtip2.com + + Copyright (c) 2012 Craig Michael Thompson + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'jQuery MiniColors' which is available under the MIT License. +For details see http://www.abeautifulsite.net/ + + Copyright Cory LaViska for A Beautiful Site, LLC. (http://www.abeautifulsite.net/) + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + +This product bundles 'jsoup' which is available under the MIT License. +For details see http://jsoup.org/ + + jsoup License + The jsoup code-com.hurence.logisland.logisland.parser.base (include source and compiled packages) are distributed under the open source MIT license as described below. + + The MIT License + Copyright © 2009 - 2013 Jonathan Hedley (jonathan@hedley.net) + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. + + + +This product bundles 'json2.js' which is available in the 'public domain'. + For details see https://github.com/douglascrockford/JSON-js + +This product bundles 'reset.css' which is available in the 'public domain'. + For details see http://meyerweb.com/eric/tools/css/reset/ + +This product bundles HexViewJS available under an MIT License + + Copyright (c) 2010 Nick McVeity + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + The binary distribution of this product bundles 'Slf4j' which is available + under a "3-clause BSD" license. For details see http://www.slf4j.org/ + + Copyright (c) 2004-2013 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + The binary distribution of this product bundles 'Antlr 3' which is available + under a "3-clause BSD" license. For details see http://www.antlr3.org/license.html + + Copyright (c) 2010 Terence Parr + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + Neither the name of the author nor the names of its contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'Bouncy Castle JDK 1.5 Provider' + under an MIT style license. + + Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + The binary distribution of this product bundles 'XMLENC' which is available + under a BSD license. More details found here: http://xmlenc.sourceforge.net. + + Copyright 2003-2005, Ernst de Haan + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'Slf4j' which is available under + an MIT license. + + Copyright (c) 2004-2013 QOS.ch + All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + The binary distribution of this product bundles 'ParaNamer' and 'Paranamer Core' + which is available under a BSD style license. + + Copyright (c) 2006 Paul Hammant & ThoughtWorks Inc + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'Protocol Buffers - Google's data interchange format' + which is available under a BSD style license. + + Copyright 2008 Google Inc. All rights reserved. + http://code.google.com/p/protobuf/ + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'JCraft Jsch' which is available + under a BSD style license. + Copyright (c) 2002-2015 Atsuhiko Yamanaka, JCraft,Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'Scala Library' under a BSD + style license. + + Copyright (c) 2002-2015 EPFL + Copyright (c) 2011-2015 Typesafe, Inc. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + + Neither the name of the EPFL nor the names of its contributors may be used to endorse + or promote products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'JLine' under a BSD + style license. + + Copyright (c) 2002-2006, Marc Prud'hommeaux + All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the following + conditions are met: + + Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with + the distribution. + + Neither the name of JLine nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + OF THE POSSIBILITY OF SUCH DAMAGE. + + The binary distribution of this product bundles 'JOpt Simple' under an MIT + style license. + + Copyright (c) 2009 Paul R. Holser, Jr. + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + The binary distribution of this product bundles 'Jcodings' under an MIT style + license. + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + The binary distribution of this product bundles 'Joni' under an MIT style + license. + + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +The binary distribution of this product bundles 'Google Protocol Buffers Java 2.5.0' +which is licensed under a BSD license. + + This license applies to all parts of Protocol Buffers except the following: + + - Atomicops support for generic gcc, located in + src/google/protobuf/stubs/atomicops_internals_generic_gcc.h. + This file is copyrighted by Red Hat Inc. + + - Atomicops support for AIX/POWER, located in + src/google/protobuf/stubs/atomicops_internals_aix.h. + This file is copyrighted by Bloomberg Finance LP. + + Copyright 2014, Google Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Code generated by the Protocol Buffer compiler is owned by the owner + of the input file used when generating it. This code is not + standalone and requires a support library to be linked with it. This + support library is itself covered by the above license. + +This product bundles 'JCraft Jzlib' which is available under a 3-Clause BSD License. + + Copyright (c) 2002-2014 Atsuhiko Yamanaka, JCraft,Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + 3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT, + INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'asm' which is available under a 3-Clause BSD style license. +For details see http://asm.ow2.org/asmdex-license.html + + Copyright (c) 2012 France Télécom + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'Hamcrest' which is available +under a BSD license. More details found here: http://hamcrest.org. + + Copyright (c) 2000-2006, www.hamcrest.org + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. Redistributions in binary form must reproduce + the above copyright notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the distribution. + + Neither the name of Hamcrest nor the names of its contributors may be used to endorse + or promote products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + +The binary distribution of this product bundles 'leveldbjni-all-1.8.jar' which is available + under a BSD style license + + Copyright (c) 2011 FuseSource Corp. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of FuseSource Corp. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The binary distribution of this product bundles 'Woodstox StAX 2 API' which is + "licensed under standard BSD license" + +This product bundles 'Adobe XMPCore' which is available under "The BSD license". More +information can be found here: http://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html + + Copyright (c) 2009, Adobe Systems Incorporated All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated, nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANT ABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. + +This product bundles 'Jsoup' which is available under "The MIT license". More +information can be found here: http://jsoup.org/license + + The MIT License + + Copyright (c) 2009-2015, Jonathan Hedley + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'Luaj' which is available under an MIT style license. More +information can be found here: + + Copyright (c) 2009 Luaj.org. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + +This product bundles 'jBCrypt' which is available under a BSD license. +For details see https://github.com/svenkubiak/jBCrypt/blob/0.4.1/LICENSE + + Copyright (c) 2006 Damien Miller + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/logisland-assembly/NOTICE b/logisland-assembly/NOTICE new file mode 100644 index 000000000..06c5be409 --- /dev/null +++ b/logisland-assembly/NOTICE @@ -0,0 +1,926 @@ +Logisland +Copyright 2014-2016 Hurence + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes the following work from the Apache Hadoop project: + +BoundedByteArrayOutputStream.java which was adapted to SoftLimitBoundedByteArrayOutputStream.java + +=========================================== +Apache Software License v2 +=========================================== + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Apache Commons IO + The following NOTICE information applies: + Apache Commons IO + Copyright 2002-2012 The Apache Software Foundation + + (ASLv2) Apache Commons Net + The following NOTICE information applies: + Apache Commons Net + Copyright 2001-2013 The Apache Software Foundation + + (ASLv2) Apache Commons Collections + The following NOTICE information applies: + Apache Commons Collections + Copyright 2001-2013 The Apache Software Foundation + + (ASLv2) Apache Commons Compress + The following NOTICE information applies: + Apache Commons Compress + Copyright 2002-2014 The Apache Software Foundation + + The files in the package org.apache.commons.compress.archivers.sevenz + were derived from the LZMA SDK, version 9.20 (C/ and CPP/7zip/), + which has been placed in the public domain: + + "LZMA SDK is placed in the public domain." (http://www.7-zip.org/sdk.html) + + (ASLv2) Jettison + The following NOTICE information applies: + Copyright 2006 Envoi Solutions LLC + + (ASLv2) Jasypt + The following NOTICE information applies: + Copyright (c) 2007-2010, The JASYPT team (http://www.jasypt.org) + + (ASLv2) Apache Commons Codec + The following NOTICE information applies: + Apache Commons Codec + Copyright 2002-2014 The Apache Software Foundation + + src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java + contains test data from http://aspell.net/test/orig/batch0.tab. + Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org) + + =============================================================================== + + The content of package org.apache.commons.codec.language.bm has been translated + from the original php source code available at http://stevemorse.org/phoneticinfo.htm + with permission from the original authors. + Original source copyright: + Copyright (c) 2008 Alexander Beider & Stephen P. Morse. + + (ASLv2) Apache HttpComponents + The following NOTICE information applies: + Apache HttpClient + Copyright 1999-2015 The Apache Software Foundation + + Apache HttpCore + Copyright 2005-2015 The Apache Software Foundation + + Apache HttpMime + Copyright 1999-2013 The Apache Software Foundation + + This project contains annotations derived from JCIP-ANNOTATIONS + Copyright (c) 2005 Brian Goetz and Tim Peierls. See http://www.jcip.net + + (ASLv2) Apache Jakarta HttpClient + The following NOTICE information applies: + Apache Jakarta HttpClient + Copyright 1999-2007 The Apache Software Foundation + + (ASLv2) Apache Commons Logging + The following NOTICE information applies: + Apache Commons Logging + Copyright 2003-2014 The Apache Software Foundation + + (ASLv2) Apache Commons Lang + The following NOTICE information applies: + Apache Commons Lang + Copyright 2001-2015 The Apache Software Foundation + + This product includes software from the Spring Framework, + under the Apache License 2.0 (see: StringUtils.containsWhitespace()) + + (ASLv2) Apache Commons Configuration + The following NOTICE information applies: + Apache Commons Configuration + Copyright 2001-2008 The Apache Software Foundation + + (ASLv2) Apache Commons JEXL + The following NOTICE information applies: + Apache Commons JEXL + Copyright 2001-2011 The Apache Software Foundation + + (ASLv2) Spring Framework + The following NOTICE information applies: + Spring Framework 4.1.4.RELEASE + Copyright (c) 2002-2015 Pivotal, Inc. + + (ASLv2) Spring Security + The following NOTICE information applies: + Spring Framework 4.0.3.RELEASE + Copyright (c) 2002-2015 Pivotal, Inc. + + (ASLv2) Apache Flume + The following NOTICE information applies: + Apache Flume + Copyright 2011-2015 Apache Software Foundation + + asynchbase is BSD-licensed software (https://github.com/OpenTSDB/asynchbase) + + async is BSD-licensed software (https://github.com/stumbleupon/async) + + jopt-simple is MIT licensed software (http://pholser.github.io/jopt-simple/license.html) + + scala-library is BSD-like licensed software (http://www.scala-lang.org/license.html) + + (ASLv2) Xalan + This product includes software developed by + The Apache Software Foundation (http://www.apache.org/). + + Portions of this software was originally based on the following: + + - software copyright (c) 1999-2002, Lotus Development Corporation., http://www.lotus.com. + - software copyright (c) 2001-2002, Sun Microsystems., http://www.sun.com. + - software copyright (c) 2003, IBM Corporation., http://www.ibm.com. + - voluntary contributions made by Ovidiu Predescu (ovidiu@cup.hp.com) on behalf of the + Apache Software Foundation and was originally developed at Hewlett Packard Company. + + (ASLv2) Apache XML Commons XML APIs + Copyright 2006 The Apache Software Foundation. + + This product includes software developed at + The Apache Software Foundation (http://www.apache.org/). + + Portions of this software were originally based on the following: + - software copyright (c) 1999, IBM Corporation., http://www.ibm.com. + - software copyright (c) 1999, Sun Microsystems., http://www.sun.com. + - software copyright (c) 2000 World Wide Web Consortium, http://www.w3.org + + (ASLv2) IRClib + The following NOTICE information applies: + IRClib -- A Java Internet Relay Chat library -- + Copyright (C) 2002 - 2006 Christoph Schwering + + (ASLv2) Jackson JSON component + The following NOTICE information applies: + # Jackson JSON component + + Jackson is a high-performance, Free/Open Source JSON processing library. + It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has + been in development since 2007. + It is currently developed by a community of developers, as well as supported + commercially by FasterXML.com. + + ## Licensing + + Jackson core and extension components may licensed under different licenses. + To find the details that apply to this artifact see the accompanying LICENSE file. + For more information, including possible other licensing options, contact + FasterXML.com (http://fasterxml.com). + + ## Credits + + A list of contributors may be found from CREDITS file, which is included + in some artifacts (usually source distributions); but is always available + from the source code management (SCM) system project uses. + + (ASLv2) Apache Thrift + The following NOTICE information applies: + Apache Thrift + Copyright 2006-2010 The Apache Software Foundation. + + (ASLv2) Apache MINA + The following NOTICE information applies: + Apache MINA Core + Copyright 2004-2011 Apache MINA Project + + (ASLv2) opencsv (net.sf.opencsv:opencsv:2.3) + + (ASLv2) Apache Velocity + The following NOTICE information applies: + Apache Velocity + Copyright (C) 2000-2007 The Apache Software Foundation + + (ASLv2) ZkClient + The following NOTICE information applies: + ZkClient + Copyright 2009 Stefan Groschupf + + (ASLv2) Apache Commons CLI + The following NOTICE information applies: + Apache Commons CLI + Copyright 2001-2009 The Apache Software Foundation + + (ASLv2) Apache Commons Math + The following NOTICE information applies: + Apache Commons Math + Copyright 2001-2012 The Apache Software Foundation + + This product includes software developed by + The Apache Software Foundation (http://www.apache.org/). + + =============================================================================== + + The BracketFinder (package org.apache.commons.math3.optimization.univariate) + and PowellOptimizer (package org.apache.commons.math3.optimization.general) + classes are based on the Python code in module "optimize.py" (version 0.5) + developed by Travis E. Oliphant for the SciPy library (http://www.scipy.org/) + Copyright © 2003-2009 SciPy Developers. + =============================================================================== + + The LinearConstraint, LinearObjectiveFunction, LinearOptimizer, + RelationShip, SimplexSolver and SimplexTableau classes in package + org.apache.commons.math3.optimization.linear include software developed by + Benjamin McCann (http://www.benmccann.com) and distributed with + the following copyright: Copyright 2009 Google Inc. + =============================================================================== + + This product includes software developed by the + University of Chicago, as Operator of Argonne National + Laboratory. + The LevenbergMarquardtOptimizer class in package + org.apache.commons.math3.optimization.general includes software + translated from the lmder, lmpar and qrsolv Fortran routines + from the Minpack package + Minpack Copyright Notice (1999) University of Chicago. All rights reserved + =============================================================================== + + The GraggBulirschStoerIntegrator class in package + org.apache.commons.math3.ode.nonstiff includes software translated + from the odex Fortran routine developed by E. Hairer and G. Wanner. + Original source copyright: + Copyright (c) 2004, Ernst Hairer + =============================================================================== + + The EigenDecompositionImpl class in package + org.apache.commons.math3.linear includes software translated + from some LAPACK Fortran routines. Original source copyright: + Copyright (c) 1992-2008 The University of Tennessee. All rights reserved. + =============================================================================== + + The MersenneTwister class in package org.apache.commons.math3.random + includes software translated from the 2002-01-26 version of + the Mersenne-Twister generator written in C by Makoto Matsumoto and Takuji + Nishimura. Original source copyright: + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved + =============================================================================== + + The LocalizedFormatsTest class in the unit tests is an adapted version of + the OrekitMessagesTest class from the orekit library distributed under the + terms of the Apache 2 licence. Original source copyright: + Copyright 2010 CS Systèmes d'Information + =============================================================================== + + The HermiteInterpolator class and its corresponding test have been imported from + the orekit library distributed under the terms of the Apache 2 licence. Original + source copyright: + Copyright 2010-2012 CS Systèmes d'Information + =============================================================================== + + The creation of the package "o.a.c.m.analysis.integration.gauss" was inspired + by an original code donated by Sébastien Brisard. + =============================================================================== + + (ASLv2) Apache log4j + The following NOTICE information applies: + Apache log4j + Copyright 2007 The Apache Software Foundation + + (ASLv2) Apache Tika + The following NOTICE information applies: + Apache Tika Core + Copyright 2007-2015 The Apache Software Foundation + + (ASLv2) Apache Jakarta Commons Digester + The following NOTICE information applies: + Apache Jakarta Commons Digester + Copyright 2001-2006 The Apache Software Foundation + + (ASLv2) Apache Commons BeanUtils + The following NOTICE information applies: + Apache Commons BeanUtils + Copyright 2000-2008 The Apache Software Foundation + + (ASLv2) Apache Avro + The following NOTICE information applies: + Apache Avro + Copyright 2009-2013 The Apache Software Foundation + + (ASLv2) Snappy Java + The following NOTICE information applies: + This product includes software developed by Google + Snappy: http://code.google.com/p/snappy/ (New BSD License) + + This product includes software developed by Apache + PureJavaCrc32C from apache-hadoop-common http://hadoop.apache.org/ + (Apache 2.0 license) + + This library containd statically linked libstdc++. This inclusion is allowed by + "GCC RUntime Library Exception" + http://gcc.gnu.org/onlinedocs/libstdc++/manual/license.html + + (ASLv2) ApacheDS + The following NOTICE information applies: + ApacheDS + Copyright 2003-2013 The Apache Software Foundation + + (ASLv2) Apache ZooKeeper + The following NOTICE information applies: + Apache ZooKeeper + Copyright 2009-2012 The Apache Software Foundation + + (ASLv2) Apache Commons Daemon + The following NOTICE information applies: + Apache Commons Daemon + Copyright 1999-2013 The Apache Software Foundation + + (ASLv2) Apache Commons EL + The following NOTICE information applies: + Apache Commons EL + Copyright 1999-2007 The Apache Software Foundation + + EL-8 patch - Copyright 2004-2007 Jamie Taylor + http://issues.apache.org/jira/browse/EL-8 + + (ASLv2) Jetty + The following NOTICE information applies: + Jetty Web Container + Copyright 1995-2015 Mort Bay Consulting Pty Ltd. + + (ASLv2) Apache Tomcat + The following NOTICE information applies: + Apache Tomcat + Copyright 2007 The Apache Software Foundation + + Java Management Extensions (JMX) support is provided by + the MX4J package, which is open source software. The + original software and related information is available + at http://mx4j.sourceforge.net. + + Java compilation software for JSP pages is provided by Eclipse, + which is open source software. The orginal software and + related infomation is available at + http://www.eclipse.org. + + (ASLv2) Apache Kafka + The following NOTICE information applies: + Apache Kafka + Copyright 2012 The Apache Software Foundation. + + (ASLv2) Yammer Metrics + The following NOTICE information applies: + Metrics + Copyright 2010-2012 Coda Hale and Yammer, Inc. + + This product includes software developed by Coda Hale and Yammer, Inc. + + This product includes code derived from the JSR-166 project (ThreadLocalRandom), which was released + with the following comments: + + Written by Doug Lea with assistance from members of JCP JSR-166 + Expert Group and released to the public domain, as explained at + http://creativecommons.org/publicdomain/zero/1.0/ + + (ASLv2) Apache Lucene + The following NOTICE information applies: + Apache Lucene + Copyright 2014 The Apache Software Foundation + + Includes software from other Apache Software Foundation projects, + including, but not limited to: + - Apache Ant + - Apache Jakarta Regexp + - Apache Commons + - Apache Xerces + + ICU4J, (under analysis/icu) is licensed under an MIT styles license + and Copyright (c) 1995-2008 International Business Machines Corporation and others + + Some data files (under analysis/icu/src/data) are derived from Unicode data such + as the Unicode Character Database. See http://unicode.org/copyright.html for more + details. + + Brics Automaton (under core/src/java/org/apache/lucene/util/automaton) is + BSD-licensed, created by Anders Møller. See http://www.brics.dk/automaton/ + + The levenshtein automata tables (under core/src/java/org/apache/lucene/util/automaton) were + automatically generated with the moman/finenight FSA library, created by + Jean-Philippe Barrette-LaPierre. This library is available under an MIT license, + see http://sites.google.com/site/rrettesite/moman and + http://bitbucket.org/jpbarrette/moman/overview/ + + The class org.apache.lucene.util.WeakIdentityMap was derived from + the Apache CXF project and is Apache License 2.0. + + The Google Code Prettify is Apache License 2.0. + See http://code.google.com/p/google-code-prettify/ + + JUnit (junit-4.10) is licensed under the Common Public License v. 1.0 + See http://junit.sourceforge.net/cpl-v10.html + + This product includes code (JaspellTernarySearchTrie) from Java Spelling Checkin + g Package (jaspell): http://jaspell.sourceforge.net/ + License: The BSD License (http://www.opensource.org/licenses/bsd-license.php) + + The snowball stemmers in + analysis/common/src/java/net/sf/snowball + were developed by Martin Porter and Richard Boulton. + The snowball stopword lists in + analysis/common/src/resources/org/apache/lucene/analysis/snowball + were developed by Martin Porter and Richard Boulton. + The full snowball package is available from + http://snowball.tartarus.org/ + + The KStem stemmer in + analysis/common/src/org/apache/lucene/analysis/en + was developed by Bob Krovetz and Sergio Guzman-Lara (CIIR-UMass Amherst) + under the BSD-license. + + The Arabic,Persian,Romanian,Bulgarian, and Hindi analyzers (common) come with a default + stopword list that is BSD-licensed created by Jacques Savoy. These files reside in: + analysis/common/src/resources/org/apache/lucene/analysis/ar/stopwords.txt, + analysis/common/src/resources/org/apache/lucene/analysis/fa/stopwords.txt, + analysis/common/src/resources/org/apache/lucene/analysis/ro/stopwords.txt, + analysis/common/src/resources/org/apache/lucene/analysis/bg/stopwords.txt, + analysis/common/src/resources/org/apache/lucene/analysis/hi/stopwords.txt + See http://members.unine.ch/jacques.savoy/clef/index.html. + + The German,Spanish,Finnish,French,Hungarian,Italian,Portuguese,Russian and Swedish light stemmers + (common) are based on BSD-licensed reference implementations created by Jacques Savoy and + Ljiljana Dolamic. These files reside in: + analysis/common/src/java/org/apache/lucene/analysis/de/GermanLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/de/GermanMinimalStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/es/SpanishLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/fi/FinnishLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/fr/FrenchLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/fr/FrenchMinimalStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/hu/HungarianLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/it/ItalianLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/pt/PortugueseLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/ru/RussianLightStemmer.java + analysis/common/src/java/org/apache/lucene/analysis/sv/SwedishLightStemmer.java + + The Stempel analyzer (stempel) includes BSD-licensed software developed + by the Egothor project http://egothor.sf.net/, created by Leo Galambos, Martin Kvapil, + and Edmond Nolan. + + The Polish analyzer (stempel) comes with a default + stopword list that is BSD-licensed created by the Carrot2 project. The file resides + in stempel/src/resources/org/apache/lucene/analysis/pl/stopwords.txt. + See http://project.carrot2.org/license.html. + + The SmartChineseAnalyzer source code (smartcn) was + provided by Xiaoping Gao and copyright 2009 by www.imdict.net. + + WordBreakTestUnicode_*.java (under modules/analysis/common/src/test/) + is derived from Unicode data such as the Unicode Character Database. + See http://unicode.org/copyright.html for more details. + + The Morfologik analyzer (morfologik) includes BSD-licensed software + developed by Dawid Weiss and Marcin Miłkowski (http://morfologik.blogspot.com/). + + Morfologik uses data from Polish ispell/myspell dictionary + (http://www.sjp.pl/slownik/en/) licenced on the terms of (inter alia) + LGPL and Creative Commons ShareAlike. + + Morfologic includes data from BSD-licensed dictionary of Polish (SGJP) + (http://sgjp.pl/morfeusz/) + + Servlet-api.jar and javax.servlet-*.jar are under the CDDL license, the original + source code for this can be found at http://www.eclipse.org/jetty/downloads.php + + =========================================================================== + Kuromoji Japanese Morphological Analyzer - Apache Lucene Integration + =========================================================================== + + This software includes a binary and/or source version of data from + + mecab-ipadic-2.7.0-20070801 + + which can be obtained from + + http://atilika.com/releases/mecab-ipadic/mecab-ipadic-2.7.0-20070801.tar.gz + + or + + http://jaist.dl.sourceforge.net/project/mecab/mecab-ipadic/2.7.0-20070801/mecab-ipadic-2.7.0-20070801.tar.gz + + =========================================================================== + mecab-ipadic-2.7.0-20070801 Notice + =========================================================================== + + Nara Institute of Science and Technology (NAIST), + the copyright holders, disclaims all warranties with regard to this + software, including all implied warranties of merchantability and + fitness, in no event shall NAIST be liable for + any special, indirect or consequential damages or any damages + whatsoever resulting from loss of use, data or profits, whether in an + action of contract, negligence or other tortuous action, arising out + of or in connection with the use or performance of this software. + + A large portion of the dictionary entries + originate from ICOT Free Software. The following conditions for ICOT + Free Software applies to the current dictionary as well. + + Each User may also freely distribute the Program, whether in its + original form or modified, to any third party or parties, PROVIDED + that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + on, or be attached to, the Program, which is distributed substantially + in the same form as set out herein and that such intended + distribution, if actually made, will neither violate or otherwise + contravene any of the laws and regulations of the countries having + jurisdiction over the User or the intended distribution itself. + + NO WARRANTY + + The program was produced on an experimental basis in the course of the + research and development conducted during the project and is provided + to users as so produced on an experimental basis. Accordingly, the + program is provided without any warranty whatsoever, whether express, + implied, statutory or otherwise. The term "warranty" used herein + includes, but is not limited to, any warranty of the quality, + performance, merchantability and fitness for a particular purpose of + the program and the nonexistence of any infringement or violation of + any right of any third party. + + Each user of the program will agree and understand, and be deemed to + have agreed and understood, that there is no warranty whatsoever for + the program and, accordingly, the entire risk arising from or + otherwise connected with the program is assumed by the user. + + Therefore, neither ICOT, the copyright holder, or any other + organization that participated in or was otherwise related to the + development of the program and their respective officials, directors, + officers and other employees shall be held liable for any and all + damages, including, without limitation, general, special, incidental + and consequential damages, arising out of or otherwise in connection + with the use or inability to use the program or any product, material + or result produced or otherwise obtained by using the program, + regardless of whether they have been advised of, or otherwise had + knowledge of, the possibility of such damages at any time during the + project or thereafter. Each user will be deemed to have agreed to the + foregoing by his or her commencement of use of the program. The term + "use" as used herein includes, but is not limited to, the use, + modification, copying and distribution of the program and the + production of secondary products from the program. + + In the case where the program, whether in its original form or + modified, was distributed or delivered to or received by a user from + any person, organization or entity other than ICOT, unless it makes or + grants independently of ICOT any specific warranty to the user in + writing, such person, organization or entity, will also be exempted + from and not be held liable to the user for any such damages as noted + above as far as the program is concerned. + + (ASLv2) Joda Time + The following NOTICE information applies: + This product includes software developed by + Joda.org (http://www.joda.org/). + + (ASLv2) Apache ActiveMQ + The following NOTICE information applies: + ActiveMQ :: Client + Copyright 2005-2015 The Apache Software Foundation + + (ASLv2) Apache Geronimo + The following NOTICE information applies: + Apache Geronimo + Copyright 2003-2008 The Apache Software Foundation + + (ASLv2) Swagger Core + The following NOTICE information applies: + Swagger Core 1.5.3-M1 + Copyright 2015 Reverb Technologies, Inc. + + (ASLv2) Google GSON + The following NOTICE information applies: + Copyright 2008 Google Inc. + + (ASLv2) JSON-SMART + The following NOTICE information applies: + Copyright 2011 JSON-SMART authors + + (ASLv2) JsonPath + The following NOTICE information applies: + Copyright 2011 JsonPath authors + + (ASLv2) Kite SDK + The following NOTICE information applies: + This product includes software developed by Cloudera, Inc. + (http://www.cloudera.com/). + + This product includes software developed at + The Apache Software Foundation (http://www.apache.org/). + + This product includes software developed by + Saxonica (http://www.saxonica.com/). + + (ASLv2) MongoDB Java Driver + The following NOTICE information applies: + Copyright (C) 2008-2013 10gen, Inc. + + (ASLv2) Parquet MR + The following NOTICE information applies: + Parquet MR + Copyright 2012 Twitter, Inc. + + This project includes code from https://github.com/lemire/JavaFastPFOR + parquet-column/src/main/java/parquet/column/values/bitpacking/LemireBitPacking.java + Apache License Version 2.0 http://www.apache.org/licenses/. + (c) Daniel Lemire, http://lemire.me/en/ + + (ASLv2) Twitter4J + The following NOTICE information applies: + Copyright 2007 Yusuke Yamamoto + + Twitter4J includes software from JSON.org to parse JSON response from the Twitter API. You can see the license term at http://www.JSON.org/license.html + + (ASLv2) JOAuth + The following NOTICE information applies: + JOAuth + Copyright 2010-2013 Twitter, Inc + + (ASLv2) Hosebird Client + The following NOTICE information applies: + Hosebird Client (hbc) + Copyright 2013 Twitter, Inc. + + (ASLv2) GeoIP2 Java API + The following NOTICE information applies: + GeoIP2 Java API + This software is Copyright (c) 2013 by MaxMind, Inc. + + (ASLv2) Woodstox Core ASL + The following NOTICE information applies: + This product currently only contains code developed by authors + of specific components, as identified by the source code files. + + Since product implements StAX API, it has dependencies to StAX API + classes. + + (ASLv2) Amazon Web Services SDK + The following NOTICE information applies: + Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + This product includes software developed by + Amazon Technologies, Inc (http://www.amazon.com/). + + ********************** + THIRD PARTY COMPONENTS + ********************** + This software includes third party software subject to the following copyrights: + - XML parsing and utility functions from JetS3t - Copyright 2006-2009 James Murty. + - JSON parsing and utility functions from JSON.org - Copyright 2002 JSON.org. + - PKCS#1 PEM encoded private key parsing and utility functions from oauth.googlecode.com - Copyright 1998-2010 AOL Inc. + + (ASLv2) Apache Commons DBCP + The following NOTICE information applies: + Apache Commons DBCP + Copyright 2001-2015 The Apache Software Foundation. + + (ASLv2) Apache Commons Pool + The following NOTICE information applies: + Apache Commons Pool + Copyright 1999-2009 The Apache Software Foundation. + + (ASLv2) Apache Derby + The following NOTICE information applies: + Apache Derby + Copyright 2004-2014 Apache, Apache DB, Apache Derby, Apache Torque, Apache JDO, Apache DDLUtils, + the Derby hat logo, the Apache JDO logo, and the Apache feather logo are trademarks of The Apache Software Foundation. + + (ASLv2) Apache Directory Server + The following NOTICE information applies: + ApacheDS Protocol Kerberos Codec + Copyright 2003-2013 The Apache Software Foundation + + ApacheDS I18n + Copyright 2003-2013 The Apache Software Foundation + + Apache Directory API ASN.1 API + Copyright 2003-2013 The Apache Software Foundation + + Apache Directory LDAP API Utilities + Copyright 2003-2013 The Apache Software Foundation + + (ASLv2) Apache Curator + The following NOTICE information applies: + Curator Framework + Copyright 2011-2014 The Apache Software Foundation + + Curator Client + Copyright 2011-2014 The Apache Software Foundation + + Curator Recipes + Copyright 2011-2014 The Apache Software Foundation + + (ASLv2) The Netty Project + The following NOTICE information applies: + The Netty Project + Copyright 2011 The Netty Project + + (ASLv2) Apache Xerces Java + The following NOTICE information applies: + Apache Xerces Java + Copyright 1999-2007 The Apache Software Foundation + + This product includes software developed at + The Apache Software Foundation (http://www.apache.org/). + + Portions of this software were originally based on the following: + - software copyright (c) 1999, IBM Corporation., http://www.ibm.com. + - software copyright (c) 1999, Sun Microsystems., http://www.sun.com. + - voluntary contributions made by Paul Eng on behalf of the + Apache Software Foundation that were originally developed at iClick, Inc., + software copyright (c) 1999. + + (ASLv2) Metadata-Extractor + The following NOTICE information applies: + Metadata-Extractor + Copyright 2002-2015 Drew Noakes + + (ASLv2) Couchbase Java SDK + The following NOTICE information applies: + Couchbase Java SDK + Copyright 2014 Couchbase, Inc. + + (ASLv2) RxJava + The following NOTICE information applies: + Couchbase Java SDK + Copyright 2012 Netflix, Inc. + + (ASLv2) HBase Common + The following NOTICE information applies: + This product includes portions of the Guava project v14, specifically + 'hbase-common/src/main/java/org/apache/hadoop/hbase/io/LimitInputStream.java' + + Copyright (C) 2007 The Guava Authors + + Licensed under the Apache License, Version 2.0 + + (ASLv2) HTrace Core + The following NOTICE information applies: + In addition, this product includes software dependencies. See + the accompanying LICENSE.txt for a listing of dependencies + that are NOT Apache licensed (with pointers to their licensing) + + Apache HTrace includes an Apache Thrift connector to Zipkin. Zipkin + is a distributed tracing system that is Apache 2.0 Licensed. + Copyright 2012 Twitter, Inc. + + (ASLv2) Apache Qpid AMQP 1.0 Client + The following NOTICE information applies: + Copyright 2006-2015 The Apache Software Foundation + + (ASLv2) EventHubs Client (com.microsoft.eventhubs.client:eventhubs-client:0.9.1 - https://github.com/hdinsight/eventhubs-client/) + + (ASLv2) Groovy (org.codehaus.groovy:groovy:jar:2.4.5 - http://www.groovy-lang.org) + The following NOTICE information applies: + Groovy Language + Copyright 2003-2015 The respective authors and developers + Developers and Contributors are listed in the project POM file + and Gradle build file + + This product includes software developed by + The Groovy community (http://groovy.codehaus.org/). + +(ASLv2) Carrotsearch HPPC + The following NOTICE information applies: + HPPC borrowed code, ideas or both from: + + * Apache Lucene, http://lucene.apache.org/ + (Apache license) + * Fastutil, http://fastutil.di.unimi.it/ + (Apache license) + * Koloboke, https://github.com/OpenHFT/Koloboke + (Apache license) + + (ASLv2) t-digest + The following NOTICE information applies: + The code for the t-digest was originally authored by Ted Dunning + A number of small but very helpful changes have been contributed by Adrien Grand (https://github.com/jpountz) + +************************ +Common Development and Distribution License 1.1 +************************ + +The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details. + + (CDDL 1.1) (GPL2 w/ CPE) jersey-core-client (org.glassfish.jersey.core:jersey-client:jar:2.19 - https://jersey.java.net/jersey-client/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-client (com.sun.jersey:jersey-client:jar:1.19 - https://jersey.java.net/jersey-client/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-core-common (org.glassfish.jersey.core:jersey-common:jar:2.19 - https://jersey.java.net/jersey-common/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-core (com.sun.jersey:jersey-core:jar:1.19 - https://jersey.java.net/jersey-core/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-spring (com.sun.jersey:jersey-spring:jar:1.19 - https://jersey.java.net/jersey-spring/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-servlet (com.sun.jersey:jersey-servlet:jar:1.19 - https://jersey.java.net/jersey-servlet/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-multipart (com.sun.jersey:jersey-multipart:jar:1.19 - https://jersey.java.net/jersey-multipart/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-server (com.sun.jersey:jersey-server:jar:1.19 - https://jersey.java.net/jersey-server/) + (CDDL 1.1) (GPL2 w/ CPE) jersey-json (com.sun.jersey:jersey-json:jar:1.19 - https://jersey.java.net/jersey-json/) + (CDDL 1.1) (GPL2 w/ CPE) Old JAXB Runtime (com.sun.xml.bind:jaxb-impl:jar:2.2.3-1 - http://jaxb.java.net/) + (CDDL 1.1) (GPL2 w/ CPE) Java Architecture For XML Binding (javax.xml.bind:jaxb-api:jar:2.2.2 - https://jaxb.dev.java.net/) + (CDDL 1.1) (GPL2 w/ CPE) MIME Streaming Extension (org.jvnet.mimepull:mimepull:jar:1.9.3 - http://mimepull.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JavaMail API (compat) (javax.mail:mail:jar:1.4.7 - http://kenai.com/projects/javamail/mail) + (CDDL 1.1) (GPL2 w/ CPE) JSP Implementation (org.glassfish.web:javax.servlet.jsp:jar:2.3.2 - http://jsp.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages (TM) TagLib Implementation (org.glassfish.web:javax.servlet.jsp.jstl:jar:1.2.2 - http://jstl.java.net) + (CDDL 1.1) (GPL2 w/ CPE) Expression Language 3.0 (org.glassfish:javax.el:jar:3.0.0 - http://el-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages(TM) API (javax.servlet.jsp:javax.servlet.jsp-api:jar:2.3.1 - http://jsp.java.net) + (CDDL 1.1) (GPL2 w/ CPE) Expression Language 3.0 API (javax.el:javax.el-api:jar:3.0.0 - http://uel-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages(TM) Standard Tag Library API (javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:jar:1.2.1 - http://jcp.org/en/jsr/detail?id=52) + (CDDL 1.1) (GPL2 w/ CPE) Java Servlet API (javax.servlet:javax.servlet-api:jar:3.1.0 - http://servlet-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) Javax JMS Api (javax.jms:javax.jms-api:jar:2.0.1 - http://java.net/projects/jms-spec/pages/Home) + (CDDL 1.1) (GPL2 w/ CPE) JSON Processing API (javax.json:javax.json-api:jar:1.0 - http://json-processing-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) JSON Processing Default Provider (org.glassfish:javax.json:jar:1.0.4 - https://jsonp.java.net) + (CDDL 1.1) (GPL2 w/ CPE) OSGi resource locator bundle (org.glassfish.hk2:osgi-resource-locator:jar:1.0.1 - http://glassfish.org/osgi-resource-locator) + (CDDL 1.1) (GPL2 w/ CPE) javax.annotation API (javax.annotation:javax.annotation-api:jar:1.2 - http://jcp.org/en/jsr/detail?id=250) + (CDDL 1.1) (GPL2 w/ CPE) HK2 API module (org.glassfish.hk2:hk2-api:jar:2.4.0-b25 - https://hk2.java.net/hk2-api) + (CDDL 1.1) (GPL2 w/ CPE) ServiceLocator Default Implementation (org.glassfish.hk2:hk2-locator:jar:2.4.0-b25 - https://hk2.java.net/hk2-locator) + (CDDL 1.1) (GPL2 w/ CPE) HK2 Implementation Utilities (org.glassfish.hk2:hk2-utils:jar:2.4.0-b25 - https://hk2.java.net/hk2-utils) + (CDDL 1.1) (GPL2 w/ CPE) aopalliance version 1.0 repackaged as a module (org.glassfish.hk2.external:aopalliance-repackaged:jar:2.4.0-b25 - https://hk2.java.net/external/aopalliance-repackaged) + (CDDL 1.1) (GPL2 w/ CPE) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:jar:2.4.0-b25 - https://hk2.java.net/external/javax.inject) + (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:jar:2.0.1 - http://jax-rs-spec.java.net) + (CDDL 1.1) (GPL2 w/ CPE) jersey-repackaged-guava (org.glassfish.jersey.bundles.repackaged:jersey-guava:bundle:2.19 - https://jersey.java.net/project/project/jersey-guava/) + + +************************ +Common Development and Distribution License 1.0 +************************ + +The following binary components are provided under the Common Development and Distribution License 1.0. See project link for details. + + (CDDL 1.0) JavaServlet(TM) Specification (javax.servlet:servlet-api:jar:2.5 - no url available) + (CDDL 1.0) (GPL3) Streaming API For XML (javax.xml.processor:stax-api:jar:1.0-2 - no url provided) + (CDDL 1.0) JavaBeans Activation Framework (JAF) (javax.activation:activation:jar:1.1 - http://java.sun.com/products/javabeans/jaf/index.jsp) + (CDDL 1.0) JSR311 API (javax.ws.rs:jsr311-api:jar:1.1.1 - https://jsr311.dev.java.net) + +************************ +Creative Commons Attribution-ShareAlike 3.0 +************************ + +The following binary components are provided under the Creative Commons Attribution-ShareAlike 3.0. See project link for details. + + (CCAS 3.0) MaxMind DB (https://github.com/maxmind/MaxMind-DB) + +************************ +Eclipse Public License 1.0 +************************ + +The following binary components are provided under the Eclipse Public License 1.0. See project link for details. + + (EPL 1.0) AspectJ Weaver (org.aspectj:aspectjweaver:jar:1.8.5 - http://www.aspectj.org) + (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:1.3.176 - http://www.h2database.com/html/license.html) + (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.1.3 - http://logback.qos.ch/) + (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.1.3 - http://logback.qos.ch/) + (EPLv1.0) JRuby (org.jruby:jruby-complete:9.0.4.0 - http://jruby.org). + + JRuby is licensed under three licenses - the EPL 1.0, GPL 2 and LGPL 2.1. Apache NiFi uses the EPL v1.0 license. + + The following NOTICE information applies: + Copyright (c) 2007-2015 The JRuby project + +***************** +Mozilla Public License v2.0 +***************** + +The following binary components are provided under the Mozilla Public License v2.0. See project link for details. + + (MPL 2.0) Saxon HE (net.sf.saxon:Saxon-HE:jar:9.6.0-5 - http://www.saxonica.com/) + +***************** +Mozilla Public License v1.1 +***************** + +The following binary components are provided under the Mozilla Public License v1.1. See project link for details. + + (MPL 1.1) HAPI Base (ca.uhn.hapi:hapi-com.hurence.logisland.logisland.parser.base:2.2 - http://hl7api.sourceforge.net/) + (MPL 1.1) HAPI Structures (ca.uhn.hapi:hapi-structures-v*:2.2 - http://hl7api.sourceforge.net/) + +****************** +Python Software Foundation License v2 +****************** + +The following binary components are provided under the Python Software Foundation License v2 + + (PSFLv2) Jython (org.python:jython-standalone:2.7.0 - http://www.jython.org/) + The following NOTICE information applies: + Copyright (c) 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 Jython Developers All rights reserved. + +****************** +MIT License +****************** + +The following binary components are provided under an MIT-style license + + (MIT) Luaj (org.luaj:luaj-jse:3.0.1 - http://www.luaj.org/luaj/3.0/README.html) + The following NOTICE information applies: + Copyright (c) 2009-2011 Luaj.org. All rights reserved. + +***************** +Public Domain +***************** + +The following binary components are provided to the 'Public Domain'. See project link for details. + + (Public Domain) XZ for Java (org.tukaani:xz:jar:1.5 - http://tukaani.org/xz/java.html + (Public Domain) AOP Alliance 1.0 (http://aopalliance.sourceforge.net/) + +The following binary components are provided under the Creative Commons Zero license version 1.0. See project link for details. + + (CC0v1.0) JSR166e for Twitter (com.twitter:jsr166e:jar:1.1.0 - https://github.com/twitter/jsr166e) + diff --git a/logisland-assembly/README.md b/logisland-assembly/README.md new file mode 100644 index 000000000..e5edb2f24 --- /dev/null +++ b/logisland-assembly/README.md @@ -0,0 +1,63 @@ + +# Apache NiFi + +Apache NiFi is an easy to use, powerful, and reliable system to process and distribute data. + +## Table of Contents + +- [Features](#features) +- [Getting Started](#getting-started) +- [Getting Help](#getting-help) +- [Requirements](#requirements) +- [License](#license) +- [Export Control] (#export-control) + +## Features + +Logisland was made for providing event mining tools on logs streams at scale. + +## Getting Started + +To start Logisland: +- execute bin/logisland.sh start +- Direct your browser to http://localhost:8080/logisland/ + +## Getting Help +If you have questions, you can reach out to our mailing list: contact@hurence.com + +We're also often available in IRC: #logisland on +[irc.freenode.net](http://webchat.freenode.net/?channels=#logisland). + +## Requirements +* JDK 1.7 or higher + +## License + +Except as otherwise noted this software is licensed under the +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + From 9bef0d5aba7c1fb7c30d9c2755d2dd1502bfb599 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 6 Jul 2018 12:19:48 +0200 Subject: [PATCH 55/63] Avoid serialization issues on 1.6 --- .../logisland/engine/spark/KafkaStreamProcessingEngine.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index e916aae81..5aa3f9239 100644 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -292,7 +292,6 @@ object KafkaStreamProcessingEngine { class KafkaStreamProcessingEngine extends AbstractProcessingEngine { private val logger = LoggerFactory.getLogger(classOf[KafkaStreamProcessingEngine]) - private val conf = new SparkConf() override def getSupportedPropertyDescriptors: util.List[PropertyDescriptor] = { @@ -367,6 +366,7 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { * job configuration */ + val conf = new SparkConf() conf.setAppName(appName) conf.setMaster(sparkMaster) def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { @@ -466,7 +466,7 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { override def awaitTermination(engineContext: EngineContext): Unit = { var timeout = engineContext.getPropertyValue(KafkaStreamProcessingEngine.SPARK_STREAMING_TIMEOUT) .asInteger().toInt - val sc = SparkContext.getOrCreate(conf) + val sc = SparkContext.getOrCreate() while (!sc.isStopped) { try { From ce20a1bda438b5c7b5fc3806b5789a853ccfb3fb Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Fri, 6 Jul 2018 17:51:14 +0200 Subject: [PATCH 56/63] Fix es deps and serialization issues --- .../engine/spark/KafkaStreamProcessingEngine.scala | 2 +- .../logisland-common-processors-plugin/pom.xml | 10 +--------- .../logisland-elasticsearch-plugin/pom.xml | 6 ++---- .../pom.xml | 7 +++---- .../pom.xml | 1 + 5 files changed, 8 insertions(+), 18 deletions(-) diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index 5aa3f9239..7e9ba8207 100644 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -366,7 +366,7 @@ class KafkaStreamProcessingEngine extends AbstractProcessingEngine { * job configuration */ - val conf = new SparkConf() + @transient val conf = new SparkConf() conf.setAppName(appName) conf.setMaster(sparkMaster) def setConfProperty(conf: SparkConf, engineContext: EngineContext, propertyDescriptor: PropertyDescriptor) = { diff --git a/logisland-plugins/logisland-common-processors-plugin/pom.xml b/logisland-plugins/logisland-common-processors-plugin/pom.xml index 019bd94d9..504cc6b7f 100644 --- a/logisland-plugins/logisland-common-processors-plugin/pom.xml +++ b/logisland-plugins/logisland-common-processors-plugin/pom.xml @@ -81,10 +81,6 @@ com.google.guava guava
- - com.hurence.logisland - logisland-elasticsearch-plugin - @@ -98,10 +94,6 @@ logisland-cache_key_value-service-api test - - com.hurence.logisland - logisland-elasticsearch_2_4_0-client-service - test - + diff --git a/logisland-plugins/logisland-elasticsearch-plugin/pom.xml b/logisland-plugins/logisland-elasticsearch-plugin/pom.xml index d896d39a5..4cfae185d 100644 --- a/logisland-plugins/logisland-elasticsearch-plugin/pom.xml +++ b/logisland-plugins/logisland-elasticsearch-plugin/pom.xml @@ -66,13 +66,11 @@ commons-io test - - joda-time - joda-time - + com.fasterxml.jackson.core jackson-core + 2.6.6 com.hurence.logisland diff --git a/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml b/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml index 7898911a5..5a9f22349 100644 --- a/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml +++ b/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml @@ -30,10 +30,8 @@ org.slf4j slf4j-api - - com.hurence.logisland - logisland-api - + + com.hurence.logisland logisland-elasticsearch-client-service-api @@ -67,6 +65,7 @@ com.fasterxml.jackson.core jackson-core + 2.6.6 diff --git a/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml b/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml index 3c220864e..9e60bb364 100644 --- a/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml +++ b/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml @@ -82,6 +82,7 @@ com.fasterxml.jackson.core jackson-core + 2.6.6 From d80b4322950106ea601dc3aecab4b9a78a5efd07 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Mon, 9 Jul 2018 19:52:21 +0200 Subject: [PATCH 57/63] Fix unit testing --- .../converter/LogIslandRecordConverterTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java index 3228df6e0..d98a1811c 100644 --- a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java +++ b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java @@ -46,14 +46,22 @@ private LogIslandRecordConverter setupInstance(Class private void assertFieldEquals(Record record, String fieldName, Object expected) { Field field = record.getField(fieldName); - assertNotNull(field); - assertEquals(expected, record.getField(fieldName).getRawValue()); + if (expected == null) { + assertNull(field); + } else { + assertNotNull(field); + assertEquals(expected, field.getRawValue()); + } } private void assertFieldEquals(Record record, String fieldName, byte[] expected) { Field field = record.getField(fieldName); - assertNotNull(field); - assertArrayEquals(expected, (byte[]) record.getField(fieldName).getRawValue()); + if (expected == null) { + assertNull(field); + } else { + assertNotNull(field); + assertArrayEquals(expected, (byte[]) field.getRawValue()); + } } From c066f44703a5d770a885bf3c3d2e86b3b953b142 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Tue, 10 Jul 2018 16:46:00 +0200 Subject: [PATCH 58/63] Typo in Serializer validation. Now supports stringSerializer too --- .../main/scala/com/hurence/logisland/stream/spark/package.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala index c99d21c0f..8cebb3aee 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala @@ -91,7 +91,7 @@ object StreamProperties { "avro serialization", "serialize events as json blocs") val KRYO_SERIALIZER = new AllowableValue(classOf[KryoSerializer].getName, "kryo serialization", "serialize events as binary blocs") - val STRING_SERIALIZER = new AllowableValue(classOf[KryoSerializer].getName, + val STRING_SERIALIZER = new AllowableValue(classOf[StringSerializer].getName, "string serialization", "serialize events as string") val BYTESARRAY_SERIALIZER = new AllowableValue(classOf[BytesArraySerializer].getName, "byte array serialization", "serialize events as byte arrays") From 4db031078190aa2027f9b9f6a646fda55e6383e9 Mon Sep 17 00:00:00 2001 From: oalam Date: Fri, 13 Jul 2018 12:26:57 +0200 Subject: [PATCH 59/63] update build version + minor fix on JS components --- README.rst | 16 +- launch-tuto.sh | 2 +- logisland-api/pom.xml | 2 +- .../component/AbstractPropertyValue.java | 2 +- .../logisland/component/ComponentContext.java | 8 +- .../DecoratedInterpretedPropertyValue.java | 2 +- .../component/InterpretedPropertyValue.java | 2 +- .../component/PropertyValueFactory.java | 2 +- .../logisland/config/DefaultConfigValues.java | 8 +- .../logisland/engine/EngineContext.java | 8 +- .../engine/MockProcessingEngine.java | 8 +- .../logisland/engine/ProcessingEngine.java | 8 +- .../engine/StandardEngineContext.java | 8 +- .../expressionlanguage/InterpreterEngine.java | 2 +- .../InterpreterEngineException.java | 2 +- .../InterpreterEngineFactory.java | 2 +- .../expressionlanguage/Jsr223Context.java | 2 +- .../Jsr223InterpreterEngine.java | 10 +- .../com/hurence/logisland/record/Field.java | 8 +- .../logisland/record/FieldDictionary.java | 13 +- .../hurence/logisland/record/Position.java | 15 +- .../logisland/record/StandardRecord.java | 8 +- .../DatastoreClientServiceException.java | 15 +- .../datastore/MultiGetQueryRecord.java | 6 +- .../datastore/MultiGetQueryRecordBuilder.java | 2 +- .../stream/StandardStreamContext.java | 8 +- .../logisland/stream/StreamContext.java | 8 +- .../hurence/logisland/util/FormatUtils.java | 15 +- .../com/hurence/logisland/util/Tuple.java | 15 +- .../validator/StandardValidators.java | 8 +- .../logisland/record/StandardRecordTest.java | 8 +- logisland-assembly/pom.xml | 2 +- .../logisland-connect-spark/pom.xml | 2 +- .../converter/LogIslandRecordConverter.java | 16 +- .../source/KafkaConnectStreamSource.java | 6 +- .../KafkaConnectStreamSourceProvider.java | 6 +- .../source/SharedSourceTaskContext.java | 6 +- .../connect/source/SourceThread.java | 6 +- .../source/timed/ClockSourceConnector.java | 16 +- .../connect/source/timed/ClockSourceTask.java | 16 +- ...afkaConnectStructuredProviderService.scala | 16 +- .../stream/spark/provider/package.scala | 16 +- .../logisland/connect/KafkaConnectTest.java | 15 +- .../LogIslandRecordConverterTest.java | 16 +- .../logisland/connect/fake/FakeConnector.java | 16 +- .../resources/conf/kafka-connect-stream.yml | 2 +- .../logisland-connectors-bundle/pom.xml | 2 +- .../src/main/java/Dummy.java | 16 +- .../logisland-connector-opc/pom.xml | 2 +- .../logisland/connect/opc/CommonUtils.java | 16 +- .../connect/opc/OpcRecordFields.java | 16 +- .../connect/opc/SmartOpcOperations.java | 16 +- .../logisland/connect/opc/TagInfo.java | 16 +- .../connect/opc/da/OpcDaSourceConnector.java | 16 +- .../connect/opc/da/OpcDaSourceTask.java | 16 +- .../connect/opc/ua/OpcUaSourceConnector.java | 16 +- .../connect/opc/ua/OpcUaSourceTask.java | 16 +- .../opc/da/OpcDaSourceConnectorTest.java | 16 +- .../connect/opc/ua/OpcUaSourceTaskTest.java | 16 +- .../logisland-connectors/pom.xml | 2 +- logisland-connect/pom.xml | 2 +- logisland-docker/full-container/Dockerfile | 2 +- logisland-docker/full-container/README.rst | 10 +- logisland-docker/pom.xml | 2 +- .../src/it/resources/data/all-tutorials.yml | 2 +- logisland-documentation/changes.rst | 2 +- logisland-documentation/conf.py | 4 +- logisland-documentation/developer.rst | 4 +- logisland-documentation/monitoring.rst | 4 +- logisland-documentation/overview-slides.md | 2 +- logisland-documentation/plugins_old.rst | 2 +- logisland-documentation/pom.xml | 2 +- logisland-documentation/release.rst | 79 ++ .../tutorials/prerequisites.rst | 4 +- .../logisland-spark_1_6-engine/pom.xml | 2 +- .../util/spark/ProcessorMetrics.java | 8 +- .../logisland/util/kafka/KafkaReporter.scala | 15 + .../apache/spark/metrics/sink/KafkaSink.scala | 15 + .../logisland-spark_2_1-engine/pom.xml | 2 +- ...PipelineConfigurationBroadcastWrapper.java | 16 +- .../engine/spark/remote/RemoteApiClient.java | 16 +- .../remote/RemoteApiComponentFactory.java | 16 +- .../engine/spark/remote/model/Component.java | 16 +- .../engine/spark/remote/model/DataFlow.java | 16 +- .../engine/spark/remote/model/Pipeline.java | 16 +- .../engine/spark/remote/model/Processor.java | 16 +- .../engine/spark/remote/model/Property.java | 16 +- .../engine/spark/remote/model/Service.java | 16 +- .../engine/spark/remote/model/Stream.java | 16 +- .../engine/spark/remote/model/Versioned.java | 16 +- .../util/spark/ProcessorMetrics.java | 8 +- .../util/spark/ProtoBufRegistrator.java | 15 +- .../spark/KafkaStreamProcessingEngine.scala | 15 + .../RemoteApiStreamProcessingEngine.scala | 16 +- .../spark/AbstractKafkaRecordStream.scala | 15 + .../stream/spark/DummyRecordStream.scala | 16 +- .../KafkaRecordStreamParallelProcessing.scala | 15 + .../logisland/stream/spark/package.scala | 15 + .../stream/spark/structured/Sandbox.scala | 15 + .../spark/structured/StructuredStream.scala | 15 + .../spark/structured/handler/HDFSBurner.scala | 15 + .../handler/PipelineProcessor.scala | 15 + .../structured/handler/SQLAggregator.scala | 15 + .../handler/StructuredStreamHandler.scala | 15 + ...nsoleStructuredStreamProviderService.scala | 15 +- .../provider/KafkaStreamWriter.scala | 16 +- ...KafkaStructuredStreamProviderService.scala | 16 +- .../MQTTStructuredStreamProviderService.scala | 15 +- .../StructuredStreamProviderService.scala | 15 + .../util/kafka/ConsumerGroupUtils.scala | 15 + .../logisland/util/kafka/KafkaReporter.scala | 15 + .../util/mqtt/MQTTStreamSource.scala | 16 +- .../logisland/util/mqtt/MessageStore.scala | 16 +- .../apache/spark/metrics/sink/KafkaSink.scala | 15 + ...stractStreamProcessingIntegrationTest.java | 8 +- .../logisland/engine/RemoteApiEngineTest.java | 15 +- .../spark/remote/RemoteApiClientTest.java | 16 +- .../spark/remote/mock/MockProcessor.java | 16 +- .../remote/mock/MockServiceController.java | 16 +- .../engine/spark/remote/mock/MockStream.java | 16 +- .../src/test/resources/conf/remote-engine.yml | 2 +- .../test/resources/conf/structured-stream.yml | 2 +- logisland-engines/pom.xml | 2 +- .../logisland-bootstrap/pom.xml | 2 +- .../runner/StreamProcessingRunner.java | 2 +- .../logisland-hadoop-utils/pom.xml | 2 +- .../logisland-resources/pom.xml | 2 +- .../main/resources/conf/aggregate-events.yml | 2 +- .../resources/conf/configuration-template.yml | 2 +- .../conf/data/threshold-alerting/apache-1.log | 6 + .../main/resources/conf/docker-compose.yml | 4 +- .../resources/conf/enrich-apache-logs.yml | 2 +- .../resources/conf/future-factory-indexer.yml | 2 +- .../resources/conf/index-apache-logs-solr.yml | 2 +- .../main/resources/conf/index-apache-logs.yml | 2 +- .../conf/index-blockchain-transactions.yml | 2 +- .../resources/conf/index-network-packets.yml | 2 +- .../conf/logisland-kafka-connect.yml | 2 +- .../src/main/resources/conf/match-queries.yml | 2 +- .../main/resources/conf/mqtt-to-historian.yml | 2 +- .../src/main/resources/conf/opc-iiot.yml | 2 +- .../main/resources/conf/outlier-detection.yml | 2 +- .../main/resources/conf/python-processing.yml | 2 +- .../conf/retrieve-data-from-elasticsearch.yml | 2 +- .../src/main/resources/conf/save-to-hdfs.yml | 2 +- .../conf/send-apache-logs-to-hbase.yml | 2 +- .../main/resources/conf/store-to-redis.yml | 2 +- .../resources/conf/timeseries-parsing.yml | 2 +- .../src/main/resources/docs/changes.rst | 2 +- .../src/main/resources/docs/developer.rst | 4 +- .../src/main/resources/docs/monitoring.rst | 4 +- .../src/main/resources/docs/plugins_old.rst | 2 +- .../main/resources/docs/tutorials/index.rst | 1 + .../docs/tutorials/prerequisites.rst | 4 +- .../logisland-scripting-base/pom.xml | 2 +- .../logisland/jsr223/BaseScriptEngine.java | 2 +- .../logisland/jsr223/BindingsImpl.java | 2 +- .../logisland/jsr223/ScriptContextImpl.java | 2 +- .../logisland-scripting-mvel/pom.xml | 2 +- .../jsr223/mvel/MvelCompiledScript.java | 2 +- .../jsr223/mvel/MvelScriptEngine.java | 2 +- .../jsr223/mvel/MvelScriptEngineFactory.java | 2 +- .../logisland/jsr223/mvel/TestMvelEngine.java | 2 +- .../logisland-scripting/pom.xml | 2 +- logisland-framework/logisland-utils/pom.xml | 2 +- .../logisland/component/ComponentFactory.java | 8 +- .../logisland/config/ConfigReader.java | 8 +- .../StandardControllerServiceLookup.java | 8 +- .../com/hurence/logisland/metrics/Names.java | 16 +- .../logisland/serializer/JsonSerializer.java | 8 +- .../serializer/KuraProtobufSerializer.java | 15 +- .../serializer/StringSerializer.java | 8 +- .../com/hurence/logisland/util/GZipUtil.java | 15 +- .../com/hurence/logisland/util/ListUtils.java | 15 +- .../logisland/util/kura/KuraPayload.java | 15 +- .../util/kura/KuraPayloadDecoder.java | 15 +- .../util/kura/KuraPayloadEncoder.java | 15 +- .../logisland/util/kura/KuraPosition.java | 15 +- .../hurence/logisland/util/kura/Metric.java | 15 +- .../hurence/logisland/util/kura/Metrics.java | 15 +- .../hurence/logisland/util/kura/Optional.java | 22 +- .../hurence/logisland/util/kura/Payload.java | 15 +- ...ontrollerServiceInitializationContext.java | 9 +- .../util/runner/MockProcessContext.java | 8 +- .../logisland/util/stream/io/StreamUtils.java | 8 +- .../logisland/util/string/StringUtils.java | 8 +- .../serializer/JsonSerializerTest.java | 8 +- .../KuraProtobufSerializerTest.java | 8 +- .../util/kura/KuraPayloadDecoderTest.java | 8 +- .../util/string/StringUtilsTest.java | 15 +- .../resources/configuration-templatev2.yml | 2 +- logisland-framework/pom.xml | 2 +- .../logisland-botsearch-plugin/pom.xml | 2 +- .../logisland/math/FlowDistanceMeasure.java | 2 +- .../logisland/botsearch/TraceTest.java | 8 +- .../resources/data/TracesAnalysis_samples.txt | 4 +- .../test/resources/enriched_sample_values.txt | 4 +- .../logisland-common-logs-plugin/pom.xml | 2 +- .../commonlogs/gitlab/ParseGitlabLog.java | 2 +- .../commonlogs/gitlab/ParseGitlabLogTest.java | 2 +- .../pom.xml | 2 +- .../hurence/logisland/processor/FlatMap.java | 8 +- .../hurence/logisland/processor/ModifyId.java | 8 +- .../AbstractNashornSandboxProcessor.java | 15 +- .../processor/alerting/CheckAlerts.java | 46 +- .../processor/alerting/CheckThresholds.java | 63 +- .../processor/alerting/ComputeTags.java | 35 +- .../datastore/AbstractDatastoreProcessor.java | 15 +- .../processor/datastore/BulkPut.java | 15 +- .../processor/datastore/EnrichRecords.java | 15 +- .../processor/datastore/MultiGet.java | 15 +- .../logisland/processor/FlatMapTest.java | 8 +- .../processor/alerting/CheckAlertsTest.java | 15 +- .../alerting/CheckThresholdsTest.java | 15 +- .../processor/alerting/ComputeTagsTest.java | 15 +- .../datastore/EnrichRecordsTest.java | 15 +- .../datastore/MockDatastoreService.java | 15 +- .../resources/data/TracesAnalysis_samples.txt | 4 +- .../test/resources/enriched_sample_values.txt | 4 +- .../logisland-cyber-security-plugin/pom.xml | 2 +- .../logisland-elasticsearch-plugin/pom.xml | 2 +- .../logisland-enrichment-plugin/pom.xml | 2 +- .../enrichment/IpAbstractProcessor.java | 15 + .../processor/enrichment/IpToFqdn.java | 15 + .../processor/enrichment/IpToGeo.java | 10 +- .../processor/enrichment/IpToFqdnTest.java | 15 + .../processor/enrichment/IpToGeoTest.java | 15 + .../logisland-excel-plugin/pom.xml | 2 +- .../processor/excel/ExcelExtract.java | 5 +- .../excel/ExcelExtractProperties.java | 6 +- .../logisland/processor/excel/Fields.java | 6 +- .../processor/excel/ExcelExtractTest.java | 6 +- .../logisland-hbase-plugin/pom.xml | 2 +- .../processor/hbase/util/TestObjectSerDe.java | 9 +- .../pom.xml | 2 +- .../converters/MappingConverter.java | 8 +- .../logisland-querymatcher-plugin/pom.xml | 2 +- .../logisland/processor/MatchHandlers.java | 2 +- .../hurence/logisland/processor/MatchIP.java | 2 +- .../logisland/processor/MatchQuery.java | 2 +- .../logisland/processor/MatchingRule.java | 2 +- .../logisland/processor/MatchIPTest.java | 2 +- .../logisland/processor/MatchQueryTest.java | 2 +- .../logisland-sampling-plugin/pom.xml | 2 +- .../src/test/resources/data/raw-data1.txt | 656 ++++++------ .../src/test/resources/data/raw-data2.txt | 942 +++++++++--------- .../logisland-scripting-plugin/pom.xml | 2 +- .../logisland-useragent-plugin/pom.xml | 2 +- .../logisland-web-analytics-plugin/pom.xml | 2 +- logisland-plugins/pom.xml | 2 +- .../pom.xml | 2 +- .../cache/CSVKeyValueCacheService.java | 10 +- .../cache/CSVKeyValueCacheServiceTest.java | 10 +- .../service/cache/model/LRUCacheTest.java | 15 + .../cache/model/LRULinkedHashMapTest.java | 15 + .../pom.xml | 2 +- .../multiGet/MultiGetQueryRecord.java | 6 +- .../multiGet/MultiGetQueryRecordBuilder.java | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../ElasticsearchRecordConverter.java | 2 +- .../service/elasticsearch/GeoPoint.java | 15 +- .../ElasticsearchRecordConverterTest.java | 15 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../logisland-ip-to-geo-service-api/pom.xml | 2 +- .../service/iptogeo/IpToGeoService.java | 2 +- .../pom.xml | 2 +- .../maxmind/MaxmindIpToGeoService.java | 2 +- .../maxmind/MaxmindIpToGeoServiceTest.java | 2 +- .../logisland-redis_4-client-service/pom.xml | 2 +- .../logisland/redis/RedisConnectionPool.java | 15 +- .../hurence/logisland/redis/RedisType.java | 15 +- .../redis/service/RedisConnectionPool.java | 15 +- .../service/RedisKeyValueCacheService.java | 8 +- .../logisland/redis/util/RedisAction.java | 15 +- .../logisland/redis/util/RedisUtils.java | 15 +- .../redis/service/FakeRedisProcessor.java | 15 +- .../ITRedisKeyValueCacheClientService.java | 15 +- .../TestRedisConnectionPoolService.java | 15 +- .../logisland-solr-client-service-api/pom.xml | 2 +- .../service/solr/api/SolrRecordConverter.java | 2 +- .../service/solr/api/SolrUpdater.java | 15 +- .../pom.xml | 4 +- .../service/solr/SolrRecordConverterTest.java | 15 +- .../pom.xml | 4 +- .../pom.xml | 2 +- .../service/solr/ChronixUpdater.java | 15 +- .../solr/Solr_6_4_2_ChronixClientService.java | 8 +- .../solr/ChronixClientServiceTest.java | 8 +- .../service/solr/SolrTokenizationTest.java | 15 +- .../pom.xml | 6 +- .../solr/Solr_6_6_2_RecordConverter.java | 15 + .../logisland-solr-client-service/pom.xml | 2 +- logisland-services/pom.xml | 2 +- pom.xml | 2 +- 296 files changed, 2265 insertions(+), 1944 deletions(-) create mode 100644 logisland-documentation/release.rst create mode 100644 logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-1.log diff --git a/README.rst b/README.rst index 9ffdad01e..97f14334b 100755 --- a/README.rst +++ b/README.rst @@ -53,7 +53,7 @@ to build from the source just clone source and package with maven cd logisland mvn clean install -the final package is available at `logisland-assembly/target/logisland-0.13.0-bin-hdp2.5.tar.gz` +the final package is available at `logisland-assembly/target/logisland-0.14.0-bin-hdp2.5.tar.gz` You can also download the `latest release build `_ @@ -76,9 +76,9 @@ Alternatively you can deploy **logisland** on any linux server from which Kafka curl -s http://d3kbcqa49mib13.cloudfront.net/spark-2.1.0-bin-hadoop2.7.tgz | tar -xz -C /usr/local/ export SPARK_HOME=/usr/local/spark-2.1.0-bin-hadoop2.7 - # install Logisland 0.13.0 - curl -s https://github.com/Hurence/logisland/releases/download/v0.10.0/logisland-0.13.0-bin-hdp2.5.tar.gz | tar -xz -C /usr/local/ - cd /usr/local/logisland-0.13.0 + # install Logisland 0.14.0 + curl -s https://github.com/Hurence/logisland/releases/download/v0.10.0/logisland-0.14.0-bin-hdp2.5.tar.gz | tar -xz -C /usr/local/ + cd /usr/local/logisland-0.14.0 # launch a logisland job bin/logisland.sh --conf conf/index-apache-logs.yml @@ -107,9 +107,9 @@ Launching logisland streaming apps is just easy as unarchiving logisland distrib .. code-block:: sh - # install Logisland 0.13.0 - curl -s https://github.com/Hurence/logisland/releases/download/v0.10.0/logisland-0.13.0-bin-hdp2.5.tar.gz | tar -xz -C /usr/local/ - cd /usr/local/logisland-0.13.0 + # install Logisland 0.14.0 + curl -s https://github.com/Hurence/logisland/releases/download/v0.10.0/logisland-0.14.0-bin-hdp2.5.tar.gz | tar -xz -C /usr/local/ + cd /usr/local/logisland-0.14.0 bin/logisland.sh --conf conf/index-apache-logs.yml @@ -130,7 +130,7 @@ The first part is the `ProcessingEngine` configuration (here a Spark streaming e .. code-block:: yaml - version: 0.13.0 + version: 0.14.0 documentation: LogIsland job config file engine: component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine diff --git a/launch-tuto.sh b/launch-tuto.sh index 6e2e62be1..c1bb94bab 100755 --- a/launch-tuto.sh +++ b/launch-tuto.sh @@ -1,4 +1,4 @@ #!/bin/bash -logisland-assembly/target/logisland-0.13.0-bin-hdp2.5/logisland-0.13.0/bin/logisland.sh \ +logisland-assembly/target/logisland-0.14.0-bin-hdp2.5/logisland-0.14.0/bin/logisland.sh \ --conf logisland-framework/logisland-resources/src/main/resources/conf/$1 diff --git a/logisland-api/pom.xml b/logisland-api/pom.xml index 97e856c71..ec9b42833 100644 --- a/logisland-api/pom.xml +++ b/logisland-api/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 logisland-api jar diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java b/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java index 4e5b2676a..ea4e58e11 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/AbstractPropertyValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java b/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java index 41f63e98b..c84080563 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/ComponentContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/DecoratedInterpretedPropertyValue.java b/logisland-api/src/main/java/com/hurence/logisland/component/DecoratedInterpretedPropertyValue.java index 023294eb8..03155458f 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/DecoratedInterpretedPropertyValue.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/DecoratedInterpretedPropertyValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/InterpretedPropertyValue.java b/logisland-api/src/main/java/com/hurence/logisland/component/InterpretedPropertyValue.java index f9eefdd86..ad37510ae 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/InterpretedPropertyValue.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/InterpretedPropertyValue.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/component/PropertyValueFactory.java b/logisland-api/src/main/java/com/hurence/logisland/component/PropertyValueFactory.java index dc6990a56..588defb11 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/component/PropertyValueFactory.java +++ b/logisland-api/src/main/java/com/hurence/logisland/component/PropertyValueFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java b/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java index 2b14041c2..32ba8ba6d 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java +++ b/logisland-api/src/main/java/com/hurence/logisland/config/DefaultConfigValues.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java index 31912cdf4..0ec4d6efb 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/EngineContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java b/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java index ae2e621fc..e2e424453 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/MockProcessingEngine.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java b/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java index 5784809df..5180a0c62 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/ProcessingEngine.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java index 729e4112a..132c8adab 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/engine/StandardEngineContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngine.java b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngine.java index 126269e14..61ed055ae 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngine.java +++ b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngine.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineException.java b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineException.java index 1db0caa3d..193010c48 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineException.java +++ b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineException.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineFactory.java b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineFactory.java index 8a743f15a..bc0020988 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineFactory.java +++ b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/InterpreterEngineFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223Context.java b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223Context.java index 4eaf12d0d..228f43486 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223Context.java +++ b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223Context.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223InterpreterEngine.java b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223InterpreterEngine.java index e7763affd..cf8b50db5 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223InterpreterEngine.java +++ b/logisland-api/src/main/java/com/hurence/logisland/expressionlanguage/Jsr223InterpreterEngine.java @@ -1,12 +1,12 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) - *

+ * Copyright (C) 2016 Hurence (support@hurence.com) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/Field.java b/logisland-api/src/main/java/com/hurence/logisland/record/Field.java index 107a754ca..8dccdf374 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/Field.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/Field.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java b/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java index 883466729..40a9f085c 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/FieldDictionary.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -33,6 +33,7 @@ public class FieldDictionary { public static String RECORD_POSITION = "record_position"; public static String RECORD_BODY = "record_body"; public static String RECORD_COUNT = "record_count"; + public static String RECORD_LAST_UPDATE_TIME = "record_last_update_time"; public static String RECORD_POSITION_LATITUDE = "record_position_latitude"; @@ -69,7 +70,9 @@ public static List asList() { RECORD_POSITION_SATELLITES, RECORD_POSITION_SPEED, RECORD_POSITION_STATUS, - RECORD_POSITION_TIMESTAMP + RECORD_POSITION_TIMESTAMP, + RECORD_COUNT, + RECORD_LAST_UPDATE_TIME ); } } diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/Position.java b/logisland-api/src/main/java/com/hurence/logisland/record/Position.java index 1c58af2d4..0979a9eaf 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/Position.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/Position.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java b/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java index 649659c2c..9920f6893 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java +++ b/logisland-api/src/main/java/com/hurence/logisland/record/StandardRecord.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientServiceException.java b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientServiceException.java index 3cfd11a16..5dc80934c 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientServiceException.java +++ b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/DatastoreClientServiceException.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecord.java b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecord.java index 75a2ed070..f6548c6ce 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecord.java +++ b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecord.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecordBuilder.java b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecordBuilder.java index 903bd09bc..110155ca1 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecordBuilder.java +++ b/logisland-api/src/main/java/com/hurence/logisland/service/datastore/MultiGetQueryRecordBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java b/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java index fe926c857..d6fa4354f 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/stream/StandardStreamContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java b/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java index 237bb6c66..f1e11ba3a 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java +++ b/logisland-api/src/main/java/com/hurence/logisland/stream/StreamContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/main/java/com/hurence/logisland/util/FormatUtils.java b/logisland-api/src/main/java/com/hurence/logisland/util/FormatUtils.java index 0380e9dc8..2cf6b8db2 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/util/FormatUtils.java +++ b/logisland-api/src/main/java/com/hurence/logisland/util/FormatUtils.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-api/src/main/java/com/hurence/logisland/util/Tuple.java b/logisland-api/src/main/java/com/hurence/logisland/util/Tuple.java index 271720567..52c64e77a 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/util/Tuple.java +++ b/logisland-api/src/main/java/com/hurence/logisland/util/Tuple.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-api/src/main/java/com/hurence/logisland/validator/StandardValidators.java b/logisland-api/src/main/java/com/hurence/logisland/validator/StandardValidators.java index ac97a7459..d02d10c09 100644 --- a/logisland-api/src/main/java/com/hurence/logisland/validator/StandardValidators.java +++ b/logisland-api/src/main/java/com/hurence/logisland/validator/StandardValidators.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-api/src/test/java/com/hurence/logisland/record/StandardRecordTest.java b/logisland-api/src/test/java/com/hurence/logisland/record/StandardRecordTest.java index 09258b370..57a8ef9fa 100755 --- a/logisland-api/src/test/java/com/hurence/logisland/record/StandardRecordTest.java +++ b/logisland-api/src/test/java/com/hurence/logisland/record/StandardRecordTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-assembly/pom.xml b/logisland-assembly/pom.xml index 7223f575d..5dcdfc360 100644 --- a/logisland-assembly/pom.xml +++ b/logisland-assembly/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 logisland-assembly pom diff --git a/logisland-connect/logisland-connect-spark/pom.xml b/logisland-connect/logisland-connect-spark/pom.xml index 7c5cb7456..73f07bec0 100644 --- a/logisland-connect/logisland-connect-spark/pom.xml +++ b/logisland-connect/logisland-connect-spark/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland-connect - 0.13.0 + 0.14.0 jar diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java index 72d692026..6c056624a 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/converter/LogIslandRecordConverter.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.converter; import com.hurence.logisland.record.*; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java index 482fc7513..3a2991ac9 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSource.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.source; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSourceProvider.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSourceProvider.java index 36b8e3417..441e4795e 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSourceProvider.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/KafkaConnectStreamSourceProvider.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.source; import com.hurence.logisland.stream.spark.StreamOptions; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java index 300fbbd65..4cbd7d620 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SharedSourceTaskContext.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.source; import org.apache.kafka.connect.source.SourceRecord; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java index aa49acc4d..d05e6961e 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/SourceThread.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.source; import org.apache.commons.lang3.RandomUtils; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java index 5f0dd6664..e4bb1b33c 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceConnector.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.source.timed; import org.apache.kafka.common.config.ConfigDef; diff --git a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java index 83384d230..66467bb6d 100644 --- a/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java +++ b/logisland-connect/logisland-connect-spark/src/main/java/com/hurence/logisland/connect/source/timed/ClockSourceTask.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.source.timed; import org.apache.kafka.connect.data.Schema; diff --git a/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/KafkaConnectStructuredProviderService.scala b/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/KafkaConnectStructuredProviderService.scala index bfeb7a4fb..75dc674a5 100644 --- a/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/KafkaConnectStructuredProviderService.scala +++ b/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/KafkaConnectStructuredProviderService.scala @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.stream.spark.provider import java.util diff --git a/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/package.scala b/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/package.scala index 0efa12304..155224937 100644 --- a/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/package.scala +++ b/logisland-connect/logisland-connect-spark/src/main/scala/com/hurence/logisland/stream/spark/provider/package.scala @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.stream.spark import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} diff --git a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/KafkaConnectTest.java b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/KafkaConnectTest.java index 19d5fbf4f..637deb5a9 100644 --- a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/KafkaConnectTest.java +++ b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/KafkaConnectTest.java @@ -1,18 +1,17 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.hurence.logisland.connect; diff --git a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java index d98a1811c..1618799b1 100644 --- a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java +++ b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/converter/LogIslandRecordConverterTest.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.converter; import com.hurence.logisland.record.Field; diff --git a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/fake/FakeConnector.java b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/fake/FakeConnector.java index eae6b060d..b3887c970 100644 --- a/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/fake/FakeConnector.java +++ b/logisland-connect/logisland-connect-spark/src/test/java/com/hurence/logisland/connect/fake/FakeConnector.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.fake; import org.apache.commons.lang3.RandomStringUtils; diff --git a/logisland-connect/logisland-connect-spark/src/test/resources/conf/kafka-connect-stream.yml b/logisland-connect/logisland-connect-spark/src/test/resources/conf/kafka-connect-stream.yml index f3df717de..f9b5cd493 100644 --- a/logisland-connect/logisland-connect-spark/src/test/resources/conf/kafka-connect-stream.yml +++ b/logisland-connect/logisland-connect-spark/src/test/resources/conf/kafka-connect-stream.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland future factory job engine: diff --git a/logisland-connect/logisland-connectors-bundle/pom.xml b/logisland-connect/logisland-connectors-bundle/pom.xml index 268f1be28..ce6e58770 100644 --- a/logisland-connect/logisland-connectors-bundle/pom.xml +++ b/logisland-connect/logisland-connectors-bundle/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland-connect - 0.13.0 + 0.14.0 jar diff --git a/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java b/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java index 7f861832b..8d6ebf772 100644 --- a/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java +++ b/logisland-connect/logisland-connectors-bundle/src/main/java/Dummy.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,6 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - public class Dummy { } diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml index a54829627..718a89c33 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland-connectors - 0.13.0 + 0.14.0 jar diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java index a5322bfe5..bd9977281 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/CommonUtils.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc; import com.hurence.opc.OpcData; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java index 28af3ca4d..b9dbe80e5 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/OpcRecordFields.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc; import com.hurence.logisland.record.FieldDictionary; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java index c64746122..ec30bcfbd 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/SmartOpcOperations.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc; import com.hurence.opc.*; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java index a0914c3fd..fd820cc00 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/TagInfo.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc; import com.hurence.opc.OpcTagInfo; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java index 0ddef4cbc..23c20a171 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnector.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc.da; import com.hurence.logisland.connect.opc.CommonUtils; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java index 58cbadc61..2b352e2c2 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/da/OpcDaSourceTask.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc.da; import com.hurence.logisland.connect.opc.CommonUtils; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java index 04c8e7088..d3f8f89b6 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceConnector.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc.ua; import com.hurence.logisland.connect.opc.CommonUtils; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java index efd8bd82b..568782503 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/main/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTask.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc.ua; import com.hurence.logisland.connect.opc.CommonUtils; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java index 2f8a72ae9..e4b535c49 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/da/OpcDaSourceConnectorTest.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc.da; import com.google.gson.Gson; diff --git a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java index 3b6dcc6b3..afab110bf 100644 --- a/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java +++ b/logisland-connect/logisland-connectors/logisland-connector-opc/src/test/java/com/hurence/logisland/connect/opc/ua/OpcUaSourceTaskTest.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.connect.opc.ua; import com.hurence.opc.auth.X509Credentials; diff --git a/logisland-connect/logisland-connectors/pom.xml b/logisland-connect/logisland-connectors/pom.xml index 1bcb8692f..22e1e5462 100644 --- a/logisland-connect/logisland-connectors/pom.xml +++ b/logisland-connect/logisland-connectors/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland-connect - 0.13.0 + 0.14.0 pom diff --git a/logisland-connect/pom.xml b/logisland-connect/pom.xml index c122ef751..29806c425 100644 --- a/logisland-connect/pom.xml +++ b/logisland-connect/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 pom diff --git a/logisland-docker/full-container/Dockerfile b/logisland-docker/full-container/Dockerfile index 42b0f9184..66b7a4a12 100644 --- a/logisland-docker/full-container/Dockerfile +++ b/logisland-docker/full-container/Dockerfile @@ -7,7 +7,7 @@ USER root COPY logisland-*.tar.gz /usr/local/ RUN cd /usr/local; \ tar -xzf logisland-*.tar.gz; \ - ln -s /usr/local/logisland-0.13.0 /usr/local/logisland; \ + ln -s /usr/local/logisland-0.14.0 /usr/local/logisland; \ mkdir /usr/local/logisland/log; \ rm -f /usr/local/*.gz ENV LOGISLAND_HOME /usr/local/logisland diff --git a/logisland-docker/full-container/README.rst b/logisland-docker/full-container/README.rst index 51dc8ffb2..0c8a5a076 100644 --- a/logisland-docker/full-container/README.rst +++ b/logisland-docker/full-container/README.rst @@ -7,7 +7,7 @@ Small standalone Hadoop distribution for development and testing purpose : - Elasticsearch 2.3.3 - Kibana 4.5.1 - Kafka 0.9.0.1 -- Logisland 0.13.0 +- Logisland 0.14.0 This repository contains a Docker file to build a Docker image with Apache Spark, HBase, Flume & Zeppelin. @@ -32,14 +32,14 @@ Building the image # build logisland mvn clean install - cp logisland-assembly/target/logisland-0.13.0-bin.tar.gz logisland-docker + cp logisland-assembly/target/logisland-0.14.0-bin.tar.gz logisland-docker The archive is generated under dist directory, you have to copy this file into your Dockerfile directory you can now issue .. code-block:: sh - docker build --rm -t hurence/logisland:0.13.0 . + docker build --rm -t hurence/logisland:0.14.0 . Running the image @@ -64,13 +64,13 @@ Running the image -p 4040-4060:4040-4060 \ --name logisland \ -h sandbox \ - hurence/logisland-hdp2.4:0.13.0 bash + hurence/logisland-hdp2.4:0.14.0 bash or .. code-block:: - docker run -d -h sandbox hurence/logisland-hdp2.4:0.13.0 -d + docker run -d -h sandbox hurence/logisland-hdp2.4:0.14.0 -d if you want to mount a directory from your host, add the following option : diff --git a/logisland-docker/pom.xml b/logisland-docker/pom.xml index bcd8042c0..577208dc9 100644 --- a/logisland-docker/pom.xml +++ b/logisland-docker/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 pom logisland-docker diff --git a/logisland-docker/src/it/resources/data/all-tutorials.yml b/logisland-docker/src/it/resources/data/all-tutorials.yml index cbedd52db..0e08afd88 100644 --- a/logisland-docker/src/it/resources/data/all-tutorials.yml +++ b/logisland-docker/src/it/resources/data/all-tutorials.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-documentation/changes.rst b/logisland-documentation/changes.rst index cdeae8308..a00cecf26 100644 --- a/logisland-documentation/changes.rst +++ b/logisland-documentation/changes.rst @@ -3,7 +3,7 @@ What's new in logisland ? -v0.13.0 +v0.14.0 ------- - add support for SOLR diff --git a/logisland-documentation/conf.py b/logisland-documentation/conf.py index 6b6a30609..601fe68c2 100644 --- a/logisland-documentation/conf.py +++ b/logisland-documentation/conf.py @@ -71,9 +71,9 @@ # built documents. # # The short X.Y version. -version = '0.13.0' +version = '0.14.0' # The full version, including alpha/beta/rc tags. -release = '0.13.0' +release = '0.14.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/logisland-documentation/developer.rst b/logisland-documentation/developer.rst index 3e6b9c2f0..081ffdf8f 100644 --- a/logisland-documentation/developer.rst +++ b/logisland-documentation/developer.rst @@ -204,7 +204,7 @@ to release artifacts (if you're allowed to), follow this guide `release to OSS S .. code-block:: sh - ./update-version.sh -o 0.13.0 -n 14.4 + ./update-version.sh -o 0.14.0 -n 14.4 mvn license:format mvn test mvn -DperformRelease=true clean deploy -Phdp2.5 @@ -222,7 +222,7 @@ Publish release assets to github please refer to `https://developer.github.com/v3/repos/releases `_ -curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.13.0/assets?name=logisland-0.13.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' +curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.14.0/assets?name=logisland-0.14.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' diff --git a/logisland-documentation/monitoring.rst b/logisland-documentation/monitoring.rst index 0927fe3a0..2816ef76c 100644 --- a/logisland-documentation/monitoring.rst +++ b/logisland-documentation/monitoring.rst @@ -63,8 +63,8 @@ Manual mode : # download the latest build of Node Exporter cd /opt - wget https://github.com/prometheus/node_exporter/releases/download/0.13.0/node_exporter-0.13.0.linux-amd64.tar.gz -O /tmp/node_exporter-0.13.0.linux-amd64.tar.gz - sudo tar -xvzf /tmp/node_exporter-0.13.0.linux-amd64.tar.gz + wget https://github.com/prometheus/node_exporter/releases/download/0.14.0/node_exporter-0.14.0.linux-amd64.tar.gz -O /tmp/node_exporter-0.14.0.linux-amd64.tar.gz + sudo tar -xvzf /tmp/node_exporter-0.14.0.linux-amd64.tar.gz # Create a soft link to the node_exporter binary in /usr/bin. sudo ln -s /opt/node_exporter /usr/bin diff --git a/logisland-documentation/overview-slides.md b/logisland-documentation/overview-slides.md index ed33b565a..c0b555c2a 100644 --- a/logisland-documentation/overview-slides.md +++ b/logisland-documentation/overview-slides.md @@ -369,7 +369,7 @@ you configure here your Spark job parameters Download the latest release from [github](https://github.com/Hurence/logisland/releases) - tar -xzf logisland-0.13.0-bin.tar.gz + tar -xzf logisland-0.14.0-bin.tar.gz Create a job configuration diff --git a/logisland-documentation/plugins_old.rst b/logisland-documentation/plugins_old.rst index 116df25e7..6f37643c6 100644 --- a/logisland-documentation/plugins_old.rst +++ b/logisland-documentation/plugins_old.rst @@ -60,7 +60,7 @@ Write your a custom LogParser for your super-plugin in ``/src/main/java/com/hure Our parser will analyze some Proxy Log String in the following form : - "Thu Jan 02 08:43:39 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.13.026/Images/RightJauge.gif 724 409 false false" + "Thu Jan 02 08:43:39 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.14.026/Images/RightJauge.gif 724 409 false false" .. code-block:: java diff --git a/logisland-documentation/pom.xml b/logisland-documentation/pom.xml index a8cf41745..a7e475ce7 100644 --- a/logisland-documentation/pom.xml +++ b/logisland-documentation/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 logisland-documentation diff --git a/logisland-documentation/release.rst b/logisland-documentation/release.rst new file mode 100644 index 000000000..305b05e81 --- /dev/null +++ b/logisland-documentation/release.rst @@ -0,0 +1,79 @@ +Releasing guide +=============== + +This guide will help you to perform the full release process for Logisland framework. + +1. + + + + + +Build the code and run the tests +-------------------------------- + + +The following commands must be run from the top-level directory. + +.. code-block:: sh + + mvn clean install + +If you wish to skip the unit tests you can do this by adding `-DskipTests` to the command line. + + +Release to maven repositories +----------------------------- +to release artifacts (if you're allowed to), follow this guide `release to OSS Sonatype with maven `_ + +.. code-block:: sh + + # update the version (you should run a dry run first) + ./update-version.sh -o 0.14.0 -n 0.14.0 -d + ./update-version.sh -o 0.14.0 -n 0.14.0 + mvn license:format + mvn clean install + mvn -DperformRelease=true clean deploy -Phdp2.5 + mvn versions:commit + + +follow the staging procedure in `oss.sonatype.org `_ or read `Sonatype book `_ + +go to `oss.sonatype.org `_ to release manually the artifact + + + +Publish release assets to github +-------------------------------- + +please refer to `https://developer.github.com/v3/repos/releases `_ + +curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.14.0/assets?name=logisland-0.14.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' + + + +Publish Docker image +-------------------- +Building the image + +.. code-block:: sh + + # build logisland + mvn clean install -DskipTests -Pdocker -Dhdp2.5 + + # verify image build + docker images + + +then login and push the latest image + +.. code-block:: sh + + docker login + docker push hurence/logisland + + +Publish artifact to github +-------------------------- + +Tag the release + upload latest tgz diff --git a/logisland-documentation/tutorials/prerequisites.rst b/logisland-documentation/tutorials/prerequisites.rst index 596e3d85e..666930553 100644 --- a/logisland-documentation/tutorials/prerequisites.rst +++ b/logisland-documentation/tutorials/prerequisites.rst @@ -54,10 +54,10 @@ From an edge node of your cluster : .. code-block:: sh cd /opt - sudo wget https://github.com/Hurence/logisland/releases/download/v0.13.0/logisland-0.13.0-bin-hdp2.5.tar.gz + sudo wget https://github.com/Hurence/logisland/releases/download/v0.14.0/logisland-0.14.0-bin-hdp2.5.tar.gz export SPARK_HOME=/opt/spark-2.1.0-bin-hadoop2.7/ export HADOOP_CONF_DIR=$SPARK_HOME/conf - sudo /opt/logisland-0.13.0/bin/logisland.sh --conf /home/hurence/tom/logisland-conf/v0.10.0/future-factory.yml + sudo /opt/logisland-0.14.0/bin/logisland.sh --conf /home/hurence/tom/logisland-conf/v0.10.0/future-factory.yml diff --git a/logisland-engines/logisland-spark_1_6-engine/pom.xml b/logisland-engines/logisland-spark_1_6-engine/pom.xml index 50b24b778..0013e7307 100644 --- a/logisland-engines/logisland-spark_1_6-engine/pom.xml +++ b/logisland-engines/logisland-spark_1_6-engine/pom.xml @@ -23,7 +23,7 @@ http://www.w3.org/2001/XMLSchema-instance "> com.hurence.logisland logisland-engines - 0.13.0 + 0.14.0 logisland-spark_1_6-engine_${scala.binary.version} jar diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java b/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java index 14b2447b4..ebace0485 100644 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java +++ b/logisland-engines/logisland-spark_1_6-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala index 301262f46..accb2f1c6 100644 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala +++ b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.util.kafka import java.util.Properties diff --git a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala index 8e2b30126..3c4453c04 100644 --- a/logisland-engines/logisland-spark_1_6-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala +++ b/logisland-engines/logisland-spark_1_6-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.spark.metrics.sink import java.util.concurrent.TimeUnit diff --git a/logisland-engines/logisland-spark_2_1-engine/pom.xml b/logisland-engines/logisland-spark_2_1-engine/pom.xml index c1ae2761e..68e1321a7 100644 --- a/logisland-engines/logisland-spark_2_1-engine/pom.xml +++ b/logisland-engines/logisland-spark_2_1-engine/pom.xml @@ -23,7 +23,7 @@ http://www.w3.org/2001/XMLSchema-instance "> com.hurence.logisland logisland-engines - 0.13.0 + 0.14.0 logisland-spark_2_1-engine_${scala.binary.version} jar diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java index d9dc0541a..da7c39477 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/PipelineConfigurationBroadcastWrapper.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote; import com.hurence.logisland.engine.EngineContext; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java index 63fdcd1c9..49e3ca5e1 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiClient.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote; import com.fasterxml.jackson.databind.DeserializationFeature; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java index c40e52b65..315fcac57 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/RemoteApiComponentFactory.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote; import com.hurence.logisland.component.ConfigurableComponent; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java index 323911106..50ac7ad5a 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Component.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java index 2f0179f6f..d10fc7a76 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/DataFlow.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java index 84ad7b704..2d08c33fb 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Pipeline.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java index a4452eaf3..1350a44c5 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Processor.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import io.swagger.annotations.ApiModel; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java index 12c88fb30..739cc1707 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Property.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java index f98169d97..a4834f747 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Service.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import io.swagger.annotations.ApiModel; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java index c67ea3a77..8dc999e90 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Stream.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java index 67b9f24c8..fbd57fc65 100755 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/engine/spark/remote/model/Versioned.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.model; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java index 6bda08b77..cfb5cddad 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProcessorMetrics.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProtoBufRegistrator.java b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProtoBufRegistrator.java index 5ce6b5d25..62e0159a2 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProtoBufRegistrator.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/java/com/hurence/logisland/util/spark/ProtoBufRegistrator.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala index 20783244c..4aa469903 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/KafkaStreamProcessingEngine.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /** * Copyright (C) 2016 Hurence (support@hurence.com) * diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala index 1ca0034fc..ccfa009e3 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/engine/spark/RemoteApiStreamProcessingEngine.scala @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark import java.time.Duration diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala index 9c7ba28f4..b88518f5d 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/AbstractKafkaRecordStream.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /** * Copyright (C) 2016 Hurence (support@hurence.com) * diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala index 3ec1be546..5f0739a5b 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/DummyRecordStream.scala @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.stream.spark import java.util diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamParallelProcessing.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamParallelProcessing.scala index bece23819..c62612bc9 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamParallelProcessing.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/KafkaRecordStreamParallelProcessing.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /** * Copyright (C) 2016 Hurence (support@hurence.com) * diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala index 8cebb3aee..fcb47d96a 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/package.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream import com.hurence.logisland.component.{AllowableValue, PropertyDescriptor} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/Sandbox.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/Sandbox.scala index 4e587bd94..bce24a7a9 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/Sandbox.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/Sandbox.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream.spark.structured /* diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala index a4344cdb4..782be8cb5 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/StructuredStream.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ /** * Copyright (C) 2016 Hurence (support@hurence.com) * diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/HDFSBurner.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/HDFSBurner.scala index 3d5476527..ab64633ec 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/HDFSBurner.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/HDFSBurner.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream.spark.structured.handler /* diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/PipelineProcessor.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/PipelineProcessor.scala index a3c46bd73..3c0b3afa5 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/PipelineProcessor.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/PipelineProcessor.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream.spark.structured.handler import java.io.ByteArrayInputStream diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala index 85aa52320..cd516997b 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/SQLAggregator.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream.spark.structured.handler import java.util diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/StructuredStreamHandler.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/StructuredStreamHandler.scala index d6504a37b..927df1040 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/StructuredStreamHandler.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/handler/StructuredStreamHandler.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream.spark.structured.handler import java.util diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala index 14a8a2bce..e0c571bbc 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/ConsoleStructuredStreamProviderService.scala @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala index 26807feb0..cf98d29fa 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStreamWriter.scala @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.stream.spark.structured.provider import com.hurence.logisland.util.kafka.KafkaSink diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala index 02d44fcb1..a2e1b3a12 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/KafkaStructuredStreamProviderService.scala @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.hurence.logisland.stream.spark.structured.provider import java.io.{File, IOException} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/MQTTStructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/MQTTStructuredStreamProviderService.scala index e5a6586ad..5a6686d00 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/MQTTStructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/MQTTStructuredStreamProviderService.scala @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala index a79f90ff1..b8b377fec 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/stream/spark/structured/provider/StructuredStreamProviderService.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.stream.spark.structured.provider import java.io.{ByteArrayInputStream, ByteArrayOutputStream} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/ConsumerGroupUtils.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/ConsumerGroupUtils.scala index b20f732d4..9598a0388 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/ConsumerGroupUtils.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/ConsumerGroupUtils.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.util.kafka diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala index 301262f46..accb2f1c6 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/kafka/KafkaReporter.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.util.kafka import java.util.Properties diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MQTTStreamSource.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MQTTStreamSource.scala index e0ed00c01..f5c390822 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MQTTStreamSource.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MQTTStreamSource.scala @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.hurence.logisland.util.mqtt import java.nio.charset.Charset diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MessageStore.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MessageStore.scala index 6009a9c75..6228e2901 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MessageStore.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/com/hurence/logisland/util/mqtt/MessageStore.scala @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +14,6 @@ * limitations under the License. */ - package com.hurence.logisland.util.mqtt import java.nio.ByteBuffer diff --git a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala index 8755b1f2a..ea0adbd78 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala +++ b/logisland-engines/logisland-spark_2_1-engine/src/main/scala/org/apache/spark/metrics/sink/KafkaSink.scala @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.apache.spark.metrics.sink import java.util.{Locale, Properties} diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java index df3c2f28b..84a7a80f9 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/AbstractStreamProcessingIntegrationTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java index 12de07af4..711ae9a46 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/RemoteApiEngineTest.java @@ -1,18 +1,17 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.hurence.logisland.engine; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java index 3db9f4619..0f1c820aa 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/RemoteApiClientTest.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote; import okhttp3.Credentials; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java index bb8d00e43..5d125dc7b 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockProcessor.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.mock; import com.hurence.logisland.component.PropertyDescriptor; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java index c91387941..17931be0e 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockServiceController.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.mock; import com.hurence.logisland.component.PropertyDescriptor; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java index e4f020c93..cff86c2bc 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/java/com/hurence/logisland/engine/spark/remote/mock/MockStream.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.engine.spark.remote.mock; import com.hurence.logisland.component.PropertyDescriptor; diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml index 1ececfa04..724cf04d1 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/remote-engine.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland remote controlled. engine: diff --git a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/structured-stream.yml b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/structured-stream.yml index cb502d7e0..1fd391964 100644 --- a/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/structured-stream.yml +++ b/logisland-engines/logisland-spark_2_1-engine/src/test/resources/conf/structured-stream.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland future factory job engine: diff --git a/logisland-engines/pom.xml b/logisland-engines/pom.xml index 1ed791fb0..7e03eba05 100644 --- a/logisland-engines/pom.xml +++ b/logisland-engines/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 pom diff --git a/logisland-framework/logisland-bootstrap/pom.xml b/logisland-framework/logisland-bootstrap/pom.xml index ac1507655..03449a95f 100644 --- a/logisland-framework/logisland-bootstrap/pom.xml +++ b/logisland-framework/logisland-bootstrap/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-framework - 0.13.0 + 0.14.0 logisland-bootstrap jar diff --git a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java index bcde8c74a..cc8c8fb33 100644 --- a/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java +++ b/logisland-framework/logisland-bootstrap/src/main/java/com/hurence/logisland/runner/StreamProcessingRunner.java @@ -65,7 +65,7 @@ public static void main(String[] args) { "██║ ██║ ██║██║ ███╗ ██║███████╗██║ ███████║██╔██╗ ██║██║ ██║\n" + "██║ ██║ ██║██║ ██║ ██║╚════██║██║ ██╔══██║██║╚██╗██║██║ ██║\n" + "███████╗╚██████╔╝╚██████╔╝ ██║███████║███████╗██║ ██║██║ ╚████║██████╔╝\n" + - "╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ v0.13.0\n\n\n"; + "╚══════╝ ╚═════╝ ╚═════╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ v0.14.0\n\n\n"; System.out.println(logisland); Optional engineInstance = Optional.empty(); diff --git a/logisland-framework/logisland-hadoop-utils/pom.xml b/logisland-framework/logisland-hadoop-utils/pom.xml index 30201ede1..d63cfc761 100644 --- a/logisland-framework/logisland-hadoop-utils/pom.xml +++ b/logisland-framework/logisland-hadoop-utils/pom.xml @@ -19,7 +19,7 @@ com.hurence.logisland logisland-framework - 0.13.0 + 0.14.0 logisland-hadoop-utils jar diff --git a/logisland-framework/logisland-resources/pom.xml b/logisland-framework/logisland-resources/pom.xml index 724663aa0..6f9bd13c9 100644 --- a/logisland-framework/logisland-resources/pom.xml +++ b/logisland-framework/logisland-resources/pom.xml @@ -21,7 +21,7 @@ com.hurence.logisland logisland-framework - 0.13.0 + 0.14.0 logisland-resources pom diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/aggregate-events.yml b/logisland-framework/logisland-resources/src/main/resources/conf/aggregate-events.yml index 3151dc58c..d0ee031f2 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/aggregate-events.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/aggregate-events.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/configuration-template.yml b/logisland-framework/logisland-resources/src/main/resources/conf/configuration-template.yml index 0c7b0b733..825b647cc 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/configuration-template.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/configuration-template.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-1.log b/logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-1.log new file mode 100644 index 000000000..cadabea2a --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-1.log @@ -0,0 +1,6 @@ +bradfute.vip.best.com - - [01/Jul/1995:01:28:57 -0400] "GET /htbin/cdt_main.pl HTTP/1.0" 200 3214 +ix-mhl-ca1-18.ix.netcom.com - - [01/Jul/1995:01:29:00 -0400] "GET /shuttle/missions/sts-71/images/images.html HTTP/1.0" 200 7634 +winnie.freenet.mb.ca - - [01/Jul/1995:01:29:09 -0400] "GET /htbin/wais.pl HTTP/1.0" 200 308 +www-d4.proxy.aol.com - - [01/Jul/1995:01:29:11 -0400] "GET /shuttle/missions/sts-26/mission-sts-26.html HTTP/1.0" 200 7116 +logisland.hurence.com - - [01/Jul/1995:01:29:12 -0400] "GET /shuttle/missions/missions.html HTTP/1.0" 200 8677 + diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml b/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml index 5dee26387..be5c8c35d 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/docker-compose.yml @@ -55,7 +55,7 @@ services: # Logisland container : does nothing but launching logisland: container_name: logisland - image: hurence/logisland:0.13.0 + image: hurence/logisland:0.14.0 command: tail -f bin/logisland.sh #command: bin/logisland.sh --conf /conf/index-apache-logs.yml links: @@ -76,4 +76,4 @@ services: container_name: redis image: 'redis:latest' ports: - - '6379:6379' \ No newline at end of file + - '6379:6379' diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/enrich-apache-logs.yml b/logisland-framework/logisland-resources/src/main/resources/conf/enrich-apache-logs.yml index 2709fd956..c036435a0 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/enrich-apache-logs.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/enrich-apache-logs.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/future-factory-indexer.yml b/logisland-framework/logisland-resources/src/main/resources/conf/future-factory-indexer.yml index 3eed1698f..8dc6dd109 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/future-factory-indexer.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/future-factory-indexer.yml @@ -2,7 +2,7 @@ # Logisland configuration for future factory project ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland future factory job ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs-solr.yml b/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs-solr.yml index f167b2d03..00f2f4402 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs-solr.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs-solr.yml @@ -2,7 +2,7 @@ # Logisland configuration script tempate ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs.yml b/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs.yml index a156aa9b0..2eda8216e 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/index-apache-logs.yml @@ -2,7 +2,7 @@ # Logisland configuration script tempate ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/index-blockchain-transactions.yml b/logisland-framework/logisland-resources/src/main/resources/conf/index-blockchain-transactions.yml index 97d01bcde..3273cf74c 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/index-blockchain-transactions.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/index-blockchain-transactions.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland future factory job engine: diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/index-network-packets.yml b/logisland-framework/logisland-resources/src/main/resources/conf/index-network-packets.yml index 023f86fb0..acaff0908 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/index-network-packets.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/index-network-packets.yml @@ -2,7 +2,7 @@ # Logisland configuration script example: parse network packets and display them in Kibana ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/logisland-kafka-connect.yml b/logisland-framework/logisland-resources/src/main/resources/conf/logisland-kafka-connect.yml index ede884068..6791b888e 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/logisland-kafka-connect.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/logisland-kafka-connect.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland Kafka Connect Integration engine: diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/match-queries.yml b/logisland-framework/logisland-resources/src/main/resources/conf/match-queries.yml index 8b023410b..bf2e90ab4 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/match-queries.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/match-queries.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/mqtt-to-historian.yml b/logisland-framework/logisland-resources/src/main/resources/conf/mqtt-to-historian.yml index eafcf87c5..9cf0f06dd 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/mqtt-to-historian.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/mqtt-to-historian.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland future factory job engine: diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml b/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml index 9a48cfe0e..d65f2d3ea 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/opc-iiot.yml @@ -1,4 +1,4 @@ -version: 0.13.0 +version: 0.14.0 documentation: LogIsland IIoT OPC-UA Job engine: diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/outlier-detection.yml b/logisland-framework/logisland-resources/src/main/resources/conf/outlier-detection.yml index 372d27c70..66be07df4 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/outlier-detection.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/outlier-detection.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/python-processing.yml b/logisland-framework/logisland-resources/src/main/resources/conf/python-processing.yml index d433a75e6..ad69dad0d 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/python-processing.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/python-processing.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/retrieve-data-from-elasticsearch.yml b/logisland-framework/logisland-resources/src/main/resources/conf/retrieve-data-from-elasticsearch.yml index a4cde65a6..bae55c379 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/retrieve-data-from-elasticsearch.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/retrieve-data-from-elasticsearch.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/save-to-hdfs.yml b/logisland-framework/logisland-resources/src/main/resources/conf/save-to-hdfs.yml index 9060002c3..4390b50c9 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/save-to-hdfs.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/save-to-hdfs.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/send-apache-logs-to-hbase.yml b/logisland-framework/logisland-resources/src/main/resources/conf/send-apache-logs-to-hbase.yml index 9a8252375..0db5945db 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/send-apache-logs-to-hbase.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/send-apache-logs-to-hbase.yml @@ -2,7 +2,7 @@ # Logisland configuration script template ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: This tutorial job sends apache logs to an HBase table ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml b/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml index f9c63f458..8e16c459b 100644 --- a/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/store-to-redis.yml @@ -2,7 +2,7 @@ # Logisland configuration script tempate ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/timeseries-parsing.yml b/logisland-framework/logisland-resources/src/main/resources/conf/timeseries-parsing.yml index 5b5e11dd7..fa2174e82 100755 --- a/logisland-framework/logisland-resources/src/main/resources/conf/timeseries-parsing.yml +++ b/logisland-framework/logisland-resources/src/main/resources/conf/timeseries-parsing.yml @@ -55,7 +55,7 @@ engine: documentation: "store an in-memory cache coming from CSV" configuration: csv.format: excel_fr - csv.file.path: "logisland-assembly/target/logisland-0.13.0-bin-hdp2.5/logisland-0.13.0/conf/timeseries-lookup.csv" + csv.file.path: "logisland-assembly/target/logisland-0.14.0-bin-hdp2.5/logisland-0.14.0/conf/timeseries-lookup.csv" first.line.header: true row.key: tagname encoding.charset: ISO-8859-1 diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/changes.rst b/logisland-framework/logisland-resources/src/main/resources/docs/changes.rst index cdeae8308..a00cecf26 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/changes.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/changes.rst @@ -3,7 +3,7 @@ What's new in logisland ? -v0.13.0 +v0.14.0 ------- - add support for SOLR diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst b/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst index 3e6b9c2f0..081ffdf8f 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/developer.rst @@ -204,7 +204,7 @@ to release artifacts (if you're allowed to), follow this guide `release to OSS S .. code-block:: sh - ./update-version.sh -o 0.13.0 -n 14.4 + ./update-version.sh -o 0.14.0 -n 14.4 mvn license:format mvn test mvn -DperformRelease=true clean deploy -Phdp2.5 @@ -222,7 +222,7 @@ Publish release assets to github please refer to `https://developer.github.com/v3/repos/releases `_ -curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.13.0/assets?name=logisland-0.13.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' +curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.14.0/assets?name=logisland-0.14.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/monitoring.rst b/logisland-framework/logisland-resources/src/main/resources/docs/monitoring.rst index 0927fe3a0..2816ef76c 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/monitoring.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/monitoring.rst @@ -63,8 +63,8 @@ Manual mode : # download the latest build of Node Exporter cd /opt - wget https://github.com/prometheus/node_exporter/releases/download/0.13.0/node_exporter-0.13.0.linux-amd64.tar.gz -O /tmp/node_exporter-0.13.0.linux-amd64.tar.gz - sudo tar -xvzf /tmp/node_exporter-0.13.0.linux-amd64.tar.gz + wget https://github.com/prometheus/node_exporter/releases/download/0.14.0/node_exporter-0.14.0.linux-amd64.tar.gz -O /tmp/node_exporter-0.14.0.linux-amd64.tar.gz + sudo tar -xvzf /tmp/node_exporter-0.14.0.linux-amd64.tar.gz # Create a soft link to the node_exporter binary in /usr/bin. sudo ln -s /opt/node_exporter /usr/bin diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/plugins_old.rst b/logisland-framework/logisland-resources/src/main/resources/docs/plugins_old.rst index 116df25e7..6f37643c6 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/plugins_old.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/plugins_old.rst @@ -60,7 +60,7 @@ Write your a custom LogParser for your super-plugin in ``/src/main/java/com/hure Our parser will analyze some Proxy Log String in the following form : - "Thu Jan 02 08:43:39 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.13.026/Images/RightJauge.gif 724 409 false false" + "Thu Jan 02 08:43:39 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.14.026/Images/RightJauge.gif 724 409 false false" .. code-block:: java diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst index a9f443d19..e13f859b1 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst @@ -36,4 +36,5 @@ Contents: index-blockchain-transactions index-excel-spreadsheets mqtt-to-historian + iiot-opc-ua integrate-kafka-connect diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst index 596e3d85e..666930553 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/prerequisites.rst @@ -54,10 +54,10 @@ From an edge node of your cluster : .. code-block:: sh cd /opt - sudo wget https://github.com/Hurence/logisland/releases/download/v0.13.0/logisland-0.13.0-bin-hdp2.5.tar.gz + sudo wget https://github.com/Hurence/logisland/releases/download/v0.14.0/logisland-0.14.0-bin-hdp2.5.tar.gz export SPARK_HOME=/opt/spark-2.1.0-bin-hadoop2.7/ export HADOOP_CONF_DIR=$SPARK_HOME/conf - sudo /opt/logisland-0.13.0/bin/logisland.sh --conf /home/hurence/tom/logisland-conf/v0.10.0/future-factory.yml + sudo /opt/logisland-0.14.0/bin/logisland.sh --conf /home/hurence/tom/logisland-conf/v0.10.0/future-factory.yml diff --git a/logisland-framework/logisland-scripting/logisland-scripting-base/pom.xml b/logisland-framework/logisland-scripting/logisland-scripting-base/pom.xml index becdef7ee..2c0f56e91 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-base/pom.xml +++ b/logisland-framework/logisland-scripting/logisland-scripting-base/pom.xml @@ -4,7 +4,7 @@ com.hurence.logisland logisland-scripting - 0.13.0 + 0.14.0 logisland-scripting-base diff --git a/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BaseScriptEngine.java b/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BaseScriptEngine.java index 12e634bbe..3f533781b 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BaseScriptEngine.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BaseScriptEngine.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BindingsImpl.java b/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BindingsImpl.java index 7c37856a0..6c5ce320a 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BindingsImpl.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/BindingsImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/ScriptContextImpl.java b/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/ScriptContextImpl.java index 0888202e1..da015dce2 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/ScriptContextImpl.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-base/src/main/java/com/hurence/logisland/jsr223/ScriptContextImpl.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/logisland-scripting-mvel/pom.xml b/logisland-framework/logisland-scripting/logisland-scripting-mvel/pom.xml index 3f62c6f25..06e3c534d 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-mvel/pom.xml +++ b/logisland-framework/logisland-scripting/logisland-scripting-mvel/pom.xml @@ -4,7 +4,7 @@ com.hurence.logisland logisland-scripting - 0.13.0 + 0.14.0 logisland-scripting-mvel diff --git a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelCompiledScript.java b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelCompiledScript.java index 5b5837b6b..c06932984 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelCompiledScript.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelCompiledScript.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngine.java b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngine.java index 0f0386940..1eca23c14 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngine.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngine.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngineFactory.java b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngineFactory.java index ff7a96c91..a9b6f68a1 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngineFactory.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/main/java/com/hurence/logisland/jsr223/mvel/MvelScriptEngineFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/test/java/com/hurence/logisland/jsr223/mvel/TestMvelEngine.java b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/test/java/com/hurence/logisland/jsr223/mvel/TestMvelEngine.java index aed954bb5..f6c0c0b42 100644 --- a/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/test/java/com/hurence/logisland/jsr223/mvel/TestMvelEngine.java +++ b/logisland-framework/logisland-scripting/logisland-scripting-mvel/src/test/java/com/hurence/logisland/jsr223/mvel/TestMvelEngine.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-framework/logisland-scripting/pom.xml b/logisland-framework/logisland-scripting/pom.xml index 9b329529a..559bb4dc5 100644 --- a/logisland-framework/logisland-scripting/pom.xml +++ b/logisland-framework/logisland-scripting/pom.xml @@ -21,7 +21,7 @@ com.hurence.logisland logisland-framework - 0.13.0 + 0.14.0 logisland-scripting diff --git a/logisland-framework/logisland-utils/pom.xml b/logisland-framework/logisland-utils/pom.xml index a02067b81..0392070aa 100644 --- a/logisland-framework/logisland-utils/pom.xml +++ b/logisland-framework/logisland-utils/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-framework - 0.13.0 + 0.14.0 logisland-utils jar diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java index 6d204b5ae..476a5bb85 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/component/ComponentFactory.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java index 56ba9f6ca..152b154af 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/config/ConfigReader.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/controller/StandardControllerServiceLookup.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/controller/StandardControllerServiceLookup.java index 3b3c7fc99..78285971f 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/controller/StandardControllerServiceLookup.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/controller/StandardControllerServiceLookup.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/metrics/Names.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/metrics/Names.java index 24cb8ccf2..af2b96e97 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/metrics/Names.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/metrics/Names.java @@ -1,20 +1,18 @@ -/* - * * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.metrics; public interface Names { diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java index 412f776af..7c2ee961f 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/JsonSerializer.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/KuraProtobufSerializer.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/KuraProtobufSerializer.java index 361d43bee..448681c7a 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/KuraProtobufSerializer.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/KuraProtobufSerializer.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/StringSerializer.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/StringSerializer.java index bee772dc4..650d8b994 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/StringSerializer.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/serializer/StringSerializer.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/GZipUtil.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/GZipUtil.java index 736ee4139..81b0d7713 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/GZipUtil.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/GZipUtil.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java index 65c7f8ec0..c29782722 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/ListUtils.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayload.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayload.java index a52dab71f..eedc64ae4 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayload.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayload.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadDecoder.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadDecoder.java index e60136cda..db5421422 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadDecoder.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadDecoder.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadEncoder.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadEncoder.java index 85e5c151e..39f8725c2 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadEncoder.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPayloadEncoder.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPosition.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPosition.java index 2cdc87c2d..f4798d210 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPosition.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/KuraPosition.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metric.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metric.java index d203053da..7c482b344 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metric.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metric.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metrics.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metrics.java index 0eeb2a5eb..0cd08728a 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metrics.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Metrics.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Optional.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Optional.java index ad0d4f64f..35bc7db8b 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Optional.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Optional.java @@ -1,14 +1,18 @@ -/******************************************************************************* - * Copyright (c) 2017 Red Hat Inc and others +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * which accompanies this distribution, and is available at - * http://www.eclipse.org/legal/epl-v10.html + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * Contributors: - * Red Hat Inc - initial API and implementation - *******************************************************************************/ + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.util.kura; import java.lang.annotation.Retention; diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Payload.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Payload.java index b42b55344..efd693b03 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Payload.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/kura/Payload.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockControllerServiceInitializationContext.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockControllerServiceInitializationContext.java index 994ff92af..bf8a1d11b 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockControllerServiceInitializationContext.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockControllerServiceInitializationContext.java @@ -1,19 +1,18 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ - package com.hurence.logisland.util.runner; diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java index d7ea87437..8a7372e6f 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/runner/MockProcessContext.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/stream/io/StreamUtils.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/stream/io/StreamUtils.java index 75c1f0968..1690ddc8d 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/stream/io/StreamUtils.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/stream/io/StreamUtils.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java index d94a88ba0..3c9f50987 100644 --- a/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java +++ b/logisland-framework/logisland-utils/src/main/java/com/hurence/logisland/util/string/StringUtils.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java index f0fb9d188..9de933afe 100755 --- a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java +++ b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/JsonSerializerTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/KuraProtobufSerializerTest.java b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/KuraProtobufSerializerTest.java index 5a5d44fba..73c833449 100755 --- a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/KuraProtobufSerializerTest.java +++ b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/serializer/KuraProtobufSerializerTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/kura/KuraPayloadDecoderTest.java b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/kura/KuraPayloadDecoderTest.java index 276958997..a22f89d2f 100644 --- a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/kura/KuraPayloadDecoderTest.java +++ b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/kura/KuraPayloadDecoderTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java index bf8cff570..8477b2e92 100644 --- a/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java +++ b/logisland-framework/logisland-utils/src/test/java/com/hurence/logisland/util/string/StringUtilsTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-framework/logisland-utils/src/test/resources/configuration-templatev2.yml b/logisland-framework/logisland-utils/src/test/resources/configuration-templatev2.yml index 4e63607c3..fe5ec65d6 100644 --- a/logisland-framework/logisland-utils/src/test/resources/configuration-templatev2.yml +++ b/logisland-framework/logisland-utils/src/test/resources/configuration-templatev2.yml @@ -2,7 +2,7 @@ # Logisland configuration script tempate ######################################################################################################### -version: 0.13.0 +version: 0.14.0 documentation: LogIsland analytics main config file. Put here every engine or component config ######################################################################################################### diff --git a/logisland-framework/pom.xml b/logisland-framework/pom.xml index becd2f75a..769d591f3 100644 --- a/logisland-framework/pom.xml +++ b/logisland-framework/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 logisland-framework diff --git a/logisland-plugins/logisland-botsearch-plugin/pom.xml b/logisland-plugins/logisland-botsearch-plugin/pom.xml index cb3c255bc..254eecee7 100644 --- a/logisland-plugins/logisland-botsearch-plugin/pom.xml +++ b/logisland-plugins/logisland-botsearch-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-botsearch-plugin diff --git a/logisland-plugins/logisland-botsearch-plugin/src/main/java/com/hurence/logisland/math/FlowDistanceMeasure.java b/logisland-plugins/logisland-botsearch-plugin/src/main/java/com/hurence/logisland/math/FlowDistanceMeasure.java index 721b470e8..deca97eb5 100755 --- a/logisland-plugins/logisland-botsearch-plugin/src/main/java/com/hurence/logisland/math/FlowDistanceMeasure.java +++ b/logisland-plugins/logisland-botsearch-plugin/src/main/java/com/hurence/logisland/math/FlowDistanceMeasure.java @@ -116,7 +116,7 @@ private static double dn(HttpFlow e1, HttpFlow e2) { * * We define dv(rk, rh) to be equal to the normalized Levenshtein distance * between strings obtained by concatenating the parameter values (e.g., - * 0.13.0US). + * 0.14.0US). */ private static double dv(HttpFlow e1, HttpFlow e2) { List v1 = e1.getUrlQueryValues(); diff --git a/logisland-plugins/logisland-botsearch-plugin/src/test/java/com/hurence/logisland/botsearch/TraceTest.java b/logisland-plugins/logisland-botsearch-plugin/src/test/java/com/hurence/logisland/botsearch/TraceTest.java index f484be549..ff288a241 100755 --- a/logisland-plugins/logisland-botsearch-plugin/src/test/java/com/hurence/logisland/botsearch/TraceTest.java +++ b/logisland-plugins/logisland-botsearch-plugin/src/test/java/com/hurence/logisland/botsearch/TraceTest.java @@ -37,12 +37,12 @@ private static Trace getSampleTrace() { String[] flows - = {"Thu Jan 02 08:43:39 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.13.026/Images/RightJauge.gif 724 409 false false", - "Thu Jan 02 08:43:40 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.13.026/Images/fondJauge.gif 723 402 false false", + = {"Thu Jan 02 08:43:39 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.14.026/Images/RightJauge.gif 724 409 false false", + "Thu Jan 02 08:43:40 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.14.026/Images/fondJauge.gif 723 402 false false", "Thu Jan 02 08:43:42 CET 2014 GET 10.118.32.164 193.252.23.209 http static1.lecloud.wanadoo.fr 80 /home/fr_FR/20131202100641/img/sprite-icons.pn 495 92518 false false", "Thu Jan 02 08:43:43 CET 2014 GET 10.118.32.164 173.194.66.94 https www.google.fr 443 /complete/search 736 812 false false", - "Thu Jan 02 08:43:45 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.13.026/Images/digiposte/archiver-btn.png 736 2179 false false", - "Thu Jan 02 08:43:49 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.13.026/Images/picto_trash.gif 725 544 false false"}; + "Thu Jan 02 08:43:45 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.14.026/Images/digiposte/archiver-btn.png 736 2179 false false", + "Thu Jan 02 08:43:49 CET 2014 GET 10.118.32.164 193.251.214.117 http webmail.laposte.net 80 /webmail/fr_FR/Images/Images-2013090.14.026/Images/picto_trash.gif 725 544 false false"}; for (String flowString : flows) { String[] split = flowString.split("\t"); diff --git a/logisland-plugins/logisland-botsearch-plugin/src/test/resources/data/TracesAnalysis_samples.txt b/logisland-plugins/logisland-botsearch-plugin/src/test/resources/data/TracesAnalysis_samples.txt index e56790b5f..265b216b1 100755 --- a/logisland-plugins/logisland-botsearch-plugin/src/test/resources/data/TracesAnalysis_samples.txt +++ b/logisland-plugins/logisland-botsearch-plugin/src/test/resources/data/TracesAnalysis_samples.txt @@ -75,8 +75,8 @@ 2012-10-19T10:12:00.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 1942 MICROSOFT BITS/6.7 false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:12:06.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 472 MICROSOFT BITS/6.7 false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:12:19.000 GMT 10.112.123.187 CONNECT TCP_MISS_SSL/200 tunnel dl.google.com / 443 - 0 39 MICROSOFT BITS/6.7 false false false false false null 173.194.34.224 9q9hyebw8m76 37.41920471191406 -122.05740356445312 United States 43 null -2012-10-19T10.13.00.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 1942 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null -2012-10-19T10.13.05.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 472 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null +2012-10-19T10.14.00.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 1942 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null +2012-10-19T10.14.05.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 472 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:12:37.000 GMT 10.112.123.187 CONNECT TCP_MISS_SSL/200 tunnel dl.google.com / 443 - 0 39 - false false false false false null 173.194.34.224 9q9hyebw8m76 37.41920471191406 -122.05740356445312 United States 43 null 2012-10-19T10:17:45.000 GMT 10.112.123.187 POST TCP_DENIED/407 http tools.google.com /service/update2 80 - 0 1942 GOOGLE UPDATE/1.3.21.99,WINHTTP false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:17:51.000 GMT 10.112.123.187 POST TCP_DENIED/407 http tools.google.com /service/update2 80 - 0 472 GOOGLE UPDATE/1.3.21.99,WINHTTP false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null diff --git a/logisland-plugins/logisland-botsearch-plugin/src/test/resources/enriched_sample_values.txt b/logisland-plugins/logisland-botsearch-plugin/src/test/resources/enriched_sample_values.txt index 978147262..b45c6c710 100755 --- a/logisland-plugins/logisland-botsearch-plugin/src/test/resources/enriched_sample_values.txt +++ b/logisland-plugins/logisland-botsearch-plugin/src/test/resources/enriched_sample_values.txt @@ -23,9 +23,9 @@ 2012-10-19T16:12:53.000 UTC 10.112.122.22 POST TCP_CLIENT_REFRESH_MISS/200 http 195.145.147.146 /idle/OaOmdz02zSLnCW6Z/34 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 195.145.147.146 u1n6hu38e1n6 51.0 9.0 Germany 43 2012-10-19T16:12:53.000 UTC 10.112.122.22 POST TCP_CLIENT_REFRESH_MISS/200 http 195.145.147.146 /idle/ObOmdz02zSLkCW6Z/34 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 195.145.147.146 u1n6hu38e1n6 51.0 9.0 Germany 43 2012-10-19T16:12:53.000 UTC 10.112.122.22 POST TCP_CLIENT_REFRESH_MISS/200 http 195.145.147.146 /idle/ObOmdz02zSLkCW6Z/34 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 195.145.147.146 u1n6hu38e1n6 51.0 9.0 Germany 43 -2012-10-19T16:12:51.000 UTC 10.112.31.174 POST TCP_CLIENT_REFRESH_MISS/200 http 95.140.225.151 /idle/Cf7maD8spa7BaStZ/1030 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 95.140.225.151 gcpuvp334xv6 51.5 -0.1300048828125 United Kingdom 43 +2012-10-19T16:12:51.000 UTC 10.112.31.174 POST TCP_CLIENT_REFRESH_MISS/200 http 95.140.225.151 /idle/Cf7maD8spa7BaStZ/1030 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 95.140.225.151 gcpuvp334xv6 51.5 -0.14.0048828125 United Kingdom 43 2012-10-19T16:12:51.500 UTC 10.113.134.177 CONNECT NONE/503 tunnel sip.example.com / 443 - 0 3088 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 2012-10-19T16:12:51.000 UTC 10.113.134.177 CONNECT NONE/503 tunnel sip.example.com / 443 - 0 3088 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 2012-10-19T16:12:51.100 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 2012-10-19T16:12:51.300 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 -2012-10-19T16:12:51.000 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 \ No newline at end of file +2012-10-19T16:12:51.000 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 diff --git a/logisland-plugins/logisland-common-logs-plugin/pom.xml b/logisland-plugins/logisland-common-logs-plugin/pom.xml index 3cfcf050a..4ca26360a 100644 --- a/logisland-plugins/logisland-common-logs-plugin/pom.xml +++ b/logisland-plugins/logisland-common-logs-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-common-logs-plugin diff --git a/logisland-plugins/logisland-common-logs-plugin/src/main/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLog.java b/logisland-plugins/logisland-common-logs-plugin/src/main/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLog.java index 7ab2a916f..7b32013a3 100644 --- a/logisland-plugins/logisland-common-logs-plugin/src/main/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLog.java +++ b/logisland-plugins/logisland-common-logs-plugin/src/main/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLog.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-common-logs-plugin/src/test/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLogTest.java b/logisland-plugins/logisland-common-logs-plugin/src/test/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLogTest.java index 7004e1f3b..e3b0e57e9 100644 --- a/logisland-plugins/logisland-common-logs-plugin/src/test/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLogTest.java +++ b/logisland-plugins/logisland-common-logs-plugin/src/test/java/com/hurence/logisland/processor/commonlogs/gitlab/ParseGitlabLogTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-common-processors-plugin/pom.xml b/logisland-plugins/logisland-common-processors-plugin/pom.xml index 504cc6b7f..482a04be5 100644 --- a/logisland-plugins/logisland-common-processors-plugin/pom.xml +++ b/logisland-plugins/logisland-common-processors-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-common-processors-plugin diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/FlatMap.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/FlatMap.java index 6440c8a7c..749d45927 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/FlatMap.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/FlatMap.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java index 4206b958e..4f7d0b29d 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/ModifyId.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java index bc790f304..8bcec435d 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/AbstractNashornSandboxProcessor.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java index 5ff9ca418..98c581960 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckAlerts.java @@ -1,13 +1,12 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -117,17 +116,19 @@ protected void setupDynamicProperties(ProcessContext context) { sbActivation.append("var alert = false;\n") .append("function getValue(id) {\n") .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") - .append(" if(record == null) return Double.NaN;\n") - .append(" return record.getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n") + .append(" if(record === null) return Double.NaN;\n") + .append(" else return record.getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n") .append("function getDuration(id) {\n") .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") - .append(" if(record == null) return -1;\n") - .append(" var duration = new Date().getTime() - record.getTime().getTime();\n") - .append(" return duration; \n};\n") + .append(" if(record === null) return -1;\n") + .append(" else { \n") + .append(" var duration = new Date().getTime() - record.getTime().getTime();\n") + .append(" return duration; \n}};\n") .append("function getCount(id) {\n") .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") - .append(" if(record == null) return -1;\n") - .append(" return record.getField(\"record_count\").asLong(); \n};\n") + .append(" if(record === null) return -1;\n") + .append(" else return record.getField(\"record_count\").asLong(); \n};\n") + .append("try {\n") .append("if( ") .append(expandCode(profileActivationRule)) .append(" ) { \n"); @@ -144,15 +145,18 @@ protected void setupDynamicProperties(ProcessContext context) { sb.append(" if( ") .append(value) .append(" ) { alert = true; }\n") - .append("}\n"); + .append("}\n") + .append("} catch(error) {}"); dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - // System.out.println(sb.toString()); - // logger.debug(sb.toString()); + // System.out.println(sb.toString()); + // logger.debug(sb.toString()); } + + } @@ -183,7 +187,7 @@ public Collection process(ProcessContext context, Collection rec Record errorRecord = new StandardRecord(RecordDictionary.ERROR) .setId(entry.getKey()) .addError("ScriptException", e.getMessage()); - outputRecords.add(errorRecord); + // outputRecords.add(errorRecord); logger.error(e.toString()); } } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java index 35aa6ff3c..196923a66 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/CheckThresholds.java @@ -1,13 +1,12 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -72,10 +71,20 @@ the record time will be set to the current timestamp .build(); + public static final PropertyDescriptor MIN_UPDATE_TIME_MS = new PropertyDescriptor.Builder() + .name("min.update.time.ms") + .description("The minimum amount of time (in ms) that we expect between two consecutive update of the same threshold record") + .required(false) + .defaultValue("200") + .addValidator(StandardValidators.LONG_VALIDATOR) + .build(); + + @Override public List getSupportedPropertyDescriptors() { List properties = new ArrayList<>(super.getSupportedPropertyDescriptors()); properties.add(RECORD_TTL); + properties.add(MIN_UPDATE_TIME_MS); return properties; } @@ -93,14 +102,16 @@ protected void setupDynamicProperties(ProcessContext context) { .replaceAll("\\.value", ".getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble()"); StringBuilder sb = new StringBuilder(); - sb.append("var match=false;\n"); - sb.append("if( ") + sb.append("var match=false;\n") + .append("try {\n") + .append("if( ") .append(value) - .append(" ) { match=true; }\n"); + .append(" ) { match=true; }\n") + .append("} catch(error) {}"); dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - // System.out.println(sb.toString()); - // logger.debug(sb.toString()); + // System.out.println(sb.toString()); + // logger.debug(sb.toString()); } defaultCollection = context.getPropertyValue(DATASTORE_CACHE_COLLECTION).asString(); recordTTL = context.getPropertyValue(RECORD_TTL).asInteger(); @@ -120,7 +131,7 @@ public Collection process(ProcessContext context, Collection rec List outputRecords = new ArrayList<>(records); for (final Map.Entry entry : dynamicTagValuesMap.entrySet()) { - // look for record into the cache + // look for record into the cache & remove this if TTL expired String key = entry.getKey(); Record cachedThreshold = datastoreClientService.get(defaultCollection, new StandardRecord().setId(key)); if (cachedThreshold != null) { @@ -135,22 +146,28 @@ public Collection process(ProcessContext context, Collection rec sandbox.eval(entry.getValue()); Boolean match = (Boolean) sandbox.get("match"); if (match) { - - if (cachedThreshold != null) { + // check if we haven't handle this event yet + Long durationBeetwenLastUpdateInMs = System.currentTimeMillis() - + cachedThreshold.getField(FieldDictionary.RECORD_LAST_UPDATE_TIME).asLong(); + if (durationBeetwenLastUpdateInMs > context.getPropertyValue(MIN_UPDATE_TIME_MS).asLong()) { + Long count = cachedThreshold.getField(FieldDictionary.RECORD_COUNT).asLong(); Date firstThresholdTime = cachedThreshold.getTime(); - cachedThreshold.setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) - .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, count + 1) - .setTime(firstThresholdTime); + cachedThreshold.setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, count + 1) + .setTime(firstThresholdTime) + .setField(FieldDictionary.RECORD_LAST_UPDATE_TIME, FieldType.LONG, System.currentTimeMillis()); datastoreClientService.put(defaultCollection, cachedThreshold, true); outputRecords.add(cachedThreshold); + } + } else { Record threshold = new StandardRecord(outputRecordType) .setId(key) .setStringField(FieldDictionary.RECORD_VALUE, context.getPropertyValue(key).asString()) - .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, 1L); + .setField(FieldDictionary.RECORD_COUNT, FieldType.LONG, 1L) + .setField(FieldDictionary.RECORD_LAST_UPDATE_TIME, FieldType.LONG, System.currentTimeMillis()); datastoreClientService.put(defaultCollection, threshold, true); outputRecords.add(threshold); } @@ -159,7 +176,7 @@ public Collection process(ProcessContext context, Collection rec Record errorRecord = new StandardRecord(RecordDictionary.ERROR) .setId(entry.getKey()) .addError("ScriptException", e.getMessage()); - outputRecords.add(errorRecord); + // outputRecords.add(errorRecord); logger.error(e.toString()); } } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java index 50282ece9..b9d958f58 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/alerting/ComputeTags.java @@ -1,13 +1,12 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

* Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -21,6 +20,7 @@ import com.hurence.logisland.annotation.documentation.Tags; import com.hurence.logisland.component.PropertyDescriptor; import com.hurence.logisland.processor.ProcessContext; +import com.hurence.logisland.record.FieldDictionary; import com.hurence.logisland.record.Record; import com.hurence.logisland.record.RecordDictionary; import com.hurence.logisland.record.StandardRecord; @@ -56,8 +56,8 @@ protected void setupDynamicProperties(ProcessContext context) { sbActivation .append("function getValue(id) {\n") .append(" var record = cache.get(\"test\", new com.hurence.logisland.record.StandardRecord().setId(id));\n") - .append(" if(record == null) return Double.NaN;\n") - .append(" return record.getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n"); + .append(" if(record === undefined) return Double.NaN;\n") + .append(" else return record.getField(com.hurence.logisland.record.FieldDictionary.RECORD_VALUE).asDouble(); \n};\n"); for (final Map.Entry entry : context.getProperties().entrySet()) { @@ -74,6 +74,7 @@ protected void setupDynamicProperties(ProcessContext context) { .append("() { ") .append(value) .append(" }; \n"); + sb.append("try {\n"); sb.append("var record_") .append(key) .append(" = new com.hurence.logisland.record.StandardRecord(\"") @@ -89,10 +90,11 @@ protected void setupDynamicProperties(ProcessContext context) { .append(" com.hurence.logisland.record.FieldType.DOUBLE,") .append(key) .append("());\n"); + sb.append("}\ncatch(error){}"); dynamicTagValuesMap.put(entry.getKey().getName(), sb.toString()); - // System.out.println(sb.toString()); - // logger.debug(sb.toString()); + // System.out.println(sb.toString()); + // logger.debug(sb.toString()); } } @@ -111,12 +113,13 @@ public Collection process(ProcessContext context, Collection rec try { sandbox.eval(entry.getValue()); Record cached = (Record) sandbox.get("record_" + entry.getKey()); - outputRecords.add(cached); + if (cached.hasField(FieldDictionary.RECORD_VALUE)) + outputRecords.add(cached); } catch (ScriptException e) { Record errorRecord = new StandardRecord(RecordDictionary.ERROR) .setId(entry.getKey()) .addError("ScriptException", e.getMessage()); - outputRecords.add(errorRecord); + // outputRecords.add(errorRecord); logger.error(e.toString()); } } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/AbstractDatastoreProcessor.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/AbstractDatastoreProcessor.java index 8a4d2598e..16912458c 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/AbstractDatastoreProcessor.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/AbstractDatastoreProcessor.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/BulkPut.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/BulkPut.java index c52448501..a332da4c6 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/BulkPut.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/BulkPut.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/EnrichRecords.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/EnrichRecords.java index 2da5d0480..fb3d989fd 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/EnrichRecords.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/EnrichRecords.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/MultiGet.java b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/MultiGet.java index a4eff8430..ad3c55c8a 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/MultiGet.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/main/java/com/hurence/logisland/processor/datastore/MultiGet.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/FlatMapTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/FlatMapTest.java index f4ecd87db..51cf6af7d 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/FlatMapTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/FlatMapTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java index 0b5a6ec68..058bc77b8 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java index 8d6b34e2e..6cee69797 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckThresholdsTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index 4beb4a0a5..e6ef3ad13 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/EnrichRecordsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/EnrichRecordsTest.java index a0bd10aed..ca29f12ed 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/EnrichRecordsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/EnrichRecordsTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java index 05eecccd3..1c4ef9d21 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/datastore/MockDatastoreService.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/resources/data/TracesAnalysis_samples.txt b/logisland-plugins/logisland-common-processors-plugin/src/test/resources/data/TracesAnalysis_samples.txt index cb32805bf..e3dbca4ba 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/resources/data/TracesAnalysis_samples.txt +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/resources/data/TracesAnalysis_samples.txt @@ -59,8 +59,8 @@ 2012-10-19T10:12:00.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 1942 MICROSOFT BITS/6.7 false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:12:06.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 472 MICROSOFT BITS/6.7 false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:12:19.000 GMT 10.112.123.187 CONNECT TCP_MISS_SSL/200 tunnel dl.google.com / 443 - 0 39 MICROSOFT BITS/6.7 false false false false false null 173.194.34.224 9q9hyebw8m76 37.41920471191406 -122.05740356445312 United States 43 null -2012-10-19T10.13.00.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 1942 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null -2012-10-19T10.13.05.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 472 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null +2012-10-19T10.14.00.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 1942 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null +2012-10-19T10.14.05.000 GMT 10.112.123.187 CONNECT TCP_DENIED/407 tunnel dl.google.com / 443 - 0 472 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:12:37.000 GMT 10.112.123.187 CONNECT TCP_MISS_SSL/200 tunnel dl.google.com / 443 - 0 39 - false false false false false null 173.194.34.224 9q9hyebw8m76 37.41920471191406 -122.05740356445312 United States 43 null 2012-10-19T10:17:45.000 GMT 10.112.123.187 POST TCP_DENIED/407 http tools.google.com /service/update2 80 - 0 1942 GOOGLE UPDATE/1.3.21.99,WINHTTP false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null 2012-10-19T10:17:51.000 GMT 10.112.123.187 POST TCP_DENIED/407 http tools.google.com /service/update2 80 - 0 472 GOOGLE UPDATE/1.3.21.99,WINHTTP false false false false false null 255.255.255.255 null 0.0 0.0 null 43 null diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/resources/enriched_sample_values.txt b/logisland-plugins/logisland-common-processors-plugin/src/test/resources/enriched_sample_values.txt index 0b70af573..a2c2127e9 100755 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/resources/enriched_sample_values.txt +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/resources/enriched_sample_values.txt @@ -7,9 +7,9 @@ 2012-10-19T16:12:53.000 UTC 10.112.122.22 POST TCP_CLIENT_REFRESH_MISS/200 http 195.145.147.146 /idle/OaOmdz02zSLnCW6Z/34 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 195.145.147.146 u1n6hu38e1n6 51.0 9.0 Germany 43 2012-10-19T16:12:53.000 UTC 10.112.122.22 POST TCP_CLIENT_REFRESH_MISS/200 http 195.145.147.146 /idle/ObOmdz02zSLkCW6Z/34 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 195.145.147.146 u1n6hu38e1n6 51.0 9.0 Germany 43 2012-10-19T16:12:53.000 UTC 10.112.122.22 POST TCP_CLIENT_REFRESH_MISS/200 http 195.145.147.146 /idle/ObOmdz02zSLkCW6Z/34 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 195.145.147.146 u1n6hu38e1n6 51.0 9.0 Germany 43 -2012-10-19T16:12:51.000 UTC 10.112.31.174 POST TCP_CLIENT_REFRESH_MISS/200 http 95.140.225.151 /idle/Cf7maD8spa7BaStZ/1030 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 95.140.225.151 gcpuvp334xv6 51.5 -0.1300048828125 United Kingdom 43 +2012-10-19T16:12:51.000 UTC 10.112.31.174 POST TCP_CLIENT_REFRESH_MISS/200 http 95.140.225.151 /idle/Cf7maD8spa7BaStZ/1030 -1 - 0 277 "SHOCKWAVE FLASH" false false false false false null 95.140.225.151 gcpuvp334xv6 51.5 -0.14.0048828125 United Kingdom 43 2012-10-19T16:12:51.500 UTC 10.113.134.177 CONNECT NONE/503 tunnel sip.example.com / 443 - 0 3088 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 2012-10-19T16:12:51.000 UTC 10.113.134.177 CONNECT NONE/503 tunnel sip.example.com / 443 - 0 3088 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 2012-10-19T16:12:51.100 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 2012-10-19T16:12:51.300 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 -2012-10-19T16:12:51.000 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 \ No newline at end of file +2012-10-19T16:12:51.000 UTC 10.113.134.177 CONNECT NONE/503 tunnel sipexternal.example.com / 443 - 0 3112 - false false false false false null 255.255.255.255 null 0.0 0.0 null 43 diff --git a/logisland-plugins/logisland-cyber-security-plugin/pom.xml b/logisland-plugins/logisland-cyber-security-plugin/pom.xml index a0b7c3804..0cfeac87e 100644 --- a/logisland-plugins/logisland-cyber-security-plugin/pom.xml +++ b/logisland-plugins/logisland-cyber-security-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-cyber-security-plugin diff --git a/logisland-plugins/logisland-elasticsearch-plugin/pom.xml b/logisland-plugins/logisland-elasticsearch-plugin/pom.xml index 4cfae185d..4e48b17ff 100644 --- a/logisland-plugins/logisland-elasticsearch-plugin/pom.xml +++ b/logisland-plugins/logisland-elasticsearch-plugin/pom.xml @@ -23,7 +23,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-elasticsearch-plugin diff --git a/logisland-plugins/logisland-enrichment-plugin/pom.xml b/logisland-plugins/logisland-enrichment-plugin/pom.xml index 8ed56d372..3926ef323 100644 --- a/logisland-plugins/logisland-enrichment-plugin/pom.xml +++ b/logisland-plugins/logisland-enrichment-plugin/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-enrichment-plugin diff --git a/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpAbstractProcessor.java b/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpAbstractProcessor.java index 6eed5161b..13dae0189 100644 --- a/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpAbstractProcessor.java +++ b/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpAbstractProcessor.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.processor.enrichment; import com.hurence.logisland.component.PropertyDescriptor; diff --git a/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToFqdn.java b/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToFqdn.java index 6549e48e0..7e01db234 100644 --- a/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToFqdn.java +++ b/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToFqdn.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.processor.enrichment; import com.hurence.logisland.annotation.documentation.CapabilityDescription; diff --git a/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToGeo.java b/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToGeo.java index d4f5e4d7d..cc61e16f2 100644 --- a/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToGeo.java +++ b/logisland-plugins/logisland-enrichment-plugin/src/main/java/com/hurence/logisland/processor/enrichment/IpToGeo.java @@ -1,12 +1,12 @@ /** - * Copyright (C) 2017 Hurence - *

+ * Copyright (C) 2016 Hurence (support@hurence.com) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToFqdnTest.java b/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToFqdnTest.java index 4d5d24aa9..b56deb82e 100644 --- a/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToFqdnTest.java +++ b/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToFqdnTest.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.processor.enrichment; diff --git a/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToGeoTest.java b/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToGeoTest.java index f963e07f6..a69edeaad 100644 --- a/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToGeoTest.java +++ b/logisland-plugins/logisland-enrichment-plugin/src/test/java/com/hurence/logisland/processor/enrichment/IpToGeoTest.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.processor.enrichment; diff --git a/logisland-plugins/logisland-excel-plugin/pom.xml b/logisland-plugins/logisland-excel-plugin/pom.xml index 46c4ee19b..ed07ff554 100644 --- a/logisland-plugins/logisland-excel-plugin/pom.xml +++ b/logisland-plugins/logisland-excel-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-excel-plugin diff --git a/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtract.java b/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtract.java index 8fb4b8c12..2bf735201 100644 --- a/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtract.java +++ b/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtract.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,7 +12,6 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ package com.hurence.logisland.processor.excel; diff --git a/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtractProperties.java b/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtractProperties.java index 29a107d7f..eb48daf46 100644 --- a/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtractProperties.java +++ b/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/ExcelExtractProperties.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.processor.excel; import com.hurence.logisland.component.PropertyDescriptor; diff --git a/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/Fields.java b/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/Fields.java index 7368ff682..f44eaba59 100644 --- a/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/Fields.java +++ b/logisland-plugins/logisland-excel-plugin/src/main/java/com/hurence/logisland/processor/excel/Fields.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.processor.excel; import com.hurence.logisland.record.Field; diff --git a/logisland-plugins/logisland-excel-plugin/src/test/java/com/hurence/logisland/processor/excel/ExcelExtractTest.java b/logisland-plugins/logisland-excel-plugin/src/test/java/com/hurence/logisland/processor/excel/ExcelExtractTest.java index 3beda2d87..1c393961c 100644 --- a/logisland-plugins/logisland-excel-plugin/src/test/java/com/hurence/logisland/processor/excel/ExcelExtractTest.java +++ b/logisland-plugins/logisland-excel-plugin/src/test/java/com/hurence/logisland/processor/excel/ExcelExtractTest.java @@ -1,5 +1,5 @@ -/* - * Copyright (C) 2018 Hurence (support@hurence.com) +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,9 +12,7 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - package com.hurence.logisland.processor.excel; import com.hurence.logisland.record.FieldDictionary; diff --git a/logisland-plugins/logisland-hbase-plugin/pom.xml b/logisland-plugins/logisland-hbase-plugin/pom.xml index 5d9b0b060..d9bfc0328 100644 --- a/logisland-plugins/logisland-hbase-plugin/pom.xml +++ b/logisland-plugins/logisland-hbase-plugin/pom.xml @@ -5,7 +5,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-hbase-plugin Support for interacting with HBase diff --git a/logisland-plugins/logisland-hbase-plugin/src/test/java/com/hurence/logisland/processor/hbase/util/TestObjectSerDe.java b/logisland-plugins/logisland-hbase-plugin/src/test/java/com/hurence/logisland/processor/hbase/util/TestObjectSerDe.java index e521bb56c..0a738a078 100644 --- a/logisland-plugins/logisland-hbase-plugin/src/test/java/com/hurence/logisland/processor/hbase/util/TestObjectSerDe.java +++ b/logisland-plugins/logisland-hbase-plugin/src/test/java/com/hurence/logisland/processor/hbase/util/TestObjectSerDe.java @@ -1,19 +1,18 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ - package com.hurence.logisland.processor.hbase.util; import org.junit.Assert; diff --git a/logisland-plugins/logisland-outlier-detection-plugin/pom.xml b/logisland-plugins/logisland-outlier-detection-plugin/pom.xml index eed8429bd..c895c1193 100644 --- a/logisland-plugins/logisland-outlier-detection-plugin/pom.xml +++ b/logisland-plugins/logisland-outlier-detection-plugin/pom.xml @@ -25,7 +25,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 jar diff --git a/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/caseystella/analytics/converters/MappingConverter.java b/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/caseystella/analytics/converters/MappingConverter.java index 1753cda53..1ad720f41 100644 --- a/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/caseystella/analytics/converters/MappingConverter.java +++ b/logisland-plugins/logisland-outlier-detection-plugin/src/main/java/com/caseystella/analytics/converters/MappingConverter.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-plugins/logisland-querymatcher-plugin/pom.xml b/logisland-plugins/logisland-querymatcher-plugin/pom.xml index 42fab7dbf..aa3e1da72 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/pom.xml +++ b/logisland-plugins/logisland-querymatcher-plugin/pom.xml @@ -26,7 +26,7 @@ http://www.w3.org/2001/XMLSchema-instance "> com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 jar diff --git a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchHandlers.java b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchHandlers.java index 33dc91b88..8d6f9d16b 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchHandlers.java +++ b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchHandlers.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchIP.java b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchIP.java index 9720bdaad..8a32cdd35 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchIP.java +++ b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchIP.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016-2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchQuery.java b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchQuery.java index 1c7ed1ffe..5e0553cf0 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchQuery.java +++ b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchQuery.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016-2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchingRule.java b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchingRule.java index 6e76857a2..988084836 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchingRule.java +++ b/logisland-plugins/logisland-querymatcher-plugin/src/main/java/com/hurence/logisland/processor/MatchingRule.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016-2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchIPTest.java b/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchIPTest.java index 7e6e09fb9..f22d9f839 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchIPTest.java +++ b/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchIPTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016-2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchQueryTest.java b/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchQueryTest.java index eb2d92393..0dea976c6 100644 --- a/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchQueryTest.java +++ b/logisland-plugins/logisland-querymatcher-plugin/src/test/java/com/hurence/logisland/processor/MatchQueryTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016-2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-plugins/logisland-sampling-plugin/pom.xml b/logisland-plugins/logisland-sampling-plugin/pom.xml index 083e337a1..dabdfe5c1 100644 --- a/logisland-plugins/logisland-sampling-plugin/pom.xml +++ b/logisland-plugins/logisland-sampling-plugin/pom.xml @@ -26,7 +26,7 @@ http://www.w3.org/2001/XMLSchema-instance "> com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 jar diff --git a/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data1.txt b/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data1.txt index 19fdcbd6c..9d591bc47 100644 --- a/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data1.txt +++ b/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data1.txt @@ -76,7 +76,7 @@ 1370551800000,0.10000 1370552100000,0.10400 1370552400000,0.11250 -1370552700000,0.13.00 +1370552700000,0.14.00 1370553000000,0.11450 1370553300000,0.10950 1370553600000,0.10500 @@ -210,14 +210,14 @@ 1370592000000,0.10200 1370592300000,0.10300 1370592600000,0.10950 -1370592900000,0.13.00 -1370593200000,0.13.00 -1370593500000,0.13.00 +1370592900000,0.14.00 +1370593200000,0.14.00 +1370593500000,0.14.00 1370593800000,0.11350 1370594100000,0.11350 -1370594400000,0.13.00 +1370594400000,0.14.00 1370594700000,0.11750 -1370595000000,0.13.00 +1370595000000,0.14.00 1370595300000,0.11850 1370595600000,0.11850 1370595900000,0.12100 @@ -229,26 +229,26 @@ 1370597700000,0.12400 1370598000000,0.12350 1370598300000,0.12050 -1370598600000,0.13.00 +1370598600000,0.14.00 1370598900000,0.11550 1370599200000,0.11550 -1370599500000,0.13.00 -1370599800000,0.13.00 -1370600100000,0.13.00 -1370600400000,0.13.00 -1370600700000,0.13.00 -1370601000000,0.13.00 +1370599500000,0.14.00 +1370599800000,0.14.00 +1370600100000,0.14.00 +1370600400000,0.14.00 +1370600700000,0.14.00 +1370601000000,0.14.00 1370601300000,0.11250 1370601600000,0.11250 -1370601900000,0.13.00 +1370601900000,0.14.00 1370602200000,0.11350 1370602500000,0.11750 -1370602800000,0.13.00 +1370602800000,0.14.00 1370603100000,0.11250 1370603400000,0.10900 1370603700000,0.10800 1370604000000,0.11050 -1370604300000,0.13.00 +1370604300000,0.14.00 1370604600000,0.10900 1370604900000,0.10900 1370605200000,0.10600 @@ -271,16 +271,16 @@ 1370610300000,0.09150 1370610600000,0.09200 1370610900000,0.09150 -1370.13.00000,0.09500 -1370.13.00000,0.09550 -1370.13.00000,0.09300 +1370.14.00000,0.09500 +1370.14.00000,0.09550 +1370.14.00000,0.09300 1370612100000,0.09200 1370612400000,0.09250 1370612700000,0.09400 -1370613000000,0.09450 -1370613300000,0.09700 -1370613600000,0.09600 -1370613900000,0.09500 +1370.14.00000,0.09450 +1370.14.00000,0.09700 +1370.14.00000,0.09600 +1370.14.00000,0.09500 1370614200000,0.09650 1370614500000,0.09700 1370614800000,0.09650 @@ -438,17 +438,17 @@ 1371144900000,0.11050 1371145200000,0.11250 1371145500000,0.11250 -1371145800000,0.13.00 +1371145800000,0.14.00 1371146100000,0.11250 1371146400000,0.11150 1371146700000,0.11250 -1371147000000,0.13.00 +1371147000000,0.14.00 1371147300000,0.11250 1371147600000,0.11550 -1371147900000,0.13.00 +1371147900000,0.14.00 1371148200000,0.12050 1371148500000,0.12750 -1371148800000,0.13500 +1371148800000,0.14.00 1371149100000,0.13950 1371149400000,0.14250 1371149700000,0.14400 @@ -488,15 +488,15 @@ 1371159900000,0.14200 1371160200000,0.14050 1371160500000,0.13250 -1371160800000,0.13000 +1371160800000,0.14.00 1371161100000,0.12600 1371161400000,0.12200 1371161700000,0.11950 1371162000000,0.11850 1371162300000,0.11850 1371162600000,0.11350 -1371162900000,0.13.00 -1371163200000,0.13.00 +1371162900000,0.14.00 +1371163200000,0.14.00 1371163500000,0.11050 1371163800000,0.10850 1371164100000,0.10650 @@ -611,28 +611,28 @@ 1371196800000,0.10650 1371197100000,0.10550 1371197400000,0.10900 -1371197700000,0.13.00 +1371197700000,0.14.00 1371198000000,0.11450 -1371198300000,0.13.00 +1371198300000,0.14.00 1371198600000,0.11050 1371198900000,0.11050 1371199200000,0.10900 -1371199500000,0.13.00 -1371199800000,0.13.00 -1371200100000,0.13.00 -1371200400000,0.13.00 +1371199500000,0.14.00 +1371199800000,0.14.00 +1371200100000,0.14.00 +1371200400000,0.14.00 1371200700000,0.11150 1371201000000,0.11550 -1371201300000,0.13.00 +1371201300000,0.14.00 1371201600000,0.11550 1371201900000,0.11550 1371202200000,0.11450 1371202500000,0.11550 -1371202800000,0.13.00 +1371202800000,0.14.00 1371203100000,0.11350 -1371203400000,0.13.00 -1371203700000,0.13.00 -1371204000000,0.13.00 +1371203400000,0.14.00 +1371203700000,0.14.00 +1371204000000,0.14.00 1371204300000,0.11150 1371204600000,0.11250 1371204900000,0.11250 @@ -912,30 +912,30 @@ 1371287100000,0.10350 1371287400000,0.10700 1371287700000,0.10950 -1371288000000,0.13.00 -1371288300000,0.13.00 -1371288600000,0.13.00 +1371288000000,0.14.00 +1371288300000,0.14.00 +1371288600000,0.14.00 1371288900000,0.11550 -1371289200000,0.13.00 -1371289500000,0.13.00 -1371289800000,0.13.00 +1371289200000,0.14.00 +1371289500000,0.14.00 +1371289800000,0.14.00 1371290100000,0.11050 1371290400000,0.11350 -1371290700000,0.13.00 -1371291000000,0.13.00 +1371290700000,0.14.00 +1371291000000,0.14.00 1371291300000,0.11650 -1371291600000,0.13.00 +1371291600000,0.14.00 1371291900000,0.11650 -1371292200000,0.13.00 -1371292500000,0.13.00 +1371292200000,0.14.00 +1371292500000,0.14.00 1371292800000,0.11650 -1371293100000,0.13.00 +1371293100000,0.14.00 1371293400000,0.11850 -1371293700000,0.13.00 +1371293700000,0.14.00 1371294000000,0.11850 -1371294300000,0.13.00 +1371294300000,0.14.00 1371294600000,0.11750 -1371294900000,0.13.00 +1371294900000,0.14.00 1371295200000,0.12000 1371295500000,0.12150 1371295800000,0.12000 @@ -944,28 +944,28 @@ 1371296700000,0.11950 1371297000000,0.12100 1371297300000,0.11950 -1371297600000,0.13.00 +1371297600000,0.14.00 1371297900000,0.11350 -1371298200000,0.13.00 +1371298200000,0.14.00 1371298500000,0.11150 -1371298800000,0.13.00 -1371299100000,0.13.00 -1371299400000,0.13.00 -1371299700000,0.13.00 +1371298800000,0.14.00 +1371299100000,0.14.00 +1371299400000,0.14.00 +1371299700000,0.14.00 1371300000000,0.11350 1371300300000,0.11750 -1371300600000,0.13.00 -1371300900000,0.13.00 -1371301200000,0.13.00 -1371301500000,0.13.00 +1371300600000,0.14.00 +1371300900000,0.14.00 +1371301200000,0.14.00 +1371301500000,0.14.00 1371301800000,0.11450 -1371302100000,0.13.00 +1371302100000,0.14.00 1371302400000,0.11150 -1371302700000,0.13.00 +1371302700000,0.14.00 1371303000000,0.11250 -1371303300000,0.13.00 +1371303300000,0.14.00 1371303600000,0.11050 -1371303900000,0.13.00 +1371303900000,0.14.00 1371304200000,0.11050 1371304500000,0.11050 1371304800000,0.10950 @@ -980,7 +980,7 @@ 1371307500000,0.10500 1371307800000,0.10900 1371308100000,0.11150 -1371308400000,0.13.00 +1371308400000,0.14.00 1371308700000,0.10900 1371309000000,0.10650 1371309300000,0.10400 @@ -1216,55 +1216,55 @@ 1371378300000,0.10200 1371378600000,0.10600 1371378900000,0.10900 -1371379200000,0.13.00 -1371379500000,0.13.00 -1371379800000,0.13.00 -1371380100000,0.13.00 -1371380400000,0.13.00 -1371380700000,0.13.00 +1371379200000,0.14.00 +1371379500000,0.14.00 +1371379800000,0.14.00 +1371380100000,0.14.00 +1371380400000,0.14.00 +1371380700000,0.14.00 1371381000000,0.11750 -1371381300000,0.13.00 -1371381600000,0.13.00 -1371381900000,0.13.00 -1371382200000,0.13.00 +1371381300000,0.14.00 +1371381600000,0.14.00 +1371381900000,0.14.00 +1371382200000,0.14.00 1371382500000,0.11650 -1371382800000,0.13.00 -1371383100000,0.13.00 +1371382800000,0.14.00 +1371383100000,0.14.00 1371383400000,0.11450 -1371383700000,0.13.00 -1371384000000,0.13.00 +1371383700000,0.14.00 +1371384000000,0.14.00 1371384300000,0.11250 1371384600000,0.11650 1371384900000,0.11750 1371385200000,0.11750 1371385500000,0.11650 -1371385800000,0.13.00 +1371385800000,0.14.00 1371386100000,0.11650 1371386400000,0.12050 1371386700000,0.12300 1371387000000,0.12200 1371387300000,0.12050 1371387600000,0.12000 -1371387900000,0.13.00 -1371388200000,0.13.00 -1371388500000,0.13.00 +1371387900000,0.14.00 +1371388200000,0.14.00 +1371388500000,0.14.00 1371388800000,0.11450 1371389100000,0.11350 1371389400000,0.11450 1371389700000,0.11550 1371390000000,0.11750 1371390300000,0.11650 -1371390600000,0.13.00 -1371390900000,0.13.00 +1371390600000,0.14.00 +1371390900000,0.14.00 1371391200000,0.11350 -1371391500000,0.13.00 -1371391800000,0.13.00 +1371391500000,0.14.00 +1371391800000,0.14.00 1371392100000,0.11350 -1371392400000,0.13.00 +1371392400000,0.14.00 1371392700000,0.11250 -1371393000000,0.13.00 -1371393300000,0.13.00 -1371393600000,0.13.00 +1371393000000,0.14.00 +1371393300000,0.14.00 +1371393600000,0.14.00 1371393900000,0.11050 1371394200000,0.10800 1371394500000,0.10550 @@ -1556,14 +1556,14 @@ 1371480300000,0.13850 1371480600000,0.13150 1371480900000,0.13350 -1371481200000,0.13500 -1371481500000,0.13300 -1371481800000,0.13100 +1371481200000,0.14.00 +1371481500000,0.14.00 +1371481800000,0.14.00 1371482100000,0.12900 1371482400000,0.12450 1371482700000,0.12000 1371483000000,0.11550 -1371483300000,0.13.00 +1371483300000,0.14.00 1371483600000,0.11450 1371483900000,0.10950 1371484200000,0.10600 @@ -1765,34 +1765,34 @@ 1371543000000,0.10550 1371543300000,0.10850 1371543600000,0.11150 -1371543900000,0.13.00 +1371543900000,0.14.00 1371544200000,0.11450 -1371544500000,0.13.00 -1371544800000,0.13.00 +1371544500000,0.14.00 +1371544800000,0.14.00 1371545100000,0.11350 -1371545400000,0.13.00 +1371545400000,0.14.00 1371545700000,0.11450 -1371546000000,0.13.00 +1371546000000,0.14.00 1371546300000,0.11450 1371546600000,0.11150 1371546900000,0.10850 1371547200000,0.10750 1371547500000,0.10800 -1371547800000,0.13.00 -1371548100000,0.13.00 +1371547800000,0.14.00 +1371548100000,0.14.00 1371548400000,0.11550 1371548700000,0.11250 -1371549000000,0.13.00 +1371549000000,0.14.00 1371549300000,0.10900 -1371549600000,0.13.00 +1371549600000,0.14.00 1371549900000,0.10950 -1371550200000,0.13.00 -1371550500000,0.13.00 -1371550800000,0.13.00 +1371550200000,0.14.00 +1371550500000,0.14.00 +1371550800000,0.14.00 1371551100000,0.11550 1371551400000,0.11450 1371551700000,0.11250 -1371552000000,0.13.00 +1371552000000,0.14.00 1371552300000,0.10850 1371552600000,0.10550 1371552900000,0.10400 @@ -2046,31 +2046,31 @@ 1371627300000,0.10650 1371627600000,0.10700 1371627900000,0.10950 -1371628200000,0.13.00 +1371628200000,0.14.00 1371628500000,0.11050 -1371628800000,0.13.00 -1371629100000,0.13.00 -1371629400000,0.13.00 -1371629700000,0.13.00 -1371630000000,0.13.00 +1371628800000,0.14.00 +1371629100000,0.14.00 +1371629400000,0.14.00 +1371629700000,0.14.00 +1371630000000,0.14.00 1371630300000,0.10850 1371630600000,0.11150 -1371630900000,0.13.00 +1371630900000,0.14.00 1371631200000,0.11250 -1371631500000,0.13.00 -1371631800000,0.13.00 +1371631500000,0.14.00 +1371631800000,0.14.00 1371632100000,0.10850 1371632400000,0.11050 1371632700000,0.11050 1371633000000,0.11050 -1371633300000,0.13.00 -1371633600000,0.13.00 +1371633300000,0.14.00 +1371633600000,0.14.00 1371633900000,0.11350 1371634200000,0.11150 1371634500000,0.11050 1371634800000,0.10850 -1371635100000,0.13.00 -1371635400000,0.13.00 +1371635100000,0.14.00 +1371635400000,0.14.00 1371635700000,0.10900 1371636000000,0.10750 1371636300000,0.10800 @@ -2338,31 +2338,31 @@ 1371714900000,0.10250 1371715200000,0.10400 1371715500000,0.10700 -1371715800000,0.13.00 +1371715800000,0.14.00 1371716100000,0.10850 1371716400000,0.10800 1371716700000,0.10750 1371717000000,0.10900 1371717300000,0.10800 -1371717600000,0.13.00 -1371717900000,0.13.00 -1371718200000,0.13.00 -1371718500000,0.13.00 -1371718800000,0.13.00 -1371719100000,0.13.00 -1371719400000,0.13.00 -1371719700000,0.13.00 +1371717600000,0.14.00 +1371717900000,0.14.00 +1371718200000,0.14.00 +1371718500000,0.14.00 +1371718800000,0.14.00 +1371719100000,0.14.00 +1371719400000,0.14.00 +1371719700000,0.14.00 1371720000000,0.11150 1371720300000,0.11050 -1371720600000,0.13.00 -1371720900000,0.13.00 +1371720600000,0.14.00 +1371720900000,0.14.00 1371721200000,0.10850 1371721500000,0.10950 -1371721800000,0.13.00 -1371722100000,0.13.00 +1371721800000,0.14.00 +1371722100000,0.14.00 1371722400000,0.11350 1371722700000,0.11550 -1371723000000,0.13.00 +1371723000000,0.14.00 1371723300000,0.10950 1371723600000,0.10800 1371723900000,0.10750 @@ -2422,13 +2422,13 @@ 1371740100000,0.12600 1371740400000,0.12450 1371740700000,0.12100 -1371741000000,0.13.00 +1371741000000,0.14.00 1371741300000,0.11950 1371741600000,0.11850 -1371741900000,0.13.00 -1371742200000,0.13.00 +1371741900000,0.14.00 +1371742200000,0.14.00 1371742500000,0.11350 -1371742800000,0.13.00 +1371742800000,0.14.00 1371743100000,0.12250 1371743400000,0.12750 1371743700000,0.12800 @@ -2438,7 +2438,7 @@ 1371744900000,0.12300 1371745200000,0.12300 1371745500000,0.12150 -1371745800000,0.13.00 +1371745800000,0.14.00 1371746100000,0.11150 1371746400000,0.10900 1371746700000,0.10800 @@ -2532,11 +2532,11 @@ 1371773100000,0.10300 1371773400000,0.10350 1371773700000,0.10600 -1371774000000,0.13.00 -1371774300000,0.13.00 +1371774000000,0.14.00 +1371774300000,0.14.00 1371774600000,0.12300 1371774900000,0.12950 -1371775200000,0.13300 +1371775200000,0.14.00 1371775500000,0.13950 1371775800000,0.14250 1371776100000,0.14500 @@ -2556,12 +2556,12 @@ 1371780300000,0.14550 1371780600000,0.14350 1371780900000,0.14150 -1371781200000,0.13800 +1371781200000,0.14.00 1371781500000,0.13450 1371781800000,0.12650 1371782100000,0.12450 1371782400000,0.12100 -1371782700000,0.13.00 +1371782700000,0.14.00 1371783000000,0.11750 1371783300000,0.11650 1371783600000,0.11150 @@ -2630,33 +2630,33 @@ 1371802500000,0.10150 1371802800000,0.10000 1371803100000,0.10650 -1371803400000,0.13.00 +1371803400000,0.14.00 1371803700000,0.11250 -1371804000000,0.13.00 -1371804300000,0.13.00 +1371804000000,0.14.00 +1371804300000,0.14.00 1371804600000,0.11250 1371804900000,0.11250 1371805200000,0.11450 -1371805500000,0.13.00 +1371805500000,0.14.00 1371805800000,0.11550 -1371806100000,0.13.00 -1371806400000,0.13.00 +1371806100000,0.14.00 +1371806400000,0.14.00 1371806700000,0.11450 -1371807000000,0.13.00 -1371807300000,0.13.00 -1371807600000,0.13.00 -1371807900000,0.13.00 +1371807000000,0.14.00 +1371807300000,0.14.00 +1371807600000,0.14.00 +1371807900000,0.14.00 1371808200000,0.11350 -1371808500000,0.13.00 -1371808800000,0.13.00 -1371809100000,0.13.00 -1371809400000,0.13.00 +1371808500000,0.14.00 +1371808800000,0.14.00 +1371809100000,0.14.00 +1371809400000,0.14.00 1371809700000,0.11250 1371810000000,0.11450 1371810300000,0.11550 -1371810600000,0.13.00 -1371810900000,0.13.00 -1371811200000,0.13.00 +1371810600000,0.14.00 +1371810900000,0.14.00 +1371811200000,0.14.00 1371811500000,0.10900 1371811800000,0.10750 1371812100000,0.10600 @@ -2713,7 +2713,7 @@ 1371827400000,0.10800 1371827700000,0.11050 1371828000000,0.11150 -1371828300000,0.13.00 +1371828300000,0.14.00 1371828600000,0.11150 1371828900000,0.10850 1371829200000,0.10700 @@ -2735,29 +2735,29 @@ 1371834000000,0.09900 1371834300000,0.10800 1371834600000,0.11050 -1371834900000,0.13.00 -1371835200000,0.13.00 +1371834900000,0.14.00 +1371835200000,0.14.00 1371835500000,0.11150 -1371835800000,0.13.00 -1371836100000,0.13.00 -1371836400000,0.13.00 -1371836700000,0.13.00 +1371835800000,0.14.00 +1371836100000,0.14.00 +1371836400000,0.14.00 +1371836700000,0.14.00 1371837000000,0.12350 1371837300000,0.12700 1371837600000,0.12800 -1371837900000,0.13200 +1371837900000,0.14.00 1371838200000,0.13150 1371838500000,0.14250 1371838800000,0.14050 -1371839100000,0.13600 +1371839100000,0.14.00 1371839400000,0.13150 1371839700000,0.12750 1371840000000,0.12350 1371840300000,0.12250 -1371840600000,0.13.00 -1371840900000,0.13.00 -1371841200000,0.13.00 -1371841500000,0.13.00 +1371840600000,0.14.00 +1371840900000,0.14.00 +1371841200000,0.14.00 +1371841500000,0.14.00 1371841800000,0.11350 1371842100000,0.11150 1371842400000,0.11050 @@ -2935,22 +2935,22 @@ 1371894000000,0.10600 1371894300000,0.10950 1371894600000,0.11050 -1371894900000,0.13.00 +1371894900000,0.14.00 1371895200000,0.11350 -1371895500000,0.13.00 +1371895500000,0.14.00 1371895800000,0.12300 1371896100000,0.11050 1371896400000,0.10900 -1371896700000,0.13.00 -1371897000000,0.13.00 -1371897300000,0.13.00 -1371897600000,0.13.00 -1371897900000,0.13.00 +1371896700000,0.14.00 +1371897000000,0.14.00 +1371897300000,0.14.00 +1371897600000,0.14.00 +1371897900000,0.14.00 1371898200000,0.11750 1371898500000,0.11450 1371898800000,0.11350 -1371899100000,0.13.00 -1371899400000,0.13.00 +1371899100000,0.14.00 +1371899400000,0.14.00 1371899700000,0.11250 1371900000000,0.12100 1371900300000,0.12100 @@ -2964,16 +2964,16 @@ 1371902700000,0.12150 1371903000000,0.12050 1371903300000,0.11950 -1371903600000,0.13.00 +1371903600000,0.14.00 1371903900000,0.11750 -1371904200000,0.13.00 +1371904200000,0.14.00 1371904500000,0.11550 1371904800000,0.12000 -1371905100000,0.13.00 -1371905400000,0.13.00 +1371905100000,0.14.00 +1371905400000,0.14.00 1371905700000,0.11350 1371906000000,0.11250 -1371906300000,0.13.00 +1371906300000,0.14.00 1371906600000,0.11050 1371906900000,0.10950 1371907200000,0.10950 @@ -3226,13 +3226,13 @@ 1371981300000,0.11050 1371981600000,0.11450 1371981900000,0.11350 -1371982200000,0.13.00 -1371982500000,0.13.00 +1371982200000,0.14.00 +1371982500000,0.14.00 1371982800000,0.11250 1371983100000,0.11550 1371983400000,0.11450 1371983700000,0.11250 -1371984000000,0.13.00 +1371984000000,0.14.00 1371984300000,0.11450 1371984600000,0.11350 1371984900000,0.11650 @@ -3246,8 +3246,8 @@ 1371987300000,0.12150 1371987600000,0.12100 1371987900000,0.11950 -1371988200000,0.13.00 -1371988500000,0.13.00 +1371988200000,0.14.00 +1371988500000,0.14.00 1371988800000,0.11850 1371989100000,0.11850 1371989400000,0.11950 @@ -3265,23 +3265,23 @@ 1371993000000,0.12350 1371993300000,0.12400 1371993600000,0.11950 -1371993900000,0.13.00 -1371994200000,0.13.00 +1371993900000,0.14.00 +1371994200000,0.14.00 1371994500000,0.11750 1371994800000,0.12050 1371995100000,0.12200 1371995400000,0.11950 -1371995700000,0.13.00 -1371996000000,0.13.00 +1371995700000,0.14.00 +1371996000000,0.14.00 1371996300000,0.11650 -1371996600000,0.13.00 -1371996900000,0.13.00 -1371997200000,0.13.00 +1371996600000,0.14.00 +1371996900000,0.14.00 +1371997200000,0.14.00 1371997500000,0.11550 -1371997800000,0.13.00 -1371998100000,0.13.00 -1371998400000,0.13.00 -1371998700000,0.13.00 +1371997800000,0.14.00 +1371998100000,0.14.00 +1371998400000,0.14.00 +1371998700000,0.14.00 1371999000000,0.11650 1371999300000,0.10750 1371999600000,0.10450 @@ -3289,7 +3289,7 @@ 1372000200000,0.10150 1372000500000,0.10050 1372000800000,0.10000 -13720.13.0000,0.09900 +13720.14.0000,0.09900 1372001400000,0.10100 1372001700000,0.10200 1372002000000,0.10050 @@ -3323,7 +3323,7 @@ 1372010400000,0.08950 1372010700000,0.09050 1372011000000,0.09200 -1372011300000,0.08950 +13720.14.0000,0.08950 1372011600000,0.08700 1372011900000,0.08400 1372012200000,0.08400 @@ -3389,7 +3389,7 @@ 1372030200000,0.09200 1372030500000,0.09350 1372030800000,0.09400 -13720.13.0000,0.09350 +13720.14.0000,0.09350 1372031400000,0.09350 1372031700000,0.09300 1372032000000,0.09050 @@ -3423,7 +3423,7 @@ 1372040400000,0.06300 1372040700000,0.06300 1372041000000,0.06150 -1372041300000,0.05950 +13720.14.0000,0.05950 1372041600000,0.06000 1372041900000,0.06000 1372042200000,0.05850 @@ -3489,7 +3489,7 @@ 1372060200000,0.09700 1372060500000,0.09600 1372060800000,0.09650 -13720.13.0000,0.09950 +13720.14.0000,0.09950 1372061400000,0.09900 1372061700000,0.09900 1372062000000,0.10000 @@ -3500,30 +3500,30 @@ 1372063500000,0.10950 1372063800000,0.11150 1372064100000,0.11150 -1372064400000,0.13.00 +1372064400000,0.14.00 1372064700000,0.11350 -1372065000000,0.13.00 +1372065000000,0.14.00 1372065300000,0.11250 -1372065600000,0.13.00 -1372065900000,0.13.00 -1372066200000,0.13.00 -1372066500000,0.13.00 +1372065600000,0.14.00 +1372065900000,0.14.00 +1372066200000,0.14.00 +1372066500000,0.14.00 1372066800000,0.11650 -1372067100000,0.13.00 +1372067100000,0.14.00 1372067400000,0.11450 1372067700000,0.11150 -1372068000000,0.13.00 -1372068300000,0.13.00 +1372068000000,0.14.00 +1372068300000,0.14.00 1372068600000,0.10950 1372068900000,0.10850 -1372069200000,0.13.00 +1372069200000,0.14.00 1372069500000,0.10900 1372069800000,0.10850 1372070100000,0.11050 1372070400000,0.11250 1372070700000,0.11150 1372071000000,0.10900 -1372071300000,0.10750 +13720.14.0000,0.10750 1372071600000,0.10600 1372071900000,0.10200 1372072200000,0.10150 @@ -3589,7 +3589,7 @@ 1372090200000,0.08600 1372090500000,0.08700 1372090800000,0.08500 -13720.13.0000,0.08350 +13720.14.0000,0.08350 1372091400000,0.08250 1372091700000,0.08350 1372092000000,0.08550 @@ -3794,8 +3794,8 @@ 1372151700000,0.10900 1372152000000,0.10950 1372152300000,0.10900 -1372152600000,0.13.00 -1372152900000,0.13.00 +1372152600000,0.14.00 +1372152900000,0.14.00 1372153200000,0.11350 1372153500000,0.11150 1372153800000,0.10800 @@ -4082,7 +4082,7 @@ 1372238100000,0.11250 1372238400000,0.11150 1372238700000,0.10950 -1372239000000,0.13.00 +1372239000000,0.14.00 1372239300000,0.10900 1372239600000,0.10650 1372239900000,0.10450 @@ -4348,33 +4348,33 @@ 1372317900000,0.10550 1372318200000,0.10550 1372318500000,0.10950 -1372318800000,0.13.00 -1372319100000,0.13.00 +1372318800000,0.14.00 +1372319100000,0.14.00 1372319400000,0.11350 -1372319700000,0.13.00 -1372320000000,0.13.00 +1372319700000,0.14.00 +1372320000000,0.14.00 1372320300000,0.10700 1372320600000,0.10700 1372320900000,0.10900 1372321200000,0.10850 -1372321500000,0.13.00 -1372321800000,0.13.00 +1372321500000,0.14.00 +1372321800000,0.14.00 1372322100000,0.11150 -1372322400000,0.13.00 +1372322400000,0.14.00 1372322700000,0.11450 1372323000000,0.11350 -1372323300000,0.13.00 -1372323600000,0.13.00 -1372323900000,0.13.00 -1372324200000,0.13.00 +1372323300000,0.14.00 +1372323600000,0.14.00 +1372323900000,0.14.00 +1372324200000,0.14.00 1372324500000,0.11050 -1372324800000,0.13.00 +1372324800000,0.14.00 1372325100000,0.11050 1372325400000,0.10850 1372325700000,0.10650 1372326000000,0.10700 1372326300000,0.11050 -1372326600000,0.13.00 +1372326600000,0.14.00 1372326900000,0.11250 1372327200000,0.11250 1372327500000,0.10700 @@ -4631,10 +4631,10 @@ 1372402800000,0.08650 1372403100000,0.10700 1372403400000,0.11850 -1372403700000,0.13100 -1372404000000,0.13800 -1372404300000,0.13800 -1372404600000,0.13700 +1372403700000,0.14.00 +1372404000000,0.14.00 +1372404300000,0.14.00 +1372404600000,0.14.00 1372404900000,0.13750 1372405200000,0.14050 1372405500000,0.14500 @@ -4659,9 +4659,9 @@ 1372411200000,0.14450 1372411500000,0.14000 1372411800000,0.13550 -1372412100000,0.13500 +1372412100000,0.14.00 1372412400000,0.13250 -1372412700000,0.13000 +1372412700000,0.14.00 1372413000000,0.12650 1372413300000,0.12600 1372413600000,0.12600 @@ -4702,13 +4702,13 @@ 1372424100000,0.12100 1372424400000,0.12100 1372424700000,0.11850 -1372425000000,0.13.00 -1372425300000,0.13.00 +1372425000000,0.14.00 +1372425300000,0.14.00 1372425600000,0.12050 1372425900000,0.12150 1372426200000,0.11750 -1372426500000,0.13.00 -1372426800000,0.13.00 +1372426500000,0.14.00 +1372426800000,0.14.00 1372427100000,0.10900 1372427400000,0.10950 1372427700000,0.10600 @@ -4845,33 +4845,33 @@ 1372467000000,0.10800 1372467300000,0.10950 1372467600000,0.10950 -1372467900000,0.13.00 +1372467900000,0.14.00 1372468200000,0.11750 1372468500000,0.11550 -1372468800000,0.13.00 +1372468800000,0.14.00 1372469100000,0.11750 -1372469400000,0.13.00 -1372469700000,0.13.00 -1372470000000,0.13.00 -1372470300000,0.13.00 -1372470600000,0.13.00 +1372469400000,0.14.00 +1372469700000,0.14.00 +1372470000000,0.14.00 +1372470300000,0.14.00 +1372470600000,0.14.00 1372470900000,0.11950 1372471200000,0.11950 -1372471500000,0.13.00 +1372471500000,0.14.00 1372471800000,0.11550 1372472100000,0.11450 -1372472400000,0.13.00 +1372472400000,0.14.00 1372472700000,0.11450 1372473000000,0.11450 1372473300000,0.11450 -1372473600000,0.13.00 +1372473600000,0.14.00 1372473900000,0.11250 1372474200000,0.11250 -1372474500000,0.13.00 -1372474800000,0.13.00 +1372474500000,0.14.00 +1372474800000,0.14.00 1372475100000,0.13050 -1372475400000,0.13600 -1372475700000,0.13800 +1372475400000,0.14.00 +1372475700000,0.14.00 1372476000000,0.14350 1372476300000,0.15050 1372476600000,0.15900 @@ -4907,16 +4907,16 @@ 1372485600000,0.15750 1372485900000,0.14850 1372486200000,0.14400 -1372486500000,0.13900 -1372486800000,0.13600 -1372487100000,0.13200 +1372486500000,0.14.00 +1372486800000,0.14.00 +1372487100000,0.14.00 1372487400000,0.12750 1372487700000,0.12300 1372488000000,0.11850 1372488300000,0.11450 1372488600000,0.11250 -1372488900000,0.13.00 -1372489200000,0.13.00 +1372488900000,0.14.00 +1372489200000,0.14.00 1372489500000,0.10450 1372489800000,0.10350 1372490100000,0.10000 @@ -4951,52 +4951,52 @@ 1372498800000,0.10450 1372499100000,0.10550 1372499400000,0.10850 -1372499700000,0.13.00 -1372500000000,0.13.00 -1372500300000,0.13.00 -1372500600000,0.13.00 -1372500900000,0.13.00 -1372501200000,0.13.00 +1372499700000,0.14.00 +1372500000000,0.14.00 +1372500300000,0.14.00 +1372500600000,0.14.00 +1372500900000,0.14.00 +1372501200000,0.14.00 1372501500000,0.11350 1372501800000,0.11550 -1372502100000,0.13.00 +1372502100000,0.14.00 1372502400000,0.11650 1372502700000,0.11750 1372503000000,0.11750 -1372503300000,0.13.00 -1372503600000,0.13.00 +1372503300000,0.14.00 +1372503600000,0.14.00 1372503900000,0.11550 1372504200000,0.11650 -1372504500000,0.13.00 -1372504800000,0.13.00 +1372504500000,0.14.00 +1372504800000,0.14.00 1372505100000,0.11650 1372505400000,0.11850 1372505700000,0.12050 -1372506000000,0.13.00 -1372506300000,0.13.00 -1372506600000,0.13.00 +1372506000000,0.14.00 +1372506300000,0.14.00 +1372506600000,0.14.00 1372506900000,0.11450 1372507200000,0.11450 -1372507500000,0.13.00 -1372507800000,0.13.00 +1372507500000,0.14.00 +1372507800000,0.14.00 1372508100000,0.11450 1372508400000,0.11450 -1372508700000,0.13.00 -1372509000000,0.13.00 -1372509300000,0.13.00 +1372508700000,0.14.00 +1372509000000,0.14.00 +1372509300000,0.14.00 1372509600000,0.11850 1372509900000,0.12050 -1372510200000,0.13.00 -1372510500000,0.13.00 -1372510800000,0.13.00 +1372510200000,0.14.00 +1372510500000,0.14.00 +1372510800000,0.14.00 1372511100000,0.11150 1372511400000,0.10950 -1372511700000,0.13.00 +1372511700000,0.14.00 1372512000000,0.10900 -1372512300000,0.13.00 +1372512300000,0.14.00 1372512600000,0.11150 -1372512900000,0.13.00 -1372513200000,0.13.00 +1372512900000,0.14.00 +1372513200000,0.14.00 1372513500000,0.10900 1372513800000,0.10900 1372514100000,0.10850 @@ -5248,62 +5248,62 @@ 1372587900000,0.10900 1372588200000,0.10950 1372588500000,0.11050 -1372588800000,0.13.00 +1372588800000,0.14.00 1372589100000,0.11250 1372589400000,0.11450 -1372589700000,0.13.00 -1372590000000,0.13.00 +1372589700000,0.14.00 +1372590000000,0.14.00 1372590300000,0.11750 -1372590600000,0.13.00 +1372590600000,0.14.00 1372590900000,0.11850 -1372591200000,0.13.00 -1372591500000,0.13.00 +1372591200000,0.14.00 +1372591500000,0.14.00 1372591800000,0.11650 -1372592100000,0.13.00 +1372592100000,0.14.00 1372592400000,0.12000 1372592700000,0.12250 1372593000000,0.12000 1372593300000,0.12000 1372593600000,0.12000 -1372593900000,0.13.00 +1372593900000,0.14.00 1372594200000,0.11850 1372594500000,0.11850 -1372594800000,0.13.00 -1372595100000,0.13.00 +1372594800000,0.14.00 +1372595100000,0.14.00 1372595400000,0.11850 -1372595700000,0.13.00 +1372595700000,0.14.00 1372596000000,0.11450 1372596300000,0.11450 -1372596600000,0.13.00 +1372596600000,0.14.00 1372596900000,0.11650 1372597200000,0.11650 1372597500000,0.11750 1372597800000,0.12050 1372598100000,0.11450 -1372598400000,0.13.00 -1372598700000,0.13.00 -1372599000000,0.13.00 -1372599300000,0.13.00 -1372599600000,0.13.00 -1372599900000,0.13.00 -1372600200000,0.13.00 +1372598400000,0.14.00 +1372598700000,0.14.00 +1372599000000,0.14.00 +1372599300000,0.14.00 +1372599600000,0.14.00 +1372599900000,0.14.00 +1372600200000,0.14.00 1372600500000,0.11450 1372600800000,0.11450 1372601100000,0.11450 -1372601400000,0.13.00 -1372601700000,0.13.00 -1372602000000,0.13.00 -1372602300000,0.13.00 +1372601400000,0.14.00 +1372601700000,0.14.00 +1372602000000,0.14.00 +1372602300000,0.14.00 1372602600000,0.11250 -1372602900000,0.13.00 -1372603200000,0.13.00 -1372603500000,0.13.00 +1372602900000,0.14.00 +1372603200000,0.14.00 +1372603500000,0.14.00 1372603800000,0.10850 1372604100000,0.10500 1372604400000,0.10500 1372604700000,0.10650 -1372605000000,0.13.00 -1372605300000,0.13.00 +1372605000000,0.14.00 +1372605300000,0.14.00 1372605600000,0.10800 1372605900000,0.10450 1372606200000,0.10250 @@ -5506,4 +5506,4 @@ 1372665300000,0.09950 1372665600000,0.09950 1372665900000,0.09950 -1372666200000,0.13.00 +1372666200000,0.14.00 diff --git a/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data2.txt b/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data2.txt index 8c6769d81..ed4c719a6 100644 --- a/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data2.txt +++ b/logisland-plugins/logisland-sampling-plugin/src/test/resources/data/raw-data2.txt @@ -75,7 +75,7 @@ 1370551800000,0.10000 1370552100000,0.10400 1370552400000,0.11250 -1370552700000,0.13.00 +1370552700000,0.14.00 1370553000000,0.11450 1370553300000,0.10950 1370553600000,0.10500 @@ -209,14 +209,14 @@ 1370592000000,0.10200 1370592300000,0.10300 1370592600000,0.10950 -1370592900000,0.13.00 -1370593200000,0.13.00 -1370593500000,0.13.00 +1370592900000,0.14.00 +1370593200000,0.14.00 +1370593500000,0.14.00 1370593800000,0.11350 1370594100000,0.11350 -1370594400000,0.13.00 +1370594400000,0.14.00 1370594700000,0.11750 -1370595000000,0.13.00 +1370595000000,0.14.00 1370595300000,0.11850 1370595600000,0.11850 1370595900000,0.12100 @@ -228,26 +228,26 @@ 1370597700000,0.12400 1370598000000,0.12350 1370598300000,0.12050 -1370598600000,0.13.00 +1370598600000,0.14.00 1370598900000,0.11550 1370599200000,0.11550 -1370599500000,0.13.00 -1370599800000,0.13.00 -1370600100000,0.13.00 -1370600400000,0.13.00 -1370600700000,0.13.00 -1370601000000,0.13.00 +1370599500000,0.14.00 +1370599800000,0.14.00 +1370600100000,0.14.00 +1370600400000,0.14.00 +1370600700000,0.14.00 +1370601000000,0.14.00 1370601300000,0.11250 1370601600000,0.11250 -1370601900000,0.13.00 +1370601900000,0.14.00 1370602200000,0.11350 1370602500000,0.11750 -1370602800000,0.13.00 +1370602800000,0.14.00 1370603100000,0.11250 1370603400000,0.10900 1370603700000,0.10800 1370604000000,0.11050 -1370604300000,0.13.00 +1370604300000,0.14.00 1370604600000,0.10900 1370604900000,0.10900 1370605200000,0.10600 @@ -270,16 +270,16 @@ 1370610300000,0.09150 1370610600000,0.09200 1370610900000,0.09150 -1370.13.00000,0.09500 -1370.13.00000,0.09550 -1370.13.00000,0.09300 +1370.14.00000,0.09500 +1370.14.00000,0.09550 +1370.14.00000,0.09300 1370612100000,0.09200 1370612400000,0.09250 1370612700000,0.09400 -1370613000000,0.09450 -1370613300000,0.09700 -1370613600000,0.09600 -1370613900000,0.09500 +1370.14.00000,0.09450 +1370.14.00000,0.09700 +1370.14.00000,0.09600 +1370.14.00000,0.09500 1370614200000,0.09650 1370614500000,0.09700 1370614800000,0.09650 @@ -514,51 +514,51 @@ 1370683500000,0.10200 1370683800000,0.10250 1370684100000,0.10800 -1370684400000,0.13.00 +1370684400000,0.14.00 1370684700000,0.11950 1370685000000,0.12350 1370685300000,0.12450 1370685600000,0.12150 -1370685900000,0.13.00 -1370686200000,0.13.00 +1370685900000,0.14.00 +1370686200000,0.14.00 1370686500000,0.11750 1370686800000,0.11550 1370687100000,0.11750 1370687400000,0.11750 1370687700000,0.11550 -1370688000000,0.13.00 -1370688300000,0.13.00 +1370688000000,0.14.00 +1370688300000,0.14.00 1370688600000,0.11750 1370688900000,0.11750 1370689200000,0.11650 -1370689500000,0.13.00 -1370689800000,0.13.00 +1370689500000,0.14.00 +1370689800000,0.14.00 1370690100000,0.11750 -1370690400000,0.13.00 -1370690700000,0.13.00 +1370690400000,0.14.00 +1370690700000,0.14.00 1370691000000,0.11850 1370691300000,0.11750 1370691600000,0.11750 -1370691900000,0.13.00 +1370691900000,0.14.00 1370692200000,0.11550 -1370692500000,0.13.00 +1370692500000,0.14.00 1370692800000,0.11450 -1370693100000,0.13.00 -1370693400000,0.13.00 -1370693700000,0.13.00 -1370694000000,0.13.00 +1370693100000,0.14.00 +1370693400000,0.14.00 +1370693700000,0.14.00 +1370694000000,0.14.00 1370694300000,0.11550 -1370694600000,0.13.00 +1370694600000,0.14.00 1370694900000,0.11250 -1370695200000,0.13.00 +1370695200000,0.14.00 1370695500000,0.11650 -1370695800000,0.13.00 +1370695800000,0.14.00 1370696100000,0.11250 1370696400000,0.11150 1370696700000,0.11150 -1370697000000,0.13.00 +1370697000000,0.14.00 1370697300000,0.11050 -1370697600000,0.13.00 +1370697600000,0.14.00 1370697900000,0.10850 1370698200000,0.10750 1370698500000,0.10500 @@ -603,16 +603,16 @@ 1370710200000,0.09600 1370710500000,0.09850 1370710800000,0.09800 -1370.13.00000,0.09550 -1370.13.00000,0.09550 -1370.13.00000,0.09500 +1370.14.00000,0.09550 +1370.14.00000,0.09550 +1370.14.00000,0.09500 1370712000000,0.09550 1370712300000,0.09550 1370712600000,0.09800 1370712900000,0.09750 -1370713200000,0.09800 -1370713500000,0.09900 -1370713800000,0.10250 +1370.14.00000,0.09800 +1370.14.00000,0.09900 +1370.14.00000,0.10250 1370714100000,0.10150 1370714400000,0.10150 1370714700000,0.09850 @@ -620,8 +620,8 @@ 1370715300000,0.10150 1370715600000,0.10450 1370715900000,0.10800 -1370716200000,0.13.00 -1370716500000,0.13.00 +1370716200000,0.14.00 +1370716500000,0.14.00 1370716800000,0.11450 1370717100000,0.11150 1370717400000,0.11050 @@ -765,9 +765,9 @@ 1370758800000,0.06500 1370759100000,0.07500 1370759400000,0.10850 -1370759700000,0.13.00 -1370760000000,0.13.00 -1370760300000,0.13.00 +1370759700000,0.14.00 +1370760000000,0.14.00 +1370760300000,0.14.00 1370760600000,0.10950 1370760900000,0.10750 1370761200000,0.10450 @@ -801,11 +801,11 @@ 1370769600000,0.09250 1370769900000,0.09950 1370770200000,0.10750 -1370770500000,0.13.00 +1370770500000,0.14.00 1370770800000,0.12450 1370771100000,0.12950 1370771400000,0.13450 -1370771700000,0.13800 +1370771700000,0.14.00 1370772000000,0.14350 1370772300000,0.14700 1370772600000,0.14700 @@ -813,10 +813,10 @@ 1370773200000,0.14150 1370773500000,0.14200 1370773800000,0.14100 -1370774100000,0.13900 +1370774100000,0.14.00 1370774400000,0.13850 1370774700000,0.13750 -1370775000000,0.13700 +1370775000000,0.14.00 1370775300000,0.13450 1370775600000,0.13250 1370775900000,0.14100 @@ -827,7 +827,7 @@ 1370777400000,0.12200 1370777700000,0.12100 1370778000000,0.11850 -1370778300000,0.13.00 +1370778300000,0.14.00 1370778600000,0.12050 1370778900000,0.12050 1370779200000,0.12100 @@ -842,39 +842,39 @@ 1370781900000,0.12650 1370782200000,0.12300 1370782500000,0.11950 -1370782800000,0.13.00 +1370782800000,0.14.00 1370783100000,0.11750 1370783400000,0.11850 1370783700000,0.11750 -1370784000000,0.13.00 +1370784000000,0.14.00 1370784300000,0.11450 -1370784600000,0.13.00 +1370784600000,0.14.00 1370784900000,0.11650 1370785200000,0.11750 1370785500000,0.11750 1370785800000,0.12100 -1370786100000,0.13.00 -1370786400000,0.13.00 -1370786700000,0.13.00 -1370787000000,0.13.00 +1370786100000,0.14.00 +1370786400000,0.14.00 +1370786700000,0.14.00 +1370787000000,0.14.00 1370787300000,0.11150 -1370787600000,0.13.00 -1370787900000,0.13.00 -1370788200000,0.13.00 -1370788500000,0.13.00 -1370788800000,0.13.00 +1370787600000,0.14.00 +1370787900000,0.14.00 +1370788200000,0.14.00 +1370788500000,0.14.00 +1370788800000,0.14.00 1370789100000,0.11550 1370789400000,0.11450 -1370789700000,0.13.00 +1370789700000,0.14.00 1370790000000,0.11450 1370790300000,0.11450 1370790600000,0.11350 -1370790900000,0.13.00 -1370791200000,0.13.00 +1370790900000,0.14.00 +1370791200000,0.14.00 1370791500000,0.11150 -1370791800000,0.13.00 +1370791800000,0.14.00 1370792100000,0.11250 -1370792400000,0.13.00 +1370792400000,0.14.00 1370792700000,0.10850 1370793000000,0.10750 1370793300000,0.10500 @@ -931,21 +931,21 @@ 1370808600000,0.11050 1370808900000,0.10950 1370809200000,0.11050 -1370809500000,0.13.00 +1370809500000,0.14.00 1370809800000,0.10900 1370810100000,0.10600 1370810400000,0.10550 1370810700000,0.10500 -1370.13.00000,0.10600 -1370.13.00000,0.10850 -1370.13.00000,0.10700 -1370.13.00000,0.10750 +1370.14.00000,0.10600 +1370.14.00000,0.10850 +1370.14.00000,0.10700 +1370.14.00000,0.10750 1370812200000,0.10800 1370812500000,0.10750 1370812800000,0.10500 -1370813100000,0.10450 -1370813400000,0.10600 -1370813700000,0.10350 +1370.14.00000,0.10450 +1370.14.00000,0.10600 +1370.14.00000,0.10350 1370814000000,0.10200 1370814300000,0.10100 1370814600000,0.10050 @@ -1065,51 +1065,51 @@ 1370848800000,0.09600 1370849100000,0.09850 1370849400000,0.10100 -1370849700000,0.13.00 -1370850000000,0.13.00 +1370849700000,0.14.00 +1370850000000,0.14.00 1370850300000,0.11950 -1370850600000,0.13.00 -1370850900000,0.13.00 +1370850600000,0.14.00 +1370850900000,0.14.00 1370851200000,0.10900 1370851500000,0.10550 1370851800000,0.10550 1370852100000,0.10800 -1370852400000,0.13.00 -1370852700000,0.13.00 +1370852400000,0.14.00 +1370852700000,0.14.00 1370853000000,0.11850 1370853300000,0.11950 1370853600000,0.11750 1370853900000,0.11650 -1370854200000,0.13.00 +1370854200000,0.14.00 1370854500000,0.11650 -1370854800000,0.13.00 +1370854800000,0.14.00 1370855100000,0.11650 -1370855400000,0.13.00 +1370855400000,0.14.00 1370855700000,0.11550 -1370856000000,0.13.00 -1370856300000,0.13.00 +1370856000000,0.14.00 +1370856300000,0.14.00 1370856600000,0.11550 1370856900000,0.11750 1370857200000,0.11750 1370857500000,0.11850 1370857800000,0.12100 1370858100000,0.12050 -1370858400000,0.13.00 -1370858700000,0.13.00 -1370859000000,0.13.00 -1370859300000,0.13.00 +1370858400000,0.14.00 +1370858700000,0.14.00 +1370859000000,0.14.00 +1370859300000,0.14.00 1370859600000,0.11350 -1370859900000,0.13.00 +1370859900000,0.14.00 1370860200000,0.11250 -1370860500000,0.13.00 +1370860500000,0.14.00 1370860800000,0.10900 1370861100000,0.10850 1370861400000,0.11150 1370861700000,0.11050 -1370862000000,0.13.00 +1370862000000,0.14.00 1370862300000,0.10900 -1370862600000,0.13.00 -1370862900000,0.13.00 +1370862600000,0.14.00 +1370862900000,0.14.00 1370863200000,0.10850 1370863500000,0.10900 1370863800000,0.10750 @@ -1119,7 +1119,7 @@ 1370865000000,0.10100 1370865300000,0.10700 1370865600000,0.11050 -1370865900000,0.13.00 +1370865900000,0.14.00 1370866200000,0.10700 1370866500000,0.10650 1370866800000,0.10500 @@ -1270,16 +1270,16 @@ 1370910300000,0.08650 1370910600000,0.08450 1370910900000,0.08450 -1370.13.00000,0.08450 -1370.13.00000,0.08350 -1370.13.00000,0.08250 +1370.14.00000,0.08450 +1370.14.00000,0.08350 +1370.14.00000,0.08250 1370912100000,0.08100 1370912400000,0.08000 1370912700000,0.07800 -1370913000000,0.07700 -1370913300000,0.07700 -1370913600000,0.07700 -1370913900000,0.07550 +1370.14.00000,0.07700 +1370.14.00000,0.07700 +1370.14.00000,0.07700 +1370.14.00000,0.07550 1370914200000,0.07300 1370914500000,0.07150 1370914800000,0.07000 @@ -1360,32 +1360,32 @@ 1370937300000,0.10200 1370937600000,0.10800 1370937900000,0.11050 -1370938200000,0.13.00 +1370938200000,0.14.00 1370938500000,0.11350 1370938800000,0.11550 -1370939100000,0.13.00 -1370939400000,0.13.00 +1370939100000,0.14.00 +1370939400000,0.14.00 1370939700000,0.11750 -1370940000000,0.13.00 +1370940000000,0.14.00 1370940300000,0.12000 1370940600000,0.11850 1370940900000,0.11650 -1370941200000,0.13.00 -1370941500000,0.13.00 -1370941800000,0.13.00 +1370941200000,0.14.00 +1370941500000,0.14.00 +1370941800000,0.14.00 1370942100000,0.11350 1370942400000,0.11250 1370942700000,0.11050 -1370943000000,0.13.00 +1370943000000,0.14.00 1370943300000,0.11150 -1370943600000,0.13.00 +1370943600000,0.14.00 1370943900000,0.11450 1370944200000,0.11550 1370944500000,0.11550 -1370944800000,0.13.00 +1370944800000,0.14.00 1370945100000,0.11350 -1370945400000,0.13.00 -1370945700000,0.13.00 +1370945400000,0.14.00 +1370945700000,0.14.00 1370946000000,0.11150 1370946300000,0.10950 1370946600000,0.10750 @@ -1395,10 +1395,10 @@ 1370947800000,0.10800 1370948100000,0.10850 1370948400000,0.10800 -1370948700000,0.13.00 -1370949000000,0.13.00 +1370948700000,0.14.00 +1370949000000,0.14.00 1370949300000,0.11050 -1370949600000,0.13.00 +1370949600000,0.14.00 1370949900000,0.10750 1370950200000,0.10550 1370950500000,0.10550 @@ -1603,7 +1603,7 @@ 1371010200000,0.05100 1371010500000,0.04800 1371010800000,0.04700 -13710.13.0000,0.04850 +13710.14.0000,0.04850 1371011400000,0.04900 1371011700000,0.04800 1371012000000,0.04750 @@ -1637,7 +1637,7 @@ 1371020400000,0.08350 1371020700000,0.08600 1371021000000,0.08700 -1371021300000,0.08900 +13710.14.0000,0.08900 1371021600000,0.09650 1371021900000,0.09600 1371022200000,0.09400 @@ -1649,34 +1649,34 @@ 1371024000000,0.10550 1371024300000,0.10700 1371024600000,0.10850 -1371024900000,0.13.00 +1371024900000,0.14.00 1371025200000,0.11550 1371025500000,0.11950 1371025800000,0.12150 -1371026100000,0.13.00 +1371026100000,0.14.00 1371026400000,0.11750 1371026700000,0.11750 -1371027000000,0.13.00 +1371027000000,0.14.00 1371027300000,0.11750 -1371027600000,0.13.00 +1371027600000,0.14.00 1371027900000,0.11650 -1371028200000,0.13.00 -1371028500000,0.13.00 +1371028200000,0.14.00 +1371028500000,0.14.00 1371028800000,0.11950 -1371029100000,0.13.00 -1371029400000,0.13.00 +1371029100000,0.14.00 +1371029400000,0.14.00 1371029700000,0.11850 -1371030000000,0.13.00 -1371030300000,0.13.00 -1371030600000,0.13.00 -1371030900000,0.13.00 +1371030000000,0.14.00 +1371030300000,0.14.00 +1371030600000,0.14.00 +1371030900000,0.14.00 1371031200000,0.11750 -1371031500000,0.13.00 -1371031800000,0.13.00 -1371032100000,0.13.00 -1371032400000,0.13.00 +1371031500000,0.14.00 +1371031800000,0.14.00 +1371032100000,0.14.00 +1371032400000,0.14.00 1371032700000,0.11550 -1371033000000,0.13.00 +1371033000000,0.14.00 1371033300000,0.11250 1371033600000,0.10900 1371033900000,0.10500 @@ -1703,7 +1703,7 @@ 1371040200000,0.09900 1371040500000,0.09750 1371040800000,0.09700 -13710.13.0000,0.09700 +13710.14.0000,0.09700 1371041400000,0.09650 1371041700000,0.09800 1371042000000,0.09550 @@ -1737,7 +1737,7 @@ 1371050400000,0.09550 1371050700000,0.09350 1371051000000,0.09350 -1371051300000,0.09350 +13710.14.0000,0.09350 1371051600000,0.09050 1371051900000,0.09100 1371052200000,0.08950 @@ -1803,7 +1803,7 @@ 1371070200000,0.10050 1371070500000,0.09900 1371070800000,0.09850 -13710.13.0000,0.09800 +13710.14.0000,0.09800 1371071400000,0.09800 1371071700000,0.09600 1371072000000,0.09450 @@ -1837,7 +1837,7 @@ 1371080400000,0.09850 1371080700000,0.10000 1371081000000,0.09900 -1371081300000,0.09600 +13710.14.0000,0.09600 1371081600000,0.09250 1371081900000,0.08950 1371082200000,0.08750 @@ -1931,42 +1931,42 @@ 1371108600000,0.10450 1371108900000,0.10750 1371109200000,0.10850 -1371109500000,0.13.00 -1371109800000,0.13.00 +1371109500000,0.14.00 +1371109800000,0.14.00 1371110100000,0.10800 1371110400000,0.10700 1371110700000,0.11050 1371111000000,0.11250 -1371111300000,0.13.00 +1371111300000,0.14.00 1371111600000,0.11050 -1371111900000,0.13.00 -1371112200000,0.13.00 +1371111900000,0.14.00 +1371112200000,0.14.00 1371112500000,0.12150 1371112800000,0.12000 1371113100000,0.11950 -1371113400000,0.13.00 -1371113700000,0.13.00 +1371113400000,0.14.00 +1371113700000,0.14.00 1371114000000,0.11850 1371114300000,0.12150 1371114600000,0.12150 1371114900000,0.12050 1371115200000,0.11950 -1371115500000,0.13.00 -1371115800000,0.13.00 +1371115500000,0.14.00 +1371115800000,0.14.00 1371116100000,0.11450 -1371116400000,0.13.00 +1371116400000,0.14.00 1371116700000,0.11350 -1371117000000,0.13.00 +1371117000000,0.14.00 1371117300000,0.11150 1371117600000,0.11350 1371117900000,0.11650 1371118200000,0.11550 1371118500000,0.11450 -1371118800000,0.13.00 -1371119100000,0.13.00 +1371118800000,0.14.00 +1371119100000,0.14.00 1371119400000,0.11350 1371119700000,0.11350 -1371120000000,0.13.00 +1371120000000,0.14.00 1371120300000,0.10900 1371120600000,0.10950 1371120900000,0.10950 @@ -1978,8 +1978,8 @@ 1371122700000,0.10500 1371123000000,0.10500 1371123300000,0.10800 -1371123600000,0.13.00 -1371123900000,0.13.00 +1371123600000,0.14.00 +1371123900000,0.14.00 1371124200000,0.10950 1371124500000,0.10650 1371124800000,0.10200 @@ -2052,17 +2052,17 @@ 1371144900000,0.11050 1371145200000,0.11250 1371145500000,0.11250 -1371145800000,0.13.00 +1371145800000,0.14.00 1371146100000,0.11250 1371146400000,0.11150 1371146700000,0.11250 -1371147000000,0.13.00 +1371147000000,0.14.00 1371147300000,0.11250 1371147600000,0.11550 -1371147900000,0.13.00 +1371147900000,0.14.00 1371148200000,0.12050 1371148500000,0.12750 -1371148800000,0.13500 +1371148800000,0.14.00 1371149100000,0.13950 1371149400000,0.14250 1371149700000,0.14400 @@ -2102,15 +2102,15 @@ 1371159900000,0.14200 1371160200000,0.14050 1371160500000,0.13250 -1371160800000,0.13000 +1371160800000,0.14.00 1371161100000,0.12600 1371161400000,0.12200 1371161700000,0.11950 1371162000000,0.11850 1371162300000,0.11850 1371162600000,0.11350 -1371162900000,0.13.00 -1371163200000,0.13.00 +1371162900000,0.14.00 +1371163200000,0.14.00 1371163500000,0.11050 1371163800000,0.10850 1371164100000,0.10650 @@ -2225,28 +2225,28 @@ 1371196800000,0.10650 1371197100000,0.10550 1371197400000,0.10900 -1371197700000,0.13.00 +1371197700000,0.14.00 1371198000000,0.11450 -1371198300000,0.13.00 +1371198300000,0.14.00 1371198600000,0.11050 1371198900000,0.11050 1371199200000,0.10900 -1371199500000,0.13.00 -1371199800000,0.13.00 -1371200100000,0.13.00 -1371200400000,0.13.00 +1371199500000,0.14.00 +1371199800000,0.14.00 +1371200100000,0.14.00 +1371200400000,0.14.00 1371200700000,0.11150 1371201000000,0.11550 -1371201300000,0.13.00 +1371201300000,0.14.00 1371201600000,0.11550 1371201900000,0.11550 1371202200000,0.11450 1371202500000,0.11550 -1371202800000,0.13.00 +1371202800000,0.14.00 1371203100000,0.11350 -1371203400000,0.13.00 -1371203700000,0.13.00 -1371204000000,0.13.00 +1371203400000,0.14.00 +1371203700000,0.14.00 +1371204000000,0.14.00 1371204300000,0.11150 1371204600000,0.11250 1371204900000,0.11250 @@ -2526,30 +2526,30 @@ 1371287100000,0.10350 1371287400000,0.10700 1371287700000,0.10950 -1371288000000,0.13.00 -1371288300000,0.13.00 -1371288600000,0.13.00 +1371288000000,0.14.00 +1371288300000,0.14.00 +1371288600000,0.14.00 1371288900000,0.11550 -1371289200000,0.13.00 -1371289500000,0.13.00 -1371289800000,0.13.00 +1371289200000,0.14.00 +1371289500000,0.14.00 +1371289800000,0.14.00 1371290100000,0.11050 1371290400000,0.11350 -1371290700000,0.13.00 -1371291000000,0.13.00 +1371290700000,0.14.00 +1371291000000,0.14.00 1371291300000,0.11650 -1371291600000,0.13.00 +1371291600000,0.14.00 1371291900000,0.11650 -1371292200000,0.13.00 -1371292500000,0.13.00 +1371292200000,0.14.00 +1371292500000,0.14.00 1371292800000,0.11650 -1371293100000,0.13.00 +1371293100000,0.14.00 1371293400000,0.11850 -1371293700000,0.13.00 +1371293700000,0.14.00 1371294000000,0.11850 -1371294300000,0.13.00 +1371294300000,0.14.00 1371294600000,0.11750 -1371294900000,0.13.00 +1371294900000,0.14.00 1371295200000,0.12000 1371295500000,0.12150 1371295800000,0.12000 @@ -2558,28 +2558,28 @@ 1371296700000,0.11950 1371297000000,0.12100 1371297300000,0.11950 -1371297600000,0.13.00 +1371297600000,0.14.00 1371297900000,0.11350 -1371298200000,0.13.00 +1371298200000,0.14.00 1371298500000,0.11150 -1371298800000,0.13.00 -1371299100000,0.13.00 -1371299400000,0.13.00 -1371299700000,0.13.00 +1371298800000,0.14.00 +1371299100000,0.14.00 +1371299400000,0.14.00 +1371299700000,0.14.00 1371300000000,0.11350 1371300300000,0.11750 -1371300600000,0.13.00 -1371300900000,0.13.00 -1371301200000,0.13.00 -1371301500000,0.13.00 +1371300600000,0.14.00 +1371300900000,0.14.00 +1371301200000,0.14.00 +1371301500000,0.14.00 1371301800000,0.11450 -1371302100000,0.13.00 +1371302100000,0.14.00 1371302400000,0.11150 -1371302700000,0.13.00 +1371302700000,0.14.00 1371303000000,0.11250 -1371303300000,0.13.00 +1371303300000,0.14.00 1371303600000,0.11050 -1371303900000,0.13.00 +1371303900000,0.14.00 1371304200000,0.11050 1371304500000,0.11050 1371304800000,0.10950 @@ -2594,7 +2594,7 @@ 1371307500000,0.10500 1371307800000,0.10900 1371308100000,0.11150 -1371308400000,0.13.00 +1371308400000,0.14.00 1371308700000,0.10900 1371309000000,0.10650 1371309300000,0.10400 @@ -2830,55 +2830,55 @@ 1371378300000,0.10200 1371378600000,0.10600 1371378900000,0.10900 -1371379200000,0.13.00 -1371379500000,0.13.00 -1371379800000,0.13.00 -1371380100000,0.13.00 -1371380400000,0.13.00 -1371380700000,0.13.00 +1371379200000,0.14.00 +1371379500000,0.14.00 +1371379800000,0.14.00 +1371380100000,0.14.00 +1371380400000,0.14.00 +1371380700000,0.14.00 1371381000000,0.11750 -1371381300000,0.13.00 -1371381600000,0.13.00 -1371381900000,0.13.00 -1371382200000,0.13.00 +1371381300000,0.14.00 +1371381600000,0.14.00 +1371381900000,0.14.00 +1371382200000,0.14.00 1371382500000,0.11650 -1371382800000,0.13.00 -1371383100000,0.13.00 +1371382800000,0.14.00 +1371383100000,0.14.00 1371383400000,0.11450 -1371383700000,0.13.00 -1371384000000,0.13.00 +1371383700000,0.14.00 +1371384000000,0.14.00 1371384300000,0.11250 1371384600000,0.11650 1371384900000,0.11750 1371385200000,0.11750 1371385500000,0.11650 -1371385800000,0.13.00 +1371385800000,0.14.00 1371386100000,0.11650 1371386400000,0.12050 1371386700000,0.12300 1371387000000,0.12200 1371387300000,0.12050 1371387600000,0.12000 -1371387900000,0.13.00 -1371388200000,0.13.00 -1371388500000,0.13.00 +1371387900000,0.14.00 +1371388200000,0.14.00 +1371388500000,0.14.00 1371388800000,0.11450 1371389100000,0.11350 1371389400000,0.11450 1371389700000,0.11550 1371390000000,0.11750 1371390300000,0.11650 -1371390600000,0.13.00 -1371390900000,0.13.00 +1371390600000,0.14.00 +1371390900000,0.14.00 1371391200000,0.11350 -1371391500000,0.13.00 -1371391800000,0.13.00 +1371391500000,0.14.00 +1371391800000,0.14.00 1371392100000,0.11350 -1371392400000,0.13.00 +1371392400000,0.14.00 1371392700000,0.11250 -1371393000000,0.13.00 -1371393300000,0.13.00 -1371393600000,0.13.00 +1371393000000,0.14.00 +1371393300000,0.14.00 +1371393600000,0.14.00 1371393900000,0.11050 1371394200000,0.10800 1371394500000,0.10550 @@ -3170,14 +3170,14 @@ 1371480300000,0.13850 1371480600000,0.13150 1371480900000,0.13350 -1371481200000,0.13500 -1371481500000,0.13300 -1371481800000,0.13100 +1371481200000,0.14.00 +1371481500000,0.14.00 +1371481800000,0.14.00 1371482100000,0.12900 1371482400000,0.12450 1371482700000,0.12000 1371483000000,0.11550 -1371483300000,0.13.00 +1371483300000,0.14.00 1371483600000,0.11450 1371483900000,0.10950 1371484200000,0.10600 @@ -3379,34 +3379,34 @@ 1371543000000,0.10550 1371543300000,0.10850 1371543600000,0.11150 -1371543900000,0.13.00 +1371543900000,0.14.00 1371544200000,0.11450 -1371544500000,0.13.00 -1371544800000,0.13.00 +1371544500000,0.14.00 +1371544800000,0.14.00 1371545100000,0.11350 -1371545400000,0.13.00 +1371545400000,0.14.00 1371545700000,0.11450 -1371546000000,0.13.00 +1371546000000,0.14.00 1371546300000,0.11450 1371546600000,0.11150 1371546900000,0.10850 1371547200000,0.10750 1371547500000,0.10800 -1371547800000,0.13.00 -1371548100000,0.13.00 +1371547800000,0.14.00 +1371548100000,0.14.00 1371548400000,0.11550 1371548700000,0.11250 -1371549000000,0.13.00 +1371549000000,0.14.00 1371549300000,0.10900 -1371549600000,0.13.00 +1371549600000,0.14.00 1371549900000,0.10950 -1371550200000,0.13.00 -1371550500000,0.13.00 -1371550800000,0.13.00 +1371550200000,0.14.00 +1371550500000,0.14.00 +1371550800000,0.14.00 1371551100000,0.11550 1371551400000,0.11450 1371551700000,0.11250 -1371552000000,0.13.00 +1371552000000,0.14.00 1371552300000,0.10850 1371552600000,0.10550 1371552900000,0.10400 @@ -3660,31 +3660,31 @@ 1371627300000,0.10650 1371627600000,0.10700 1371627900000,0.10950 -1371628200000,0.13.00 +1371628200000,0.14.00 1371628500000,0.11050 -1371628800000,0.13.00 -1371629100000,0.13.00 -1371629400000,0.13.00 -1371629700000,0.13.00 -1371630000000,0.13.00 +1371628800000,0.14.00 +1371629100000,0.14.00 +1371629400000,0.14.00 +1371629700000,0.14.00 +1371630000000,0.14.00 1371630300000,0.10850 1371630600000,0.11150 -1371630900000,0.13.00 +1371630900000,0.14.00 1371631200000,0.11250 -1371631500000,0.13.00 -1371631800000,0.13.00 +1371631500000,0.14.00 +1371631800000,0.14.00 1371632100000,0.10850 1371632400000,0.11050 1371632700000,0.11050 1371633000000,0.11050 -1371633300000,0.13.00 -1371633600000,0.13.00 +1371633300000,0.14.00 +1371633600000,0.14.00 1371633900000,0.11350 1371634200000,0.11150 1371634500000,0.11050 1371634800000,0.10850 -1371635100000,0.13.00 -1371635400000,0.13.00 +1371635100000,0.14.00 +1371635400000,0.14.00 1371635700000,0.10900 1371636000000,0.10750 1371636300000,0.10800 @@ -3952,31 +3952,31 @@ 1371714900000,0.10250 1371715200000,0.10400 1371715500000,0.10700 -1371715800000,0.13.00 +1371715800000,0.14.00 1371716100000,0.10850 1371716400000,0.10800 1371716700000,0.10750 1371717000000,0.10900 1371717300000,0.10800 -1371717600000,0.13.00 -1371717900000,0.13.00 -1371718200000,0.13.00 -1371718500000,0.13.00 -1371718800000,0.13.00 -1371719100000,0.13.00 -1371719400000,0.13.00 -1371719700000,0.13.00 +1371717600000,0.14.00 +1371717900000,0.14.00 +1371718200000,0.14.00 +1371718500000,0.14.00 +1371718800000,0.14.00 +1371719100000,0.14.00 +1371719400000,0.14.00 +1371719700000,0.14.00 1371720000000,0.11150 1371720300000,0.11050 -1371720600000,0.13.00 -1371720900000,0.13.00 +1371720600000,0.14.00 +1371720900000,0.14.00 1371721200000,0.10850 1371721500000,0.10950 -1371721800000,0.13.00 -1371722100000,0.13.00 +1371721800000,0.14.00 +1371722100000,0.14.00 1371722400000,0.11350 1371722700000,0.11550 -1371723000000,0.13.00 +1371723000000,0.14.00 1371723300000,0.10950 1371723600000,0.10800 1371723900000,0.10750 @@ -4036,13 +4036,13 @@ 1371740100000,0.12600 1371740400000,0.12450 1371740700000,0.12100 -1371741000000,0.13.00 +1371741000000,0.14.00 1371741300000,0.11950 1371741600000,0.11850 -1371741900000,0.13.00 -1371742200000,0.13.00 +1371741900000,0.14.00 +1371742200000,0.14.00 1371742500000,0.11350 -1371742800000,0.13.00 +1371742800000,0.14.00 1371743100000,0.12250 1371743400000,0.12750 1371743700000,0.12800 @@ -4052,7 +4052,7 @@ 1371744900000,0.12300 1371745200000,0.12300 1371745500000,0.12150 -1371745800000,0.13.00 +1371745800000,0.14.00 1371746100000,0.11150 1371746400000,0.10900 1371746700000,0.10800 @@ -4146,11 +4146,11 @@ 1371773100000,0.10300 1371773400000,0.10350 1371773700000,0.10600 -1371774000000,0.13.00 -1371774300000,0.13.00 +1371774000000,0.14.00 +1371774300000,0.14.00 1371774600000,0.12300 1371774900000,0.12950 -1371775200000,0.13300 +1371775200000,0.14.00 1371775500000,0.13950 1371775800000,0.14250 1371776100000,0.14500 @@ -4170,12 +4170,12 @@ 1371780300000,0.14550 1371780600000,0.14350 1371780900000,0.14150 -1371781200000,0.13800 +1371781200000,0.14.00 1371781500000,0.13450 1371781800000,0.12650 1371782100000,0.12450 1371782400000,0.12100 -1371782700000,0.13.00 +1371782700000,0.14.00 1371783000000,0.11750 1371783300000,0.11650 1371783600000,0.11150 @@ -4244,33 +4244,33 @@ 1371802500000,0.10150 1371802800000,0.10000 1371803100000,0.10650 -1371803400000,0.13.00 +1371803400000,0.14.00 1371803700000,0.11250 -1371804000000,0.13.00 -1371804300000,0.13.00 +1371804000000,0.14.00 +1371804300000,0.14.00 1371804600000,0.11250 1371804900000,0.11250 1371805200000,0.11450 -1371805500000,0.13.00 +1371805500000,0.14.00 1371805800000,0.11550 -1371806100000,0.13.00 -1371806400000,0.13.00 +1371806100000,0.14.00 +1371806400000,0.14.00 1371806700000,0.11450 -1371807000000,0.13.00 -1371807300000,0.13.00 -1371807600000,0.13.00 -1371807900000,0.13.00 +1371807000000,0.14.00 +1371807300000,0.14.00 +1371807600000,0.14.00 +1371807900000,0.14.00 1371808200000,0.11350 -1371808500000,0.13.00 -1371808800000,0.13.00 -1371809100000,0.13.00 -1371809400000,0.13.00 +1371808500000,0.14.00 +1371808800000,0.14.00 +1371809100000,0.14.00 +1371809400000,0.14.00 1371809700000,0.11250 1371810000000,0.11450 1371810300000,0.11550 -1371810600000,0.13.00 -1371810900000,0.13.00 -1371811200000,0.13.00 +1371810600000,0.14.00 +1371810900000,0.14.00 +1371811200000,0.14.00 1371811500000,0.10900 1371811800000,0.10750 1371812100000,0.10600 @@ -4327,7 +4327,7 @@ 1371827400000,0.10800 1371827700000,0.11050 1371828000000,0.11150 -1371828300000,0.13.00 +1371828300000,0.14.00 1371828600000,0.11150 1371828900000,0.10850 1371829200000,0.10700 @@ -4349,29 +4349,29 @@ 1371834000000,0.09900 1371834300000,0.10800 1371834600000,0.11050 -1371834900000,0.13.00 -1371835200000,0.13.00 +1371834900000,0.14.00 +1371835200000,0.14.00 1371835500000,0.11150 -1371835800000,0.13.00 -1371836100000,0.13.00 -1371836400000,0.13.00 -1371836700000,0.13.00 +1371835800000,0.14.00 +1371836100000,0.14.00 +1371836400000,0.14.00 +1371836700000,0.14.00 1371837000000,0.12350 1371837300000,0.12700 1371837600000,0.12800 -1371837900000,0.13200 +1371837900000,0.14.00 1371838200000,0.13150 1371838500000,0.14250 1371838800000,0.14050 -1371839100000,0.13600 +1371839100000,0.14.00 1371839400000,0.13150 1371839700000,0.12750 1371840000000,0.12350 1371840300000,0.12250 -1371840600000,0.13.00 -1371840900000,0.13.00 -1371841200000,0.13.00 -1371841500000,0.13.00 +1371840600000,0.14.00 +1371840900000,0.14.00 +1371841200000,0.14.00 +1371841500000,0.14.00 1371841800000,0.11350 1371842100000,0.11150 1371842400000,0.11050 @@ -4549,22 +4549,22 @@ 1371894000000,0.10600 1371894300000,0.10950 1371894600000,0.11050 -1371894900000,0.13.00 +1371894900000,0.14.00 1371895200000,0.11350 -1371895500000,0.13.00 +1371895500000,0.14.00 1371895800000,0.12300 1371896100000,0.11050 1371896400000,0.10900 -1371896700000,0.13.00 -1371897000000,0.13.00 -1371897300000,0.13.00 -1371897600000,0.13.00 -1371897900000,0.13.00 +1371896700000,0.14.00 +1371897000000,0.14.00 +1371897300000,0.14.00 +1371897600000,0.14.00 +1371897900000,0.14.00 1371898200000,0.11750 1371898500000,0.11450 1371898800000,0.11350 -1371899100000,0.13.00 -1371899400000,0.13.00 +1371899100000,0.14.00 +1371899400000,0.14.00 1371899700000,0.11250 1371900000000,0.12100 1371900300000,0.12100 @@ -4578,16 +4578,16 @@ 1371902700000,0.12150 1371903000000,0.12050 1371903300000,0.11950 -1371903600000,0.13.00 +1371903600000,0.14.00 1371903900000,0.11750 -1371904200000,0.13.00 +1371904200000,0.14.00 1371904500000,0.11550 1371904800000,0.12000 -1371905100000,0.13.00 -1371905400000,0.13.00 +1371905100000,0.14.00 +1371905400000,0.14.00 1371905700000,0.11350 1371906000000,0.11250 -1371906300000,0.13.00 +1371906300000,0.14.00 1371906600000,0.11050 1371906900000,0.10950 1371907200000,0.10950 @@ -4840,13 +4840,13 @@ 1371981300000,0.11050 1371981600000,0.11450 1371981900000,0.11350 -1371982200000,0.13.00 -1371982500000,0.13.00 +1371982200000,0.14.00 +1371982500000,0.14.00 1371982800000,0.11250 1371983100000,0.11550 1371983400000,0.11450 1371983700000,0.11250 -1371984000000,0.13.00 +1371984000000,0.14.00 1371984300000,0.11450 1371984600000,0.11350 1371984900000,0.11650 @@ -4860,8 +4860,8 @@ 1371987300000,0.12150 1371987600000,0.12100 1371987900000,0.11950 -1371988200000,0.13.00 -1371988500000,0.13.00 +1371988200000,0.14.00 +1371988500000,0.14.00 1371988800000,0.11850 1371989100000,0.11850 1371989400000,0.11950 @@ -4879,23 +4879,23 @@ 1371993000000,0.12350 1371993300000,0.12400 1371993600000,0.11950 -1371993900000,0.13.00 -1371994200000,0.13.00 +1371993900000,0.14.00 +1371994200000,0.14.00 1371994500000,0.11750 1371994800000,0.12050 1371995100000,0.12200 1371995400000,0.11950 -1371995700000,0.13.00 -1371996000000,0.13.00 +1371995700000,0.14.00 +1371996000000,0.14.00 1371996300000,0.11650 -1371996600000,0.13.00 -1371996900000,0.13.00 -1371997200000,0.13.00 +1371996600000,0.14.00 +1371996900000,0.14.00 +1371997200000,0.14.00 1371997500000,0.11550 -1371997800000,0.13.00 -1371998100000,0.13.00 -1371998400000,0.13.00 -1371998700000,0.13.00 +1371997800000,0.14.00 +1371998100000,0.14.00 +1371998400000,0.14.00 +1371998700000,0.14.00 1371999000000,0.11650 1371999300000,0.10750 1371999600000,0.10450 @@ -4903,7 +4903,7 @@ 1372000200000,0.10150 1372000500000,0.10050 1372000800000,0.10000 -13720.13.0000,0.09900 +13720.14.0000,0.09900 1372001400000,0.10100 1372001700000,0.10200 1372002000000,0.10050 @@ -4937,7 +4937,7 @@ 1372010400000,0.08950 1372010700000,0.09050 1372011000000,0.09200 -1372011300000,0.08950 +13720.14.0000,0.08950 1372011600000,0.08700 1372011900000,0.08400 1372012200000,0.08400 @@ -5003,7 +5003,7 @@ 1372030200000,0.09200 1372030500000,0.09350 1372030800000,0.09400 -13720.13.0000,0.09350 +13720.14.0000,0.09350 1372031400000,0.09350 1372031700000,0.09300 1372032000000,0.09050 @@ -5037,7 +5037,7 @@ 1372040400000,0.06300 1372040700000,0.06300 1372041000000,0.06150 -1372041300000,0.05950 +13720.14.0000,0.05950 1372041600000,0.06000 1372041900000,0.06000 1372042200000,0.05850 @@ -5103,7 +5103,7 @@ 1372060200000,0.09700 1372060500000,0.09600 1372060800000,0.09650 -13720.13.0000,0.09950 +13720.14.0000,0.09950 1372061400000,0.09900 1372061700000,0.09900 1372062000000,0.10000 @@ -5114,30 +5114,30 @@ 1372063500000,0.10950 1372063800000,0.11150 1372064100000,0.11150 -1372064400000,0.13.00 +1372064400000,0.14.00 1372064700000,0.11350 -1372065000000,0.13.00 +1372065000000,0.14.00 1372065300000,0.11250 -1372065600000,0.13.00 -1372065900000,0.13.00 -1372066200000,0.13.00 -1372066500000,0.13.00 +1372065600000,0.14.00 +1372065900000,0.14.00 +1372066200000,0.14.00 +1372066500000,0.14.00 1372066800000,0.11650 -1372067100000,0.13.00 +1372067100000,0.14.00 1372067400000,0.11450 1372067700000,0.11150 -1372068000000,0.13.00 -1372068300000,0.13.00 +1372068000000,0.14.00 +1372068300000,0.14.00 1372068600000,0.10950 1372068900000,0.10850 -1372069200000,0.13.00 +1372069200000,0.14.00 1372069500000,0.10900 1372069800000,0.10850 1372070100000,0.11050 1372070400000,0.11250 1372070700000,0.11150 1372071000000,0.10900 -1372071300000,0.10750 +13720.14.0000,0.10750 1372071600000,0.10600 1372071900000,0.10200 1372072200000,0.10150 @@ -5203,7 +5203,7 @@ 1372090200000,0.08600 1372090500000,0.08700 1372090800000,0.08500 -13720.13.0000,0.08350 +13720.14.0000,0.08350 1372091400000,0.08250 1372091700000,0.08350 1372092000000,0.08550 @@ -5408,8 +5408,8 @@ 1372151700000,0.10900 1372152000000,0.10950 1372152300000,0.10900 -1372152600000,0.13.00 -1372152900000,0.13.00 +1372152600000,0.14.00 +1372152900000,0.14.00 1372153200000,0.11350 1372153500000,0.11150 1372153800000,0.10800 @@ -5696,7 +5696,7 @@ 1372238100000,0.11250 1372238400000,0.11150 1372238700000,0.10950 -1372239000000,0.13.00 +1372239000000,0.14.00 1372239300000,0.10900 1372239600000,0.10650 1372239900000,0.10450 @@ -5962,33 +5962,33 @@ 1372317900000,0.10550 1372318200000,0.10550 1372318500000,0.10950 -1372318800000,0.13.00 -1372319100000,0.13.00 +1372318800000,0.14.00 +1372319100000,0.14.00 1372319400000,0.11350 -1372319700000,0.13.00 -1372320000000,0.13.00 +1372319700000,0.14.00 +1372320000000,0.14.00 1372320300000,0.10700 1372320600000,0.10700 1372320900000,0.10900 1372321200000,0.10850 -1372321500000,0.13.00 -1372321800000,0.13.00 +1372321500000,0.14.00 +1372321800000,0.14.00 1372322100000,0.11150 -1372322400000,0.13.00 +1372322400000,0.14.00 1372322700000,0.11450 1372323000000,0.11350 -1372323300000,0.13.00 -1372323600000,0.13.00 -1372323900000,0.13.00 -1372324200000,0.13.00 +1372323300000,0.14.00 +1372323600000,0.14.00 +1372323900000,0.14.00 +1372324200000,0.14.00 1372324500000,0.11050 -1372324800000,0.13.00 +1372324800000,0.14.00 1372325100000,0.11050 1372325400000,0.10850 1372325700000,0.10650 1372326000000,0.10700 1372326300000,0.11050 -1372326600000,0.13.00 +1372326600000,0.14.00 1372326900000,0.11250 1372327200000,0.11250 1372327500000,0.10700 @@ -6245,10 +6245,10 @@ 1372402800000,0.08650 1372403100000,0.10700 1372403400000,0.11850 -1372403700000,0.13100 -1372404000000,0.13800 -1372404300000,0.13800 -1372404600000,0.13700 +1372403700000,0.14.00 +1372404000000,0.14.00 +1372404300000,0.14.00 +1372404600000,0.14.00 1372404900000,0.13750 1372405200000,0.14050 1372405500000,0.14500 @@ -6273,9 +6273,9 @@ 1372411200000,0.14450 1372411500000,0.14000 1372411800000,0.13550 -1372412100000,0.13500 +1372412100000,0.14.00 1372412400000,0.13250 -1372412700000,0.13000 +1372412700000,0.14.00 1372413000000,0.12650 1372413300000,0.12600 1372413600000,0.12600 @@ -6316,13 +6316,13 @@ 1372424100000,0.12100 1372424400000,0.12100 1372424700000,0.11850 -1372425000000,0.13.00 -1372425300000,0.13.00 +1372425000000,0.14.00 +1372425300000,0.14.00 1372425600000,0.12050 1372425900000,0.12150 1372426200000,0.11750 -1372426500000,0.13.00 -1372426800000,0.13.00 +1372426500000,0.14.00 +1372426800000,0.14.00 1372427100000,0.10900 1372427400000,0.10950 1372427700000,0.10600 @@ -6459,33 +6459,33 @@ 1372467000000,0.10800 1372467300000,0.10950 1372467600000,0.10950 -1372467900000,0.13.00 +1372467900000,0.14.00 1372468200000,0.11750 1372468500000,0.11550 -1372468800000,0.13.00 +1372468800000,0.14.00 1372469100000,0.11750 -1372469400000,0.13.00 -1372469700000,0.13.00 -1372470000000,0.13.00 -1372470300000,0.13.00 -1372470600000,0.13.00 +1372469400000,0.14.00 +1372469700000,0.14.00 +1372470000000,0.14.00 +1372470300000,0.14.00 +1372470600000,0.14.00 1372470900000,0.11950 1372471200000,0.11950 -1372471500000,0.13.00 +1372471500000,0.14.00 1372471800000,0.11550 1372472100000,0.11450 -1372472400000,0.13.00 +1372472400000,0.14.00 1372472700000,0.11450 1372473000000,0.11450 1372473300000,0.11450 -1372473600000,0.13.00 +1372473600000,0.14.00 1372473900000,0.11250 1372474200000,0.11250 -1372474500000,0.13.00 -1372474800000,0.13.00 +1372474500000,0.14.00 +1372474800000,0.14.00 1372475100000,0.13050 -1372475400000,0.13600 -1372475700000,0.13800 +1372475400000,0.14.00 +1372475700000,0.14.00 1372476000000,0.14350 1372476300000,0.15050 1372476600000,0.15900 @@ -6521,16 +6521,16 @@ 1372485600000,0.15750 1372485900000,0.14850 1372486200000,0.14400 -1372486500000,0.13900 -1372486800000,0.13600 -1372487100000,0.13200 +1372486500000,0.14.00 +1372486800000,0.14.00 +1372487100000,0.14.00 1372487400000,0.12750 1372487700000,0.12300 1372488000000,0.11850 1372488300000,0.11450 1372488600000,0.11250 -1372488900000,0.13.00 -1372489200000,0.13.00 +1372488900000,0.14.00 +1372489200000,0.14.00 1372489500000,0.10450 1372489800000,0.10350 1372490100000,0.10000 @@ -6565,52 +6565,52 @@ 1372498800000,0.10450 1372499100000,0.10550 1372499400000,0.10850 -1372499700000,0.13.00 -1372500000000,0.13.00 -1372500300000,0.13.00 -1372500600000,0.13.00 -1372500900000,0.13.00 -1372501200000,0.13.00 +1372499700000,0.14.00 +1372500000000,0.14.00 +1372500300000,0.14.00 +1372500600000,0.14.00 +1372500900000,0.14.00 +1372501200000,0.14.00 1372501500000,0.11350 1372501800000,0.11550 -1372502100000,0.13.00 +1372502100000,0.14.00 1372502400000,0.11650 1372502700000,0.11750 1372503000000,0.11750 -1372503300000,0.13.00 -1372503600000,0.13.00 +1372503300000,0.14.00 +1372503600000,0.14.00 1372503900000,0.11550 1372504200000,0.11650 -1372504500000,0.13.00 -1372504800000,0.13.00 +1372504500000,0.14.00 +1372504800000,0.14.00 1372505100000,0.11650 1372505400000,0.11850 1372505700000,0.12050 -1372506000000,0.13.00 -1372506300000,0.13.00 -1372506600000,0.13.00 +1372506000000,0.14.00 +1372506300000,0.14.00 +1372506600000,0.14.00 1372506900000,0.11450 1372507200000,0.11450 -1372507500000,0.13.00 -1372507800000,0.13.00 +1372507500000,0.14.00 +1372507800000,0.14.00 1372508100000,0.11450 1372508400000,0.11450 -1372508700000,0.13.00 -1372509000000,0.13.00 -1372509300000,0.13.00 +1372508700000,0.14.00 +1372509000000,0.14.00 +1372509300000,0.14.00 1372509600000,0.11850 1372509900000,0.12050 -1372510200000,0.13.00 -1372510500000,0.13.00 -1372510800000,0.13.00 +1372510200000,0.14.00 +1372510500000,0.14.00 +1372510800000,0.14.00 1372511100000,0.11150 1372511400000,0.10950 -1372511700000,0.13.00 +1372511700000,0.14.00 1372512000000,0.10900 -1372512300000,0.13.00 +1372512300000,0.14.00 1372512600000,0.11150 -1372512900000,0.13.00 -1372513200000,0.13.00 +1372512900000,0.14.00 +1372513200000,0.14.00 1372513500000,0.10900 1372513800000,0.10900 1372514100000,0.10850 @@ -6862,62 +6862,62 @@ 1372587900000,0.10900 1372588200000,0.10950 1372588500000,0.11050 -1372588800000,0.13.00 +1372588800000,0.14.00 1372589100000,0.11250 1372589400000,0.11450 -1372589700000,0.13.00 -1372590000000,0.13.00 +1372589700000,0.14.00 +1372590000000,0.14.00 1372590300000,0.11750 -1372590600000,0.13.00 +1372590600000,0.14.00 1372590900000,0.11850 -1372591200000,0.13.00 -1372591500000,0.13.00 +1372591200000,0.14.00 +1372591500000,0.14.00 1372591800000,0.11650 -1372592100000,0.13.00 +1372592100000,0.14.00 1372592400000,0.12000 1372592700000,0.12250 1372593000000,0.12000 1372593300000,0.12000 1372593600000,0.12000 -1372593900000,0.13.00 +1372593900000,0.14.00 1372594200000,0.11850 1372594500000,0.11850 -1372594800000,0.13.00 -1372595100000,0.13.00 +1372594800000,0.14.00 +1372595100000,0.14.00 1372595400000,0.11850 -1372595700000,0.13.00 +1372595700000,0.14.00 1372596000000,0.11450 1372596300000,0.11450 -1372596600000,0.13.00 +1372596600000,0.14.00 1372596900000,0.11650 1372597200000,0.11650 1372597500000,0.11750 1372597800000,0.12050 1372598100000,0.11450 -1372598400000,0.13.00 -1372598700000,0.13.00 -1372599000000,0.13.00 -1372599300000,0.13.00 -1372599600000,0.13.00 -1372599900000,0.13.00 -1372600200000,0.13.00 +1372598400000,0.14.00 +1372598700000,0.14.00 +1372599000000,0.14.00 +1372599300000,0.14.00 +1372599600000,0.14.00 +1372599900000,0.14.00 +1372600200000,0.14.00 1372600500000,0.11450 1372600800000,0.11450 1372601100000,0.11450 -1372601400000,0.13.00 -1372601700000,0.13.00 -1372602000000,0.13.00 -1372602300000,0.13.00 +1372601400000,0.14.00 +1372601700000,0.14.00 +1372602000000,0.14.00 +1372602300000,0.14.00 1372602600000,0.11250 -1372602900000,0.13.00 -1372603200000,0.13.00 -1372603500000,0.13.00 +1372602900000,0.14.00 +1372603200000,0.14.00 +1372603500000,0.14.00 1372603800000,0.10850 1372604100000,0.10500 1372604400000,0.10500 1372604700000,0.10650 -1372605000000,0.13.00 -1372605300000,0.13.00 +1372605000000,0.14.00 +1372605300000,0.14.00 1372605600000,0.10800 1372605900000,0.10450 1372606200000,0.10250 @@ -7120,4 +7120,4 @@ 1372665300000,0.09950 1372665600000,0.09950 1372665900000,0.09950 -1372666200000,0.13.00 +1372666200000,0.14.00 diff --git a/logisland-plugins/logisland-scripting-plugin/pom.xml b/logisland-plugins/logisland-scripting-plugin/pom.xml index 8924f9fb1..34c9932b7 100644 --- a/logisland-plugins/logisland-scripting-plugin/pom.xml +++ b/logisland-plugins/logisland-scripting-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-scripting-plugin diff --git a/logisland-plugins/logisland-useragent-plugin/pom.xml b/logisland-plugins/logisland-useragent-plugin/pom.xml index ba3989c8f..1cec9083a 100644 --- a/logisland-plugins/logisland-useragent-plugin/pom.xml +++ b/logisland-plugins/logisland-useragent-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-useragent-plugin diff --git a/logisland-plugins/logisland-web-analytics-plugin/pom.xml b/logisland-plugins/logisland-web-analytics-plugin/pom.xml index 7602985d1..ec818ad24 100644 --- a/logisland-plugins/logisland-web-analytics-plugin/pom.xml +++ b/logisland-plugins/logisland-web-analytics-plugin/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland-plugins - 0.13.0 + 0.14.0 logisland-web-analytics-plugin diff --git a/logisland-plugins/pom.xml b/logisland-plugins/pom.xml index 5c8517bb1..7a5cfca25 100644 --- a/logisland-plugins/pom.xml +++ b/logisland-plugins/pom.xml @@ -22,7 +22,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 logisland-plugins diff --git a/logisland-services/logisland-cache_key_value-service-api/pom.xml b/logisland-services/logisland-cache_key_value-service-api/pom.xml index 79f81b088..7c5dba5c9 100644 --- a/logisland-services/logisland-cache_key_value-service-api/pom.xml +++ b/logisland-services/logisland-cache_key_value-service-api/pom.xml @@ -7,7 +7,7 @@ logisland-services com.hurence.logisland - 0.13.0 + 0.14.0 logisland-cache_key_value-service-api diff --git a/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java b/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java index 5123fbba9..6e924448b 100644 --- a/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java +++ b/logisland-services/logisland-cache_key_value-service-api/src/main/java/com/hurence/logisland/service/cache/CSVKeyValueCacheService.java @@ -1,12 +1,12 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) - *

+ * Copyright (C) 2016 Hurence (support@hurence.com) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/CSVKeyValueCacheServiceTest.java b/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/CSVKeyValueCacheServiceTest.java index 2d8e946f4..c9c231a1f 100644 --- a/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/CSVKeyValueCacheServiceTest.java +++ b/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/CSVKeyValueCacheServiceTest.java @@ -1,12 +1,12 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) - *

+ * Copyright (C) 2016 Hurence (support@hurence.com) + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRUCacheTest.java b/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRUCacheTest.java index 77917f51c..b9bd8c051 100644 --- a/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRUCacheTest.java +++ b/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRUCacheTest.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.service.cache.model; import org.junit.Assert; diff --git a/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRULinkedHashMapTest.java b/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRULinkedHashMapTest.java index e650d566e..e1c262949 100644 --- a/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRULinkedHashMapTest.java +++ b/logisland-services/logisland-cache_key_value-service-api/src/test/java/com/hurence/logisland/service/cache/model/LRULinkedHashMapTest.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.service.cache.model; import org.junit.Assert; diff --git a/logisland-services/logisland-elasticsearch-client-service-api/pom.xml b/logisland-services/logisland-elasticsearch-client-service-api/pom.xml index fd3453783..af81a0b9c 100644 --- a/logisland-services/logisland-elasticsearch-client-service-api/pom.xml +++ b/logisland-services/logisland-elasticsearch-client-service-api/pom.xml @@ -21,7 +21,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-elasticsearch-client-service-api diff --git a/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecord.java b/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecord.java index 80166f1c6..661f43597 100644 --- a/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecord.java +++ b/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecord.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecordBuilder.java b/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecordBuilder.java index 76b04c1f7..26d1283c3 100644 --- a/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecordBuilder.java +++ b/logisland-services/logisland-elasticsearch-client-service-api/src/main/java/com/hurence/logisland/service/elasticsearch/multiGet/MultiGetQueryRecordBuilder.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2016 Hurence (bailet.thomas@gmail.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml b/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml index 5a9f22349..aa6bb4328 100644 --- a/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml +++ b/logisland-services/logisland-elasticsearch_2_4_0-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-elasticsearch_2_4_0-client-service diff --git a/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml b/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml index 9e60bb364..87edfe02d 100644 --- a/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml +++ b/logisland-services/logisland-elasticsearch_5_4_0-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-elasticsearch_5_4_0-client-service diff --git a/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverter.java b/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverter.java index 1f0561c42..f9a1dd1d3 100644 --- a/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverter.java +++ b/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverter.java @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/GeoPoint.java b/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/GeoPoint.java index b5932667c..c72deb324 100644 --- a/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/GeoPoint.java +++ b/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/main/java/com/hurence/logisland/service/elasticsearch/GeoPoint.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/test/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverterTest.java b/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/test/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverterTest.java index d31ec26e4..1bec3c378 100644 --- a/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/test/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverterTest.java +++ b/logisland-services/logisland-elasticsearch_5_4_0-client-service/src/test/java/com/hurence/logisland/service/elasticsearch/ElasticsearchRecordConverterTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-hbase-client-service-api/pom.xml b/logisland-services/logisland-hbase-client-service-api/pom.xml index ef7987127..0dd0ba998 100644 --- a/logisland-services/logisland-hbase-client-service-api/pom.xml +++ b/logisland-services/logisland-hbase-client-service-api/pom.xml @@ -21,7 +21,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-hbase-client-service-api diff --git a/logisland-services/logisland-hbase_1_1_2-client-service/pom.xml b/logisland-services/logisland-hbase_1_1_2-client-service/pom.xml index afd5fb6d3..bde0b28e5 100644 --- a/logisland-services/logisland-hbase_1_1_2-client-service/pom.xml +++ b/logisland-services/logisland-hbase_1_1_2-client-service/pom.xml @@ -19,7 +19,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-hbase_1_1_2-client-service diff --git a/logisland-services/logisland-ip-to-geo-service-api/pom.xml b/logisland-services/logisland-ip-to-geo-service-api/pom.xml index 6a6cbc4ca..30fcbd3bc 100644 --- a/logisland-services/logisland-ip-to-geo-service-api/pom.xml +++ b/logisland-services/logisland-ip-to-geo-service-api/pom.xml @@ -5,7 +5,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 4.0.0 diff --git a/logisland-services/logisland-ip-to-geo-service-api/src/main/java/com/hurence/logisland/service/iptogeo/IpToGeoService.java b/logisland-services/logisland-ip-to-geo-service-api/src/main/java/com/hurence/logisland/service/iptogeo/IpToGeoService.java index 667d77edf..a3d84e5d8 100644 --- a/logisland-services/logisland-ip-to-geo-service-api/src/main/java/com/hurence/logisland/service/iptogeo/IpToGeoService.java +++ b/logisland-services/logisland-ip-to-geo-service-api/src/main/java/com/hurence/logisland/service/iptogeo/IpToGeoService.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-services/logisland-ip-to-geo-service-maxmind/pom.xml b/logisland-services/logisland-ip-to-geo-service-maxmind/pom.xml index 1697252b1..cb2f17476 100644 --- a/logisland-services/logisland-ip-to-geo-service-maxmind/pom.xml +++ b/logisland-services/logisland-ip-to-geo-service-maxmind/pom.xml @@ -5,7 +5,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 diff --git a/logisland-services/logisland-ip-to-geo-service-maxmind/src/main/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoService.java b/logisland-services/logisland-ip-to-geo-service-maxmind/src/main/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoService.java index af73a91cc..35d618c73 100644 --- a/logisland-services/logisland-ip-to-geo-service-maxmind/src/main/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoService.java +++ b/logisland-services/logisland-ip-to-geo-service-maxmind/src/main/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoService.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-services/logisland-ip-to-geo-service-maxmind/src/test/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoServiceTest.java b/logisland-services/logisland-ip-to-geo-service-maxmind/src/test/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoServiceTest.java index 49d9f7d6a..bffb97e2f 100644 --- a/logisland-services/logisland-ip-to-geo-service-maxmind/src/test/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoServiceTest.java +++ b/logisland-services/logisland-ip-to-geo-service-maxmind/src/test/java/com/hurence/logisland/service/iptogeo/maxmind/MaxmindIpToGeoServiceTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2017 Hurence (support@hurence.com) + * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/logisland-services/logisland-redis_4-client-service/pom.xml b/logisland-services/logisland-redis_4-client-service/pom.xml index 07b8c7156..31122cbd2 100644 --- a/logisland-services/logisland-redis_4-client-service/pom.xml +++ b/logisland-services/logisland-redis_4-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-redis_4-client-service diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisConnectionPool.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisConnectionPool.java index 9a22913c5..4c2357ac2 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisConnectionPool.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisConnectionPool.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisType.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisType.java index b6a75c4c0..052a9389f 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisType.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/RedisType.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisConnectionPool.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisConnectionPool.java index dd68e0ec1..72a98ea00 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisConnectionPool.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisConnectionPool.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java index 7174c3af2..a0840ef2f 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/service/RedisKeyValueCacheService.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisAction.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisAction.java index 03cb1852e..fdc5503d6 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisAction.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisAction.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisUtils.java b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisUtils.java index 0cc336506..a54c29a1d 100644 --- a/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisUtils.java +++ b/logisland-services/logisland-redis_4-client-service/src/main/java/com/hurence/logisland/redis/util/RedisUtils.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/FakeRedisProcessor.java b/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/FakeRedisProcessor.java index 943120ffc..fab61a530 100644 --- a/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/FakeRedisProcessor.java +++ b/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/FakeRedisProcessor.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/ITRedisKeyValueCacheClientService.java b/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/ITRedisKeyValueCacheClientService.java index fcc8151d5..3111f1d06 100644 --- a/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/ITRedisKeyValueCacheClientService.java +++ b/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/ITRedisKeyValueCacheClientService.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/TestRedisConnectionPoolService.java b/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/TestRedisConnectionPoolService.java index 100f74e38..59b289c6f 100644 --- a/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/TestRedisConnectionPoolService.java +++ b/logisland-services/logisland-redis_4-client-service/src/test/java/com/hurence/logisland/redis/service/TestRedisConnectionPoolService.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/pom.xml b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/pom.xml index ae5569b89..4b6df9643 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/pom.xml +++ b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-solr-client-service - 0.13.0 + 0.14.0 logisland-solr-client-service-api diff --git a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrRecordConverter.java b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrRecordConverter.java index 8981a8199..15e336bd1 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrRecordConverter.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrRecordConverter.java @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2016 Hurence (support@hurence.com) * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrUpdater.java b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrUpdater.java index 2642ba694..18600c132 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrUpdater.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-api/src/main/java/com/hurence/logisland/service/solr/api/SolrUpdater.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/pom.xml b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/pom.xml index 43d9c24e1..844790caa 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/pom.xml +++ b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-solr-client-service - 0.13.0 + 0.14.0 logisland-solr-client-service-test @@ -29,7 +29,7 @@ com.hurence.logisland logisland-solr-client-service-api - 0.13.0 + 0.14.0 org.apache.solr diff --git a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/src/main/java/com/hurence/logisland/service/solr/SolrRecordConverterTest.java b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/src/main/java/com/hurence/logisland/service/solr/SolrRecordConverterTest.java index 336314475..730fdee7f 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/src/main/java/com/hurence/logisland/service/solr/SolrRecordConverterTest.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr-client-service-test/src/main/java/com/hurence/logisland/service/solr/SolrRecordConverterTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_5_5_5-client-service/pom.xml b/logisland-services/logisland-solr-client-service/logisland-solr_5_5_5-client-service/pom.xml index a7b73c5b7..3a587ecb2 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_5_5_5-client-service/pom.xml +++ b/logisland-services/logisland-solr-client-service/logisland-solr_5_5_5-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-solr-client-service - 0.13.0 + 0.14.0 logisland-solr_5_5_5-client-service @@ -33,7 +33,7 @@ com.hurence.logisland logisland-solr-client-service-test - 0.13.0 + 0.14.0 test diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/pom.xml b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/pom.xml index 7afe8120b..4d9a70fca 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/pom.xml +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-solr-client-service - 0.13.0 + 0.14.0 logisland-solr_6_4_2-chronix-client-service diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/ChronixUpdater.java b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/ChronixUpdater.java index 583b730cc..43a518cf1 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/ChronixUpdater.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/ChronixUpdater.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java index 13c6ae6ec..8a553c7ce 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_4_2_ChronixClientService.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/ChronixClientServiceTest.java b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/ChronixClientServiceTest.java index 966819c2b..457f57ebe 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/ChronixClientServiceTest.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/ChronixClientServiceTest.java @@ -1,12 +1,12 @@ /** * Copyright (C) 2016 Hurence (support@hurence.com) - *

+ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

+ * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/SolrTokenizationTest.java b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/SolrTokenizationTest.java index d19c55bbc..94f8859eb 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/SolrTokenizationTest.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_4_2-chronix-client-service/src/test/java/com/hurence/logisland/service/solr/SolrTokenizationTest.java @@ -1,12 +1,11 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at +/** + * Copyright (C) 2016 Hurence (support@hurence.com) * - * http://www.apache.org/licenses/LICENSE-2.0 + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/pom.xml b/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/pom.xml index 4c5adbcf0..12f614041 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/pom.xml +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-solr-client-service - 0.13.0 + 0.14.0 @@ -30,12 +30,12 @@ com.hurence.logisland logisland-solr-client-service-api - 0.13.0 + 0.14.0 com.hurence.logisland logisland-solr-client-service-test - 0.13.0 + 0.14.0 test diff --git a/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_6_2_RecordConverter.java b/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_6_2_RecordConverter.java index 7c2cf78a0..d30b89ece 100644 --- a/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_6_2_RecordConverter.java +++ b/logisland-services/logisland-solr-client-service/logisland-solr_6_6_2-client-service/src/main/java/com/hurence/logisland/service/solr/Solr_6_6_2_RecordConverter.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2016 Hurence (support@hurence.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.hurence.logisland.service.solr; import com.hurence.logisland.service.solr.api.SolrRecordConverter; diff --git a/logisland-services/logisland-solr-client-service/pom.xml b/logisland-services/logisland-solr-client-service/pom.xml index 5d880e12a..60e150489 100644 --- a/logisland-services/logisland-solr-client-service/pom.xml +++ b/logisland-services/logisland-solr-client-service/pom.xml @@ -7,7 +7,7 @@ com.hurence.logisland logisland-services - 0.13.0 + 0.14.0 logisland-solr-client-service diff --git a/logisland-services/pom.xml b/logisland-services/pom.xml index 4700be5ee..fd7e5b9e1 100644 --- a/logisland-services/pom.xml +++ b/logisland-services/pom.xml @@ -6,7 +6,7 @@ com.hurence.logisland logisland - 0.13.0 + 0.14.0 pom diff --git a/pom.xml b/pom.xml index f0439f105..b8048dcca 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 4.0.0 com.hurence.logisland logisland - 0.13.0 + 0.14.0 pom LogIsland is an event mining platform based on Kafka to handle a huge amount of data in realtime. From 55eb9a5dbf42375ac6a8bf6d2e3b081bd414dfc2 Mon Sep 17 00:00:00 2001 From: oalam Date: Wed, 18 Jul 2018 10:13:43 +0200 Subject: [PATCH 60/63] update build version + minor fix on JS components --- .../src/it/resources/docker-compose.yml | 2 +- .../tutorials/threshold-alerting.rst | 309 ++++++++++++++++++ .../conf/data/threshold-alerting/apache-0.log | 5 + .../resources/conf/threshold-alerting.yml | 218 ++++++++++++ .../src/main/resources/docs/release.rst | 79 +++++ .../docs/tutorials/threshold-alerting.rst | 309 ++++++++++++++++++ 6 files changed, 921 insertions(+), 1 deletion(-) create mode 100644 logisland-documentation/tutorials/threshold-alerting.rst create mode 100644 logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-0.log create mode 100644 logisland-framework/logisland-resources/src/main/resources/conf/threshold-alerting.yml create mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/release.rst create mode 100644 logisland-framework/logisland-resources/src/main/resources/docs/tutorials/threshold-alerting.rst diff --git a/logisland-docker/src/it/resources/docker-compose.yml b/logisland-docker/src/it/resources/docker-compose.yml index 4bbb51809..7a7684928 100644 --- a/logisland-docker/src/it/resources/docker-compose.yml +++ b/logisland-docker/src/it/resources/docker-compose.yml @@ -27,7 +27,7 @@ services: - 2181:2181 - 9092:9092 - 9000:9000 - - 4050-4060:4050-4060 + - 4050:4050 links: - "elasticsearch23" - "elasticsearch24" diff --git a/logisland-documentation/tutorials/threshold-alerting.rst b/logisland-documentation/tutorials/threshold-alerting.rst new file mode 100644 index 000000000..71f5985dd --- /dev/null +++ b/logisland-documentation/tutorials/threshold-alerting.rst @@ -0,0 +1,309 @@ +Threshold based alerting on Apache logs with Redis K/V store +============================================================ + + +In a previous tutorial we saw how to use Redis K/V store as a cache storage. In this one we will practice the use of +`ComputeTag`, `CheckThresholds` and `CheckAlerts` processor in conjunction with this Redis Cache. + +The following job is made of 2 streaming parts : + +1. A main stream which parses Apache logs and store them to a Redis cache . +2. A timer based stream which compute some new tags values based on cached records, check some thresholds cross and send alerts if needed. + +.. note:: + + Be sure to know of to launch a logisland Docker environment by reading the `prerequisites <./prerequisites.html>`_ section + +The full logisland job for this tutorial is already packaged in the tar.gz assembly and you can find it here : + +.. code-block:: sh + + docker exec -i -t logisland vim conf/threshold-alerting.yml + +We will start by explaining each part of the config file. + +1. Controller service part +-------------------------- + + +The `controllerServiceConfigurations` part is here to define all services that be shared by processors within the whole job, here a Redis KV cache service that will be used later in the ``BulkPut`` processor. + +.. code-block:: yaml + + - controllerService: datastore_service + component: com.hurence.logisland.redis.service.RedisKeyValueCacheService + type: service + documentation: redis datastore service + configuration: + connection.string: localhost:6379 + redis.mode: standalone + database.index: 0 + communication.timeout: 10 seconds + pool.max.total: 8 + pool.max.idle: 8 + pool.min.idle: 0 + pool.block.when.exhausted: true + pool.max.wait.time: 10 seconds + pool.min.evictable.idle.time: 60 seconds + pool.time.between.eviction.runs: 30 seconds + pool.num.tests.per.eviction.run: -1 + pool.test.on.create: false + pool.test.on.borrow: false + pool.test.on.return: false + pool.test.while.idle: true + record.recordSerializer: com.hurence.logisland.serializer.JsonSerializer + + +2. First stream : parse logs and compute tags +--------------------------------------------- + +Here the stream will read all the logs sent in ``logisland_raw`` topic and push the processing output into ``logisland_events`` topic as Json serialized records. + +.. code-block:: yaml + + - stream: parsing_stream + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + type: stream + documentation: a processor that converts raw apache logs into structured log records + configuration: + kafka.input.topics: logisland_raw + kafka.output.topics: logisland_events + kafka.error.topics: logisland_errors + kafka.input.topics.serializer: none + kafka.output.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + kafka.error.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.metadata.broker.list: sandbox:9092 + kafka.zookeeper.quorum: sandbox:2181 + kafka.topic.autoCreate: true + kafka.topic.default.partitions: 4 + kafka.topic.default.replicationFactor: 1 + +Within this stream a ``SplitText`` processor takes a log line as a String and computes a ``Record`` as a sequence of fields. + +.. code-block:: yaml + + - processor: apache_parser + component: com.hurence.logisland.processor.SplitText + type: parser + documentation: a parser that produce events from an apache log REGEX + configuration: + value.regex: (\S+)\s+(\S+)\s+(\S+)\s+\[([\w:\/]+\s[+\-]\d{4})\]\s+"(\S+)\s+(\S+)\s*(\S*)"\s+(\S+)\s+(\S+) + value.fields: src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out + +This stream will process log entries as soon as they will be queued into `logisland_raw` Kafka topics, each log will +be parsed as an event which will be pushed back to Kafka in the ``logisland_events`` topic. + +the next processing step is to assign `bytes_out` field as `record_value` + +.. code-block:: yaml + + - processor: normalize_fields + component: com.hurence.logisland.processor.NormalizeFields + type: parser + documentation: change current id to src_ip + configuration: + conflict.resolution.policy: overwrite_existing + record_value: bytes_out + +the we modify `record_id` to set its value as `src_ip` field. + +.. code-block:: yaml + + - processor: modify_id + component: com.hurence.logisland.processor.ModifyId + type: parser + documentation: change current id to src_ip + configuration: + id.generation.strategy: fromFields + fields.to.hash: src_ip + java.formatter.string: "%1$s" + +now we'll remove all the unwanted fields + +.. code-block:: yaml + + - processor: remove_fields + component: com.hurence.logisland.processor.RemoveFields + type: parser + documentation: remove useless fields + configuration: + fields.to.remove: src_ip,identd,user,http_method,http_query,http_version,http_status,bytes_out + +and then cast `record_value` as a double + +.. code-block:: yaml + + - processor: cast + component: com.hurence.logisland.processor.ConvertFieldsType + type: parser + documentation: cast values + configuration: + record_value: double + +The next processing step wil compute a dynamic Tag value from a Javascript expression. +here a new record with an `record_id` set to `computed1` and as a `record_value` the resulting expression of `cache("logisland.hurence.com").value * 10.2` + +.. code-block:: yaml + + - processor: compute_tag + component: com.hurence.logisland.processor.alerting.ComputeTags + type: processor + documentation: | + compute tags from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: computed_tag + max.cpu.time: 500 + max.memory: 64800000 + max.prepared.statements: 5 + allow.no.brace: false + computed1: return cache("ppp-mia-30.shadow.net").value * 10.2; + +The last processor will handle all the ``Records`` of this stream to index them into datastore previously defined (Redis) + +.. code-block:: yaml + + # all the parsed records are added to datastore by bulk + - processor: datastore_publisher + component: com.hurence.logisland.processor.datastore.BulkPut + type: processor + documentation: "indexes processed events in datastore" + configuration: + datastore.client.service: datastore_service + + + + + +3. Second stream : check threshold cross and alerting +----------------------------------------------------- +The second stream will read all the logs sent in ``logisland_events`` topic and push the processed outputs (threshold_cross & alerts records) into ``logisland_alerts`` topic as Json serialized records. + +We won't comment the stream definition as it is really straightforward. + +The first processor of this stream pipeline makes use of `CheckThresholds` component which will add a new record of type `threshold_cross` with a `record_id` set to `threshold1` if the JS expression `cache("computed1").value > 2000.0` is evaluated to true. + +.. code-block:: yaml + + - processor: compute_thresholds + component: com.hurence.logisland.processor.alerting.CheckThresholds + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + + a threshold_cross has the following properties : count, time, duration, value + configuration: + datastore.client.service: datastore_service + output.record.type: threshold_cross + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + record.ttl: 300000 + threshold1: cache("computed1").value > 2000.0 + +.. code-block:: yaml + + - processor: compute_alerts1 + component: com.hurence.logisland.processor.alerting.CheckAlerts + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: medium_alert + alert.criticity: 1 + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + profile.activation.condition: cache("threshold1").value > 3000.0 + alert1: cache("threshold1").duration > 50.0 + +The last processor will handle all the ``Records`` of this stream to index them into datastore previously defined (Redis) + +.. code-block:: yaml + + - processor: datastore_publisher + component: com.hurence.logisland.processor.datastore.BulkPut + type: processor + documentation: "indexes processed events in datastore" + configuration: + datastore.client.service: datastore_service + +4. Launch the script +-------------------- +Connect a shell to your logisland container to launch the following streaming jobs. + +.. code-block:: sh + + docker exec -i -t logisland bin/logisland.sh --conf conf/threshold-alerting.yml + +5. Inject some Apache logs into the system +------------------------------------------ +Now we're going to send some logs to ``logisland_raw`` Kafka topic. + +We could setup a logstash or flume agent to load some apache logs into a kafka topic +but there's a super useful tool in the Kafka ecosystem : `kafkacat `_, +a *generic command line non-JVM Apache Kafka producer and consumer* which can be easily installed. + + +If you don't have your own httpd logs available, you can use some freely available log files from +`NASA-HTTP `_ web site access: + +- `Jul 01 to Jul 31, ASCII format, 20.7 MB gzip compressed `_ +- `Aug 04 to Aug 31, ASCII format, 21.8 MB gzip compressed `_ + +Let's send the first 500000 lines of NASA http access over July 1995 to LogIsland with kafkacat to ``logisland_raw`` Kafka topic + +.. code-block:: sh + + cd /tmp + wget ftp://ita.ee.lbl.gov/traces/NASA_access_log_Jul95.gz + gunzip NASA_access_log_Jul95.gz + head -500000 NASA_access_log_Jul95 | kafkacat -b sandbox:9092 -t logisland_raw + + + +6. Inspect the logs and alerts +------------------------------ + +For this part of the tutorial we will use `redis-py a Python client for Redis `_. You can install it by following instructions given on `redis-py `_. + +To install redis-py, simply: + +.. code-block:: sh + + $ sudo pip install redis + + +Getting Started, check if you can connect with Redis + +.. code-block:: python + + >>> import redis + >>> r = redis.StrictRedis(host='localhost', port=6379, db=0) + >>> r.set('foo', 'bar') + >>> r.get('foo') + +Then we want to grab some logs that have been collected to Redis. We first find some keys with a pattern and get the json content of one + +.. code-block:: python + + >>> r.keys('1234*') +['123493eb-93df-4e57-a1c1-4a8e844fa92c', '123457d5-8ccc-4f0f-b4ba-d70967aa48eb', '12345e06-6d72-4ce8-8254-a7cc4bab5e31'] + + >>> r.get('123493eb-93df-4e57-a1c1-4a8e844fa92c') +'{\n "id" : "123493eb-93df-4e57-a1c1-4a8e844fa92c",\n "type" : "apache_log",\n "creationDate" : 804574829000,\n "fields" : {\n "src_ip" : "204.191.209.4",\n "record_id" : "123493eb-93df-4e57-a1c1-4a8e844fa92c",\n "http_method" : "GET",\n "http_query" : "/images/WORLD-logosmall.gif",\n "bytes_out" : "669",\n "identd" : "-",\n "http_version" : "HTTP/1.0",\n "record_raw_value" : "204.191.209.4 - - [01/Jul/1995:01:00:29 -0400] \\"GET /images/WORLD-logosmall.gif HTTP/1.0\\" 200 669",\n "http_status" : "200",\n "record_time" : 804574829000,\n "user" : "-",\n "record_type" : "apache_log"\n }\n}' + + >>> import json + >>> record = json.loads(r.get('123493eb-93df-4e57-a1c1-4a8e844fa92c')) + >>> record['fields']['bytes_out'] + diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-0.log b/logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-0.log new file mode 100644 index 000000000..5d2983f61 --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/conf/data/threshold-alerting/apache-0.log @@ -0,0 +1,5 @@ +bradfute.vip.best.com - - [01/Jul/1995:01:28:57 -0400] "GET /htbin/cdt_main.pl HTTP/1.0" 200 3214 +ix-mhl-ca1-18.ix.netcom.com - - [01/Jul/1995:01:29:00 -0400] "GET /shuttle/missions/sts-71/images/images.html HTTP/1.0" 200 7634 +ts6.northcoast.com - - [01/Jul/1995:01:29:02 -0400] "GET /history/apollo/apollo.html HTTP/1.0" 200 3258 +kristina.az.com - - [01/Jul/1995:01:29:02 -0400] "GET /shuttle/missions/51-l/movies/ HTTP/1.0" 200 372 +cs3p3.ipswichcity.qld.gov.au - - [01/Jul/1995:01:29:03 -0400] "GET /shuttle/missions/sts-71/sts-71-patch-small.gif HTTP/1.0" 200 12054 \ No newline at end of file diff --git a/logisland-framework/logisland-resources/src/main/resources/conf/threshold-alerting.yml b/logisland-framework/logisland-resources/src/main/resources/conf/threshold-alerting.yml new file mode 100644 index 000000000..0a73c16d3 --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/conf/threshold-alerting.yml @@ -0,0 +1,218 @@ +######################################################################################################### +# Logisland configuration script tempate +######################################################################################################### + +version: 0.14.0 +documentation: LogIsland analytics main config file. Put here every engine or component config + +######################################################################################################### +# engine +engine: + component: com.hurence.logisland.engine.spark.KafkaStreamProcessingEngine + type: engine + documentation: Index some apache logs with logisland + configuration: + spark.app.name: ThresholdAlerting + spark.master: local[1] + spark.driver.memory: 1G + spark.driver.cores: 1 + spark.executor.memory: 2G + spark.executor.instances: 4 + spark.executor.cores: 1 + spark.serializer: org.apache.spark.serializer.KryoSerializer + spark.streaming.batchDuration: 300 + spark.streaming.backpressure.enabled: false + spark.streaming.unpersist: false + spark.streaming.blockInterval: 500 + spark.streaming.kafka.maxRatePerPartition: 3000 + spark.streaming.timeout: -1 + spark.streaming.unpersist: false + spark.streaming.kafka.maxRetries: 3 + spark.streaming.ui.retainedBatches: 200 + spark.streaming.receiver.writeAheadLog.enable: false + spark.ui.port: 4050 + + controllerServiceConfigurations: + + - controllerService: datastore_service + component: com.hurence.logisland.redis.service.RedisKeyValueCacheService + type: service + documentation: redis datastore service + configuration: + connection.string: ${REDIS_CONNECTION} + redis.mode: standalone + database.index: 0 + communication.timeout: 10 seconds + pool.max.total: 8 + pool.max.idle: 8 + pool.min.idle: 0 + pool.block.when.exhausted: true + pool.max.wait.time: 10 seconds + pool.min.evictable.idle.time: 60 seconds + pool.time.between.eviction.runs: 30 seconds + pool.num.tests.per.eviction.run: -1 + pool.test.on.create: false + pool.test.on.borrow: false + pool.test.on.return: false + pool.test.while.idle: true + record.recordSerializer: com.hurence.logisland.serializer.JsonSerializer + + streamConfigurations: + + # main processing stream + - stream: parsing_stream + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + type: stream + documentation: a processor that converts raw apache logs into structured log records + configuration: + kafka.input.topics: logisland_raw + kafka.output.topics: logisland_events + kafka.error.topics: logisland_errors + kafka.input.topics.serializer: none + kafka.output.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.error.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.metadata.broker.list: ${KAFKA_BROKERS} + kafka.zookeeper.quorum: ${ZK_QUORUM} + kafka.topic.autoCreate: true + kafka.topic.default.partitions: 4 + kafka.topic.default.replicationFactor: 1 + processorConfigurations: + + # parse apache logs into logisland records + - processor: apache_parser + component: com.hurence.logisland.processor.SplitText + type: parser + documentation: a parser that produce events from an apache log REGEX + configuration: + record.type: apache_log + value.regex: (\S+)\s+(\S+)\s+(\S+)\s+\[([\w:\/]+\s[+\-]\d{4})\]\s+"(\S+)\s+(\S+)\s*(\S*)"\s+(\S+)\s+(\S+) + value.fields: src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out + + - processor: normalize_fields + component: com.hurence.logisland.processor.NormalizeFields + type: parser + documentation: change current id to src_ip + configuration: + conflict.resolution.policy: overwrite_existing + record_value: bytes_out + + - processor: modify_id + component: com.hurence.logisland.processor.ModifyId + type: parser + documentation: change current id to src_ip + configuration: + id.generation.strategy: fromFields + fields.to.hash: src_ip + java.formatter.string: "%1$s" + + - processor: remove_fields + component: com.hurence.logisland.processor.RemoveFields + type: parser + documentation: remove useless fields + configuration: + fields.to.remove: src_ip,identd,user,http_method,http_query,http_version,http_status,bytes_out + + - processor: cast + component: com.hurence.logisland.processor.ConvertFieldsType + type: parser + documentation: cast values + configuration: + record_value: double + + - processor: compute_tag + component: com.hurence.logisland.processor.alerting.ComputeTags + type: processor + documentation: | + compute tags from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: computed_tag + max.cpu.time: 500 + max.memory: 64800000 + max.prepared.statements: 5 + allow.no.brace: false + computed1: return cache("logisland.hurence.com").value * 10.2; + + + # all the parsed records are added to datastore by bulk + - processor: datastore_publisher + component: com.hurence.logisland.processor.datastore.BulkPut + type: processor + documentation: "indexes processed events in datastore" + configuration: + datastore.client.service: datastore_service + + + + - stream: alerting_stream + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + type: stream + documentation: a processor that converts raw apache logs into structured log records + configuration: + kafka.input.topics: logisland_events + kafka.output.topics: logisland_alerts + kafka.error.topics: logisland_errors + kafka.input.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.output.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.error.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.metadata.broker.list: ${KAFKA_BROKERS} + kafka.zookeeper.quorum: ${ZK_QUORUM} + kafka.topic.autoCreate: true + kafka.topic.default.partitions: 4 + kafka.topic.default.replicationFactor: 1 + processorConfigurations: + + - processor: compute_thresholds + component: com.hurence.logisland.processor.alerting.CheckThresholds + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + + a threshold_cross has the following properties : count, time, duration, value + configuration: + datastore.client.service: datastore_service + output.record.type: threshold_cross + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + record.ttl: 30000 + threshold1: cache("logisland.hurence.com").value > 2000.0 + + - processor: compute_alerts1 + component: com.hurence.logisland.processor.alerting.CheckAlerts + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: medium_alert + alert.criticity: 1 + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + profile.activation.condition: cache("computed1").value > 80000.0 + alert1: cache("threshold1").duration > 50.0 + + + - processor: debug + component: com.hurence.logisland.processor.DebugStream + configuration: + event.serializer: json + + - processor: datastore_publisher + component: com.hurence.logisland.processor.datastore.BulkPut + type: processor + documentation: "indexes processed events in datastore" + configuration: + datastore.client.service: datastore_service + + diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/release.rst b/logisland-framework/logisland-resources/src/main/resources/docs/release.rst new file mode 100644 index 000000000..305b05e81 --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/docs/release.rst @@ -0,0 +1,79 @@ +Releasing guide +=============== + +This guide will help you to perform the full release process for Logisland framework. + +1. + + + + + +Build the code and run the tests +-------------------------------- + + +The following commands must be run from the top-level directory. + +.. code-block:: sh + + mvn clean install + +If you wish to skip the unit tests you can do this by adding `-DskipTests` to the command line. + + +Release to maven repositories +----------------------------- +to release artifacts (if you're allowed to), follow this guide `release to OSS Sonatype with maven `_ + +.. code-block:: sh + + # update the version (you should run a dry run first) + ./update-version.sh -o 0.14.0 -n 0.14.0 -d + ./update-version.sh -o 0.14.0 -n 0.14.0 + mvn license:format + mvn clean install + mvn -DperformRelease=true clean deploy -Phdp2.5 + mvn versions:commit + + +follow the staging procedure in `oss.sonatype.org `_ or read `Sonatype book `_ + +go to `oss.sonatype.org `_ to release manually the artifact + + + +Publish release assets to github +-------------------------------- + +please refer to `https://developer.github.com/v3/repos/releases `_ + +curl -XPOST https://uploads.github.com/repos/Hurence/logisland/releases/v0.14.0/assets?name=logisland-0.14.0-bin-hdp2.5.tar.gz -v --data-binary @logisland-assembly/target/logisland-0.10.3-bin-hdp2.5.tar.gz --user oalam -H 'Content-Type: application/gzip' + + + +Publish Docker image +-------------------- +Building the image + +.. code-block:: sh + + # build logisland + mvn clean install -DskipTests -Pdocker -Dhdp2.5 + + # verify image build + docker images + + +then login and push the latest image + +.. code-block:: sh + + docker login + docker push hurence/logisland + + +Publish artifact to github +-------------------------- + +Tag the release + upload latest tgz diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/threshold-alerting.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/threshold-alerting.rst new file mode 100644 index 000000000..71f5985dd --- /dev/null +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/threshold-alerting.rst @@ -0,0 +1,309 @@ +Threshold based alerting on Apache logs with Redis K/V store +============================================================ + + +In a previous tutorial we saw how to use Redis K/V store as a cache storage. In this one we will practice the use of +`ComputeTag`, `CheckThresholds` and `CheckAlerts` processor in conjunction with this Redis Cache. + +The following job is made of 2 streaming parts : + +1. A main stream which parses Apache logs and store them to a Redis cache . +2. A timer based stream which compute some new tags values based on cached records, check some thresholds cross and send alerts if needed. + +.. note:: + + Be sure to know of to launch a logisland Docker environment by reading the `prerequisites <./prerequisites.html>`_ section + +The full logisland job for this tutorial is already packaged in the tar.gz assembly and you can find it here : + +.. code-block:: sh + + docker exec -i -t logisland vim conf/threshold-alerting.yml + +We will start by explaining each part of the config file. + +1. Controller service part +-------------------------- + + +The `controllerServiceConfigurations` part is here to define all services that be shared by processors within the whole job, here a Redis KV cache service that will be used later in the ``BulkPut`` processor. + +.. code-block:: yaml + + - controllerService: datastore_service + component: com.hurence.logisland.redis.service.RedisKeyValueCacheService + type: service + documentation: redis datastore service + configuration: + connection.string: localhost:6379 + redis.mode: standalone + database.index: 0 + communication.timeout: 10 seconds + pool.max.total: 8 + pool.max.idle: 8 + pool.min.idle: 0 + pool.block.when.exhausted: true + pool.max.wait.time: 10 seconds + pool.min.evictable.idle.time: 60 seconds + pool.time.between.eviction.runs: 30 seconds + pool.num.tests.per.eviction.run: -1 + pool.test.on.create: false + pool.test.on.borrow: false + pool.test.on.return: false + pool.test.while.idle: true + record.recordSerializer: com.hurence.logisland.serializer.JsonSerializer + + +2. First stream : parse logs and compute tags +--------------------------------------------- + +Here the stream will read all the logs sent in ``logisland_raw`` topic and push the processing output into ``logisland_events`` topic as Json serialized records. + +.. code-block:: yaml + + - stream: parsing_stream + component: com.hurence.logisland.stream.spark.KafkaRecordStreamParallelProcessing + type: stream + documentation: a processor that converts raw apache logs into structured log records + configuration: + kafka.input.topics: logisland_raw + kafka.output.topics: logisland_events + kafka.error.topics: logisland_errors + kafka.input.topics.serializer: none + kafka.output.topics.serializer: com.hurence.logisland.serializer.KryoSerializer + kafka.error.topics.serializer: com.hurence.logisland.serializer.JsonSerializer + kafka.metadata.broker.list: sandbox:9092 + kafka.zookeeper.quorum: sandbox:2181 + kafka.topic.autoCreate: true + kafka.topic.default.partitions: 4 + kafka.topic.default.replicationFactor: 1 + +Within this stream a ``SplitText`` processor takes a log line as a String and computes a ``Record`` as a sequence of fields. + +.. code-block:: yaml + + - processor: apache_parser + component: com.hurence.logisland.processor.SplitText + type: parser + documentation: a parser that produce events from an apache log REGEX + configuration: + value.regex: (\S+)\s+(\S+)\s+(\S+)\s+\[([\w:\/]+\s[+\-]\d{4})\]\s+"(\S+)\s+(\S+)\s*(\S*)"\s+(\S+)\s+(\S+) + value.fields: src_ip,identd,user,record_time,http_method,http_query,http_version,http_status,bytes_out + +This stream will process log entries as soon as they will be queued into `logisland_raw` Kafka topics, each log will +be parsed as an event which will be pushed back to Kafka in the ``logisland_events`` topic. + +the next processing step is to assign `bytes_out` field as `record_value` + +.. code-block:: yaml + + - processor: normalize_fields + component: com.hurence.logisland.processor.NormalizeFields + type: parser + documentation: change current id to src_ip + configuration: + conflict.resolution.policy: overwrite_existing + record_value: bytes_out + +the we modify `record_id` to set its value as `src_ip` field. + +.. code-block:: yaml + + - processor: modify_id + component: com.hurence.logisland.processor.ModifyId + type: parser + documentation: change current id to src_ip + configuration: + id.generation.strategy: fromFields + fields.to.hash: src_ip + java.formatter.string: "%1$s" + +now we'll remove all the unwanted fields + +.. code-block:: yaml + + - processor: remove_fields + component: com.hurence.logisland.processor.RemoveFields + type: parser + documentation: remove useless fields + configuration: + fields.to.remove: src_ip,identd,user,http_method,http_query,http_version,http_status,bytes_out + +and then cast `record_value` as a double + +.. code-block:: yaml + + - processor: cast + component: com.hurence.logisland.processor.ConvertFieldsType + type: parser + documentation: cast values + configuration: + record_value: double + +The next processing step wil compute a dynamic Tag value from a Javascript expression. +here a new record with an `record_id` set to `computed1` and as a `record_value` the resulting expression of `cache("logisland.hurence.com").value * 10.2` + +.. code-block:: yaml + + - processor: compute_tag + component: com.hurence.logisland.processor.alerting.ComputeTags + type: processor + documentation: | + compute tags from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: computed_tag + max.cpu.time: 500 + max.memory: 64800000 + max.prepared.statements: 5 + allow.no.brace: false + computed1: return cache("ppp-mia-30.shadow.net").value * 10.2; + +The last processor will handle all the ``Records`` of this stream to index them into datastore previously defined (Redis) + +.. code-block:: yaml + + # all the parsed records are added to datastore by bulk + - processor: datastore_publisher + component: com.hurence.logisland.processor.datastore.BulkPut + type: processor + documentation: "indexes processed events in datastore" + configuration: + datastore.client.service: datastore_service + + + + + +3. Second stream : check threshold cross and alerting +----------------------------------------------------- +The second stream will read all the logs sent in ``logisland_events`` topic and push the processed outputs (threshold_cross & alerts records) into ``logisland_alerts`` topic as Json serialized records. + +We won't comment the stream definition as it is really straightforward. + +The first processor of this stream pipeline makes use of `CheckThresholds` component which will add a new record of type `threshold_cross` with a `record_id` set to `threshold1` if the JS expression `cache("computed1").value > 2000.0` is evaluated to true. + +.. code-block:: yaml + + - processor: compute_thresholds + component: com.hurence.logisland.processor.alerting.CheckThresholds + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + + a threshold_cross has the following properties : count, time, duration, value + configuration: + datastore.client.service: datastore_service + output.record.type: threshold_cross + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + record.ttl: 300000 + threshold1: cache("computed1").value > 2000.0 + +.. code-block:: yaml + + - processor: compute_alerts1 + component: com.hurence.logisland.processor.alerting.CheckAlerts + type: processor + documentation: | + compute threshold cross from given formulas. + each dynamic property will return a new record according to the formula definition + the record name will be set to the property name + the record time will be set to the current timestamp + configuration: + datastore.client.service: datastore_service + output.record.type: medium_alert + alert.criticity: 1 + max.cpu.time: 100 + max.memory: 12800000 + max.prepared.statements: 5 + profile.activation.condition: cache("threshold1").value > 3000.0 + alert1: cache("threshold1").duration > 50.0 + +The last processor will handle all the ``Records`` of this stream to index them into datastore previously defined (Redis) + +.. code-block:: yaml + + - processor: datastore_publisher + component: com.hurence.logisland.processor.datastore.BulkPut + type: processor + documentation: "indexes processed events in datastore" + configuration: + datastore.client.service: datastore_service + +4. Launch the script +-------------------- +Connect a shell to your logisland container to launch the following streaming jobs. + +.. code-block:: sh + + docker exec -i -t logisland bin/logisland.sh --conf conf/threshold-alerting.yml + +5. Inject some Apache logs into the system +------------------------------------------ +Now we're going to send some logs to ``logisland_raw`` Kafka topic. + +We could setup a logstash or flume agent to load some apache logs into a kafka topic +but there's a super useful tool in the Kafka ecosystem : `kafkacat `_, +a *generic command line non-JVM Apache Kafka producer and consumer* which can be easily installed. + + +If you don't have your own httpd logs available, you can use some freely available log files from +`NASA-HTTP `_ web site access: + +- `Jul 01 to Jul 31, ASCII format, 20.7 MB gzip compressed `_ +- `Aug 04 to Aug 31, ASCII format, 21.8 MB gzip compressed `_ + +Let's send the first 500000 lines of NASA http access over July 1995 to LogIsland with kafkacat to ``logisland_raw`` Kafka topic + +.. code-block:: sh + + cd /tmp + wget ftp://ita.ee.lbl.gov/traces/NASA_access_log_Jul95.gz + gunzip NASA_access_log_Jul95.gz + head -500000 NASA_access_log_Jul95 | kafkacat -b sandbox:9092 -t logisland_raw + + + +6. Inspect the logs and alerts +------------------------------ + +For this part of the tutorial we will use `redis-py a Python client for Redis `_. You can install it by following instructions given on `redis-py `_. + +To install redis-py, simply: + +.. code-block:: sh + + $ sudo pip install redis + + +Getting Started, check if you can connect with Redis + +.. code-block:: python + + >>> import redis + >>> r = redis.StrictRedis(host='localhost', port=6379, db=0) + >>> r.set('foo', 'bar') + >>> r.get('foo') + +Then we want to grab some logs that have been collected to Redis. We first find some keys with a pattern and get the json content of one + +.. code-block:: python + + >>> r.keys('1234*') +['123493eb-93df-4e57-a1c1-4a8e844fa92c', '123457d5-8ccc-4f0f-b4ba-d70967aa48eb', '12345e06-6d72-4ce8-8254-a7cc4bab5e31'] + + >>> r.get('123493eb-93df-4e57-a1c1-4a8e844fa92c') +'{\n "id" : "123493eb-93df-4e57-a1c1-4a8e844fa92c",\n "type" : "apache_log",\n "creationDate" : 804574829000,\n "fields" : {\n "src_ip" : "204.191.209.4",\n "record_id" : "123493eb-93df-4e57-a1c1-4a8e844fa92c",\n "http_method" : "GET",\n "http_query" : "/images/WORLD-logosmall.gif",\n "bytes_out" : "669",\n "identd" : "-",\n "http_version" : "HTTP/1.0",\n "record_raw_value" : "204.191.209.4 - - [01/Jul/1995:01:00:29 -0400] \\"GET /images/WORLD-logosmall.gif HTTP/1.0\\" 200 669",\n "http_status" : "200",\n "record_time" : 804574829000,\n "user" : "-",\n "record_type" : "apache_log"\n }\n}' + + >>> import json + >>> record = json.loads(r.get('123493eb-93df-4e57-a1c1-4a8e844fa92c')) + >>> record['fields']['bytes_out'] + From d5ce58b7ca2b94234e3438f5f8fdb9f7fffbf571 Mon Sep 17 00:00:00 2001 From: oalam Date: Wed, 18 Jul 2018 10:15:56 +0200 Subject: [PATCH 61/63] update tutorial index --- logisland-documentation/tutorials/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/logisland-documentation/tutorials/index.rst b/logisland-documentation/tutorials/index.rst index e13f859b1..82b9d2e28 100644 --- a/logisland-documentation/tutorials/index.rst +++ b/logisland-documentation/tutorials/index.rst @@ -25,6 +25,7 @@ Contents: prerequisites index-apache-logs store-to-redis + threshold-alerting match-queries aggregate-events enrich-apache-logs From c720dc0657a3783d2512cb25c9689b303e5e4d66 Mon Sep 17 00:00:00 2001 From: oalam Date: Tue, 31 Jul 2018 10:22:57 +0200 Subject: [PATCH 62/63] fix alerting test issue + doc --- .../LogIsland-architecture.graffle/data.plist | Bin 25538 -> 27099 bytes logisland-documentation/components.rst | 1 + .../src/main/resources/docs/components.rst | 1 + .../main/resources/docs/tutorials/index.rst | 1 + .../processor/alerting/CheckAlertsTest.java | 2 +- .../processor/alerting/ComputeTagsTest.java | 2 +- 6 files changed, 5 insertions(+), 2 deletions(-) diff --git a/logisland-documentation/LogIsland-architecture.graffle/data.plist b/logisland-documentation/LogIsland-architecture.graffle/data.plist index 9a5b06e10af58e2eb848b7c60a3d12e0b269e82c..fbad981c219a907f1a2bf7d105104c53844c755e 100644 GIT binary patch literal 27099 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 Jj{_pi{{`MmOjQ5? literal 25538 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 Mn%>U|ax|p>0fbC1egFUf diff --git a/logisland-documentation/components.rst b/logisland-documentation/components.rst index 6cb92e54d..b0247a5e9 100644 --- a/logisland-documentation/components.rst +++ b/logisland-documentation/components.rst @@ -318,6 +318,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" + "min.update.time.ms", "The minimum amount of time (in ms) that we expect between two consecutive update of the same threshold record", "", "200", "", "" Dynamic Properties __________________ diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst index 6cb92e54d..b0247a5e9 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/components.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/components.rst @@ -318,6 +318,7 @@ In the list below, the names of required properties appear in **bold**. Any othe "js.cache.service", "The cache service to be used to store already sanitized JS expressions. If not specified a in-memory unlimited hash map will be used.", "", "null", "", "" "output.record.type", "the type of the output record", "", "event", "", "" "record.ttl", "How long (in ms) do the record will remain in cache", "", "30000", "", "" + "min.update.time.ms", "The minimum amount of time (in ms) that we expect between two consecutive update of the same threshold record", "", "200", "", "" Dynamic Properties __________________ diff --git a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst index e13f859b1..82b9d2e28 100644 --- a/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst +++ b/logisland-framework/logisland-resources/src/main/resources/docs/tutorials/index.rst @@ -25,6 +25,7 @@ Contents: prerequisites index-apache-logs store-to-redis + threshold-alerting match-queries aggregate-events enrich-apache-logs diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java index 058bc77b8..dc8ff6c38 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/CheckAlertsTest.java @@ -59,7 +59,7 @@ public void testSyntax() throws InitializationException { runner.run(); runner.assertAllInputRecordsProcessed(); runner.assertOutputRecordsCount(0); - runner.assertOutputErrorCount(2); + runner.assertOutputErrorCount(0); } diff --git a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java index e6ef3ad13..23e9c5cbe 100644 --- a/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java +++ b/logisland-plugins/logisland-common-processors-plugin/src/test/java/com/hurence/logisland/processor/alerting/ComputeTagsTest.java @@ -173,7 +173,7 @@ public void testBadRules() throws InitializationException { runner.run(); runner.assertAllInputRecordsProcessed(); runner.assertOutputRecordsCount(3); - runner.assertOutputErrorCount(1); + runner.assertOutputErrorCount(0); for (Record enriched : runner.getOutputRecords()) { if (enriched.getId().equals("cvib3")) { From 9dfedcabee9eb543a15d5315f7585b259813d2b2 Mon Sep 17 00:00:00 2001 From: oalam Date: Tue, 31 Jul 2018 10:23:33 +0200 Subject: [PATCH 63/63] update archi diagram --- .../image72.png | Bin 0 -> 1026780 bytes .../image73.tiff | Bin 0 -> 9272 bytes .../image74.tiff | Bin 0 -> 17396 bytes .../image76.tiff | Bin 0 -> 10266 bytes .../image77.tiff | Bin 0 -> 38654 bytes .../image78.tiff | Bin 0 -> 43118 bytes .../image79.tiff | Bin 0 -> 9308 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image72.png create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image73.tiff create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image74.tiff create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image76.tiff create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image77.tiff create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image78.tiff create mode 100644 logisland-documentation/LogIsland-architecture.graffle/image79.tiff diff --git a/logisland-documentation/LogIsland-architecture.graffle/image72.png b/logisland-documentation/LogIsland-architecture.graffle/image72.png new file mode 100644 index 0000000000000000000000000000000000000000..e29ca75c2fab5e3e200ac948b4b3c1dc26f5f1ef GIT binary patch literal 1026780 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 z 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 z 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⩔vM5u}l5tJMH^{mmv3pn?kMfxGq7ZMTJz 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%0AJHEMQoZ9{2&pJ)Ax{rJLMNffW}kTf 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%0AgU*P( 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

;{wZ_9!4e#2>ha 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 t@TUH`h76m!QYJ}PPTN(nmIVm5%ProUU=FAxn1 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

DRO?b!?Xdd-WgodE}8yQdCDr=7ljUeun&nyMjF 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 WcXWPHCDy>f{ybCBQZ7}p3jSX?UCFip literal 0 HcmV?d00001 diff --git a/logisland-documentation/LogIsland-architecture.graffle/image73.tiff b/logisland-documentation/LogIsland-architecture.graffle/image73.tiff new file mode 100644 index 0000000000000000000000000000000000000000..2e2e6eba48511b54a8a6d8c3a861889211903222 GIT binary patch literal 9272 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 F{{=Xk28RFu literal 0 HcmV?d00001 diff --git a/logisland-documentation/LogIsland-architecture.graffle/image74.tiff b/logisland-documentation/LogIsland-architecture.graffle/image74.tiff new file mode 100644 index 0000000000000000000000000000000000000000..ffb30c1679659d677ce60cc81bc5233161c4388e GIT binary patch literal 17396 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%0DT!}FfJ=D5gcyW;w#as!W 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 literal 0 HcmV?d00001 diff --git a/logisland-documentation/LogIsland-architecture.graffle/image76.tiff b/logisland-documentation/LogIsland-architecture.graffle/image76.tiff new file mode 100644 index 0000000000000000000000000000000000000000..bb403c20e2e9fcc513c7cf2caffd19e322c9645c GIT binary patch literal 10266 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 literal 0 HcmV?d00001 diff --git a/logisland-documentation/LogIsland-architecture.graffle/image77.tiff b/logisland-documentation/LogIsland-architecture.graffle/image77.tiff new file mode 100644 index 0000000000000000000000000000000000000000..bfdbc5a754a90f7681f9eb50a57d43ca94af1cee GIT binary patch literal 38654 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 Vm&V`ie?RrdH-Fs!n(_7Pe*tOe3O4`% literal 0 HcmV?d00001 diff --git a/logisland-documentation/LogIsland-architecture.graffle/image79.tiff b/logisland-documentation/LogIsland-architecture.graffle/image79.tiff new file mode 100644 index 0000000000000000000000000000000000000000..30e38d218908f21d2bccbde80dd86d8c4207a96e GIT binary patch literal 9308 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