├── common ├── src │ └── main │ │ └── java │ │ └── io │ │ └── github │ │ └── slvwolf │ │ ├── ConfigUpdate.java │ │ ├── UnknownConfigException.java │ │ ├── Counter.java │ │ ├── SchemaItem.java │ │ └── CCClient.java └── pom.xml ├── .gitignore ├── README.md ├── all ├── src │ └── main │ │ └── java │ │ └── io │ │ └── github │ │ └── slvwolf │ │ └── CCentral.java └── pom.xml ├── LICENSE ├── .github └── workflows │ └── codeql-analysis.yml ├── etcd ├── pom.xml └── src │ ├── main │ └── java │ │ └── io │ │ └── github │ │ └── slvwolf │ │ ├── EtcdAccess.java │ │ └── CCEtcdClient.java │ └── test │ └── java │ └── io │ └── github │ └── slvwolf │ └── CCentralTest.java └── pom.xml /common/src/main/java/io/github/slvwolf/ConfigUpdate.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | /** 4 | * Functional interface for listening configuration updates 5 | */ 6 | public interface ConfigUpdate { 7 | void valueChanged(String configKey); 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/java/io/github/slvwolf/UnknownConfigException.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | public class UnknownConfigException extends Exception { 4 | public UnknownConfigException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | .DS_Store 4 | 5 | *.iml 6 | 7 | *.class 8 | 9 | # Mobile Tools for Java (J2ME) 10 | .mtj.tmp/ 11 | 12 | # Package Files # 13 | *.jar 14 | *.war 15 | *.ear 16 | 17 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 18 | hs_err_pid* 19 | *.classpath 20 | *.project 21 | *.settings 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CCentral 2 | Java client library for CCentral (Simple centralized configuration management and real-time monitoring for services). 3 | Server implementation can be found from [here](https://github.com/slvwolf/ccentral). 4 | 5 | ## Packages 6 | 7 | - `ccentral-all` - Everything included 8 | - `ccentral-common` - Basic interfaces, no connector implementations 9 | - `ccentral-etcd` - Etcd CCentral connector -------------------------------------------------------------------------------- /all/src/main/java/io/github/slvwolf/CCentral.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | import mousio.etcd4j.EtcdClient; 4 | 5 | import java.net.URI; 6 | 7 | public class CCentral { 8 | 9 | public static CCClient initWithEtcdClient(String serviceId, EtcdClient client) { 10 | // Client ID will be injected by the CCEtcdClient 11 | return new CCEtcdClient(new EtcdAccess(client, serviceId, "")); 12 | } 13 | 14 | public static CCClient initWithEtcdHost(String serviceId, URI[] hosts) { 15 | return new CCEtcdClient(serviceId, hosts); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/io/github/slvwolf/Counter.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | class Counter { 4 | private int last; 5 | private int current; 6 | private long ts; 7 | 8 | public Counter() { 9 | last = 0; 10 | current = 0; 11 | ts = System.currentTimeMillis(); 12 | } 13 | 14 | private void refresh() { 15 | while (ts + 60_000 < System.currentTimeMillis()) { 16 | last = current; 17 | current = 0; 18 | ts = ts + 60_000; 19 | } 20 | } 21 | 22 | public void increment(int amount) { 23 | refresh(); 24 | current += amount; 25 | } 26 | 27 | public void set(int amount) { 28 | refresh(); 29 | current = amount; 30 | } 31 | 32 | public int getValue() { 33 | refresh(); 34 | return last; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Santtu Järvi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /all/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccentral-parent 7 | io.github.slvwolf 8 | 0.5.1 9 | 10 | 4.0.0 11 | 12 | ccentral-all 13 | CCentral - All 14 | 15 | jar 16 | 17 | 18 | 19 | 20 | io.github.slvwolf 21 | ccentral-common 22 | 0.5.1 23 | compile 24 | 25 | 26 | io.github.slvwolf 27 | ccentral-etcd 28 | 0.5.1 29 | compile 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /common/src/main/java/io/github/slvwolf/SchemaItem.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | class SchemaItem { 10 | public String key; 11 | public String title; 12 | public String description; 13 | @JsonProperty(value = "default") 14 | public String defaultValue; 15 | public String type; 16 | @JsonIgnore 17 | public String configValue; 18 | @JsonIgnore 19 | private final List callbacks; 20 | 21 | public enum Type { 22 | STRING("string"), 23 | PASSWORD("password"), 24 | INTEGER("integer"), 25 | FLOAT("float"), 26 | LIST("list"), 27 | BOOLEAN("boolean"); 28 | public final String value; 29 | 30 | Type(String type) { 31 | value = type; 32 | } 33 | } 34 | 35 | public SchemaItem(String key, String title, String description, String defaultValue, Type type) { 36 | this.key = key; 37 | this.title = title; 38 | this.description = description; 39 | this.defaultValue = defaultValue; 40 | this.type = type.value; 41 | configValue = null; 42 | callbacks = new LinkedList<>(); 43 | } 44 | 45 | public void addCallback(ConfigUpdate func) { 46 | callbacks.add(func); 47 | } 48 | 49 | public List getCallbacks() { 50 | return callbacks; 51 | } 52 | } -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccentral-parent 7 | io.github.slvwolf 8 | 0.5.1 9 | 10 | 4.0.0 11 | 12 | ccentral-common 13 | CCentral - Common 14 | 15 | jar 16 | 17 | 18 | 19 | 20 | com.fasterxml.jackson.core 21 | jackson-core 22 | 2.11.2 23 | 24 | 25 | 26 | 27 | com.fasterxml.jackson.core 28 | jackson-annotations 29 | 2.11.2 30 | 31 | 32 | 33 | 34 | org.slf4j 35 | slf4j-api 36 | 1.7.30 37 | 38 | 39 | 40 | 41 | com.fasterxml.jackson.core 42 | jackson-databind 43 | 2.11.2 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '39 12 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'java' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /etcd/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccentral-parent 7 | io.github.slvwolf 8 | 0.5.1 9 | 10 | 4.0.0 11 | 12 | ccentral-etcd 13 | CCentral - Etcd 14 | 15 | jar 16 | 17 | 18 | 19 | io.github.slvwolf 20 | ccentral-common 21 | 0.5.1 22 | compile 23 | 24 | 25 | 26 | 27 | org.mousio 28 | etcd4j 29 | 2.18.0 30 | 31 | 32 | 33 | 34 | io.netty 35 | netty-all 36 | 4.1.51.Final 37 | 38 | 39 | 40 | 41 | io.dropwizard.metrics 42 | metrics-core 43 | 4.1.11 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.slf4j 51 | slf4j-simple 52 | 1.7.30 53 | test 54 | 55 | 56 | 57 | junit 58 | junit 59 | 4.13.1 60 | test 61 | 62 | 63 | 64 | org.mockito 65 | mockito-all 66 | 1.10.19 67 | test 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /etcd/src/main/java/io/github/slvwolf/EtcdAccess.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | import mousio.etcd4j.EtcdClient; 4 | import mousio.etcd4j.responses.EtcdAuthenticationException; 5 | import mousio.etcd4j.responses.EtcdException; 6 | import mousio.etcd4j.responses.EtcdKeysResponse; 7 | 8 | import java.io.IOException; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.TimeoutException; 11 | 12 | 13 | /** 14 | * Simple wrapper for Etcd. 15 | */ 16 | public class EtcdAccess { 17 | 18 | private static final String LOCATION_SERVICE_BASE = "/ccentral/services/%s"; 19 | private static final String LOCATION_SCHEMA = LOCATION_SERVICE_BASE + "/schema"; 20 | private static final String LOCATION_CONFIG = LOCATION_SERVICE_BASE + "/config"; 21 | private static final String LOCATION_CLIENTS = LOCATION_SERVICE_BASE + "/clients/%s"; 22 | private static final String LOCATION_SERVICE_INFO = LOCATION_SERVICE_BASE + "/info/%s"; 23 | private static final int INSTANCE_TTL = 3 * 60; 24 | private static final int TTL_DAY = 26 * 60 * 60; 25 | private static final int TIMEOUT_SECONDS = 20; 26 | private final EtcdClient client; 27 | private final String serviceId; 28 | private String clientId; 29 | 30 | public EtcdAccess(EtcdClient client, String serviceId, String clientId) { 31 | this.client = client; 32 | this.serviceId = serviceId; 33 | this.clientId = clientId; 34 | } 35 | 36 | public void setClientId(String clientId) { 37 | this.clientId = clientId; 38 | } 39 | 40 | public EtcdClient getClient() { 41 | return client; 42 | } 43 | 44 | public void sendClientInfo(String json) throws IOException, EtcdAuthenticationException, TimeoutException, EtcdException { 45 | client.put(String.format(LOCATION_CLIENTS, serviceId, clientId), json) 46 | .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 47 | .ttl(INSTANCE_TTL) 48 | .send() 49 | .get(); 50 | } 51 | 52 | public String fetchConfig() throws IOException, EtcdAuthenticationException, TimeoutException, EtcdException { 53 | EtcdKeysResponse response = client.get(String.format(LOCATION_CONFIG, serviceId)) 54 | .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 55 | .send() 56 | .get(); 57 | return response.node.value; 58 | } 59 | 60 | public void sendSchema(String schemaJson) throws IOException, EtcdAuthenticationException, TimeoutException, EtcdException { 61 | client.put(String.format(LOCATION_SCHEMA, serviceId), schemaJson) 62 | .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 63 | .send() 64 | .get(); 65 | } 66 | 67 | public void sendServiceInfo(String key, String data) throws IOException, EtcdAuthenticationException, TimeoutException, EtcdException { 68 | client.put(String.format(LOCATION_SERVICE_INFO, serviceId, key), data) 69 | .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) 70 | .ttl(TTL_DAY) 71 | .send() 72 | .get(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /common/src/main/java/io/github/slvwolf/CCClient.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | import java.util.List; 4 | 5 | public interface CCClient { 6 | /** 7 | * Get unique clientId. 8 | * @return Unique Id. 9 | */ 10 | String getClientId(); 11 | 12 | /** 13 | * Add a string configuration field. 14 | * 15 | * @param key Unique ket for configuration. 16 | * @param title (UI) Human readable title. 17 | * @param description (UI) Documentation about the configuration. 18 | * @param defaultValue Default value. 19 | */ 20 | void addField(String key, String title, String description, String defaultValue); 21 | 22 | /** 23 | * Add a integer configuration field. 24 | * 25 | * @param key Unique ket for configuration. 26 | * @param title (UI) Human readable title. 27 | * @param description (UI) Documentation about the configuration. 28 | * @param defaultValue Default value. 29 | */ 30 | void addIntField(String key, String title, String description, int defaultValue); 31 | 32 | /** 33 | * Add a float configuration field. 34 | * 35 | * @param key Unique ket for configuration. 36 | * @param title (UI) Human readable title. 37 | * @param description (UI) Documentation about the configuration. 38 | * @param defaultValue Default value. 39 | */ 40 | 41 | void addFloatField(String key, String title, String description, float defaultValue); 42 | 43 | /** 44 | * Add a password configuration field. This field will have its value hidden in the UI. This does 45 | * not protect the password in any other way. 46 | * 47 | * @param key Unique ket for configuration. 48 | * @param title (UI) Human readable title. 49 | * @param description (UI) Documentation about the configuration. 50 | * @param defaultValue Default value. 51 | */ 52 | void addPasswordField(String key, String title, String description, String defaultValue); 53 | 54 | /** 55 | * Add a list configuration field. 56 | * 57 | * @param key Unique ket for configuration. 58 | * @param title (UI) Human readable title. 59 | * @param description (UI) Documentation about the configuration. 60 | * @param defaultValue Default value. 61 | */ 62 | void addListField(String key, String title, String description, List defaultValue); 63 | 64 | /** 65 | * Add a boolean configuration field. 66 | * 67 | * @param key Unique ket for configuration. 68 | * @param title (UI) Human readable title. 69 | * @param description (UI) Documentation about the configuration. 70 | * @param defaultValue Default value. 71 | */ 72 | void addBooleanField(String key, String title, String description, boolean defaultValue); 73 | 74 | /** 75 | * Get list value from configuration. 76 | * 77 | * @param key Key for configuration. 78 | * @return value or null if not found. 79 | */ 80 | List getConfigList(String key); 81 | 82 | /** 83 | * Get configuration. 84 | * 85 | * @param key Key for configuration. 86 | * @return value or null if not found. 87 | * @throws UnknownConfigException If configuration has not been defined on init. 88 | */ 89 | String getConfig(String key) throws UnknownConfigException; 90 | 91 | /** 92 | * Get boolean value from configuration. 93 | * 94 | * @param key Key for configuration. 95 | * @return value or null if not found. 96 | */ 97 | Boolean getConfigBool(String key); 98 | 99 | /** 100 | * Get int value from configuration. 101 | * 102 | * @param key Key for configuration. 103 | * @return value or null if not found. 104 | */ 105 | Integer getConfigInt(String key); 106 | 107 | /** 108 | * Get float value from configuration 109 | * 110 | * @param key Key for configuration 111 | * @return value or null if not found 112 | */ 113 | Float getConfigFloat(String key); 114 | 115 | /** 116 | * Get string value from configuration. 117 | * 118 | * @param key Key for configuration. 119 | * @return Value or null if not found. 120 | */ 121 | String getConfigString(String key); 122 | 123 | void addInstanceInfo(String key, String data); 124 | 125 | void addServiceInfo(String key, String data); 126 | 127 | void refresh(); 128 | 129 | /** 130 | * Increment instance counter 131 | * @param key Counter key 132 | * @param amount Amount to increment 133 | * @param groups Additional groups for this key 134 | */ 135 | void incrementInstanceCounter(String key, int amount, String ...groups); 136 | 137 | /** 138 | * Increment instance counter. Groups are optional and will create additional dimensions for that metric. 139 | * @param key Counter key 140 | * @param groups Additional groups for this key 141 | */ 142 | void incrementInstanceCounter(String key, String ...groups); 143 | 144 | /** 145 | * Reset instance counter to predefined value 146 | * 147 | * @param key Counter key 148 | * @param amount Value to set 149 | * @param groups Additional groups for this key 150 | */ 151 | void setInstanceCounter(String key, int amount, String... groups); 152 | 153 | void addHistogram(String key, long timeInMilliseconds); 154 | 155 | String getApiVersion(); 156 | 157 | /** 158 | * Use callback function when given configuration option changes. This can be handy option in cases where 159 | * configuration is usually set only once in init and needs new initialization when updated. 160 | * 161 | * @param configuration Key for configuration, has to be defined before called 162 | * @param func Called function 163 | * @throws UnknownConfigException Configuration item missing 164 | */ 165 | void addCallback(String configuration, ConfigUpdate func) throws UnknownConfigException; 166 | } 167 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | io.github.slvwolf 7 | ccentral-parent 8 | 9 | 0.5.2 10 | 11 | common 12 | etcd 13 | all 14 | 15 | 16 | pom 17 | 18 | CCentral 19 | Java client library for CCentral (Simple centralized configuration management and real-time monitoring 20 | for services). 21 | 22 | https://github.com/slvwolf/ccentral4j 23 | 24 | 25 | 26 | Santtu Järvi 27 | santtu.jarvi@finfur.net 28 | 29 | 30 | 31 | 32 | 33 | MIT License 34 | https://opensource.org/licenses/MIT 35 | repo 36 | 37 | 38 | 39 | 40 | https://github.com/slvwolf/ccentral4j 41 | scm:git:git@github.com:slvwolf/ccentral4j.git 42 | scm:git:git@github.com:slvwolf/ccentral4j.git 43 | 44 | 45 | 46 | 1.8 47 | UTF-8 48 | UTF-8 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.1 58 | 59 | 1.8 60 | 1.8 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-surefire-plugin 67 | 2.7 68 | 69 | false 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-checkstyle-plugin 76 | 2.17 77 | 78 | 79 | validate 80 | validate 81 | 82 | google_checks.xml 83 | UTF-8 84 | true 85 | true 86 | 87 | 88 | check 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | release 100 | 101 | 102 | ossrh 103 | https://oss.sonatype.org/content/repositories/snapshots 104 | 105 | 106 | ossrh 107 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 108 | 109 | 110 | 111 | 112 | 113 | org.sonatype.plugins 114 | nexus-staging-maven-plugin 115 | 1.6.7 116 | true 117 | 118 | ossrh 119 | https://oss.sonatype.org/ 120 | true 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-source-plugin 126 | 2.2.1 127 | 128 | 129 | attach-sources 130 | 131 | jar-no-fork 132 | 133 | 134 | 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-javadoc-plugin 139 | 2.9.1 140 | 141 | 142 | attach-javadocs 143 | 144 | jar 145 | 146 | 147 | 148 | 149 | 150 | org.apache.maven.plugins 151 | maven-gpg-plugin 152 | 1.5 153 | 154 | 155 | sign-artifacts 156 | verify 157 | 158 | sign 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /etcd/src/test/java/io/github/slvwolf/CCentralTest.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import mousio.etcd4j.responses.EtcdAuthenticationException; 6 | import mousio.etcd4j.responses.EtcdException; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.ArgumentCaptor; 10 | import org.mockito.Captor; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.mockito.MockitoAnnotations; 14 | import org.slf4j.Logger; 15 | 16 | import java.io.IOException; 17 | import java.time.Clock; 18 | import java.time.Duration; 19 | import java.util.Collections; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.TimeoutException; 23 | 24 | import static junit.framework.TestCase.assertTrue; 25 | import static org.hamcrest.CoreMatchers.hasItems; 26 | import static org.hamcrest.CoreMatchers.is; 27 | import static org.hamcrest.CoreMatchers.notNullValue; 28 | import static org.hamcrest.MatcherAssert.assertThat; 29 | import static org.mockito.Mockito.eq; 30 | import static org.mockito.Mockito.reset; 31 | import static org.mockito.Mockito.times; 32 | import static org.mockito.Mockito.verify; 33 | import static org.mockito.Mockito.verifyNoMoreInteractions; 34 | import static org.mockito.Mockito.when; 35 | 36 | public class CCentralTest { 37 | private static final ObjectMapper MAPPER = new ObjectMapper(); 38 | 39 | private CCEtcdClient cCentral; 40 | @Mock 41 | private EtcdAccess client; 42 | @Mock 43 | private Logger logger; 44 | @Captor 45 | private ArgumentCaptor stringCaptor; 46 | 47 | @Before 48 | public void setUp() { 49 | MockitoAnnotations.initMocks(this); 50 | cCentral = new CCEtcdClient(client); 51 | } 52 | 53 | /** 54 | * Schema is sent on first refresh 55 | */ 56 | @Test 57 | public void sendSchema() throws Exception { 58 | cCentral.refresh(); 59 | verify(client).sendSchema( 60 | "{\"v\":{\"key\":\"v\",\"title\":\"Version\",\"description\":\"Schema version for tracking instances\",\"type\":\"integer\",\"default\":\"0\"}}"); 61 | } 62 | 63 | /** List types, get defaults */ 64 | @Test 65 | public void getListDefault() { 66 | cCentral.addListField("list", "title", "description", Collections.singletonList("default")); 67 | 68 | List values = cCentral.getConfigList("list"); 69 | 70 | assertThat("Exactly one item in list", values.size(), is(1)); 71 | assertThat("Item should be 'default'", values.get(0), is("default")); 72 | } 73 | 74 | /** List types, get value */ 75 | @Test 76 | public void getListValue() throws Exception { 77 | when(client.fetchConfig()).thenReturn("{\"list\": {\"value\": \"[\\\"current\\\"]\"}}"); 78 | cCentral.addListField("list", "title", "description", Collections.singletonList("default")); 79 | 80 | List values = cCentral.getConfigList("list"); 81 | 82 | assertThat("Exactly one item in list", values.size(), is(1)); 83 | assertThat("Item should be 'current'", values.get(0), is("current")); 84 | } 85 | 86 | /** Bool types, get defaults */ 87 | @Test 88 | public void getBoolDefault() { 89 | cCentral.addBooleanField("bool", "title", "description", false); 90 | 91 | assertThat("Result should be false", cCentral.getConfigBool("bool"), is(false)); 92 | } 93 | 94 | /** 95 | * Bool types, get value 96 | */ 97 | @Test 98 | public void getBoolValue() throws Exception { 99 | when(client.fetchConfig()).thenReturn("{\"bool\": {\"value\": \"1\"}}"); 100 | cCentral.addBooleanField("bool", "title", "description", false); 101 | 102 | assertThat("Result should be true", cCentral.getConfigBool("bool"), is(true)); 103 | } 104 | 105 | /** 106 | * Provided callback function is called back on configuration change 107 | */ 108 | @Test 109 | public void noCallbackOnFirstRun() throws Exception { 110 | when(client.fetchConfig()).thenReturn("{\"bool\": {\"value\": \"1\"}}"); 111 | ConfigUpdate configUpdate = Mockito.mock(ConfigUpdate.class); 112 | cCentral.setConfigCheckInterval(-1); 113 | cCentral.addBooleanField("bool", "title", "description", false); 114 | cCentral.addCallback("bool", configUpdate); 115 | 116 | cCentral.refresh(); 117 | 118 | verifyNoMoreInteractions(configUpdate); 119 | } 120 | 121 | /** 122 | * Provided callback function is called back on configuration change 123 | */ 124 | @Test 125 | public void callback() throws Exception { 126 | when(client.fetchConfig()).thenReturn("{\"bool\": {\"value\": \"1\"}}"); 127 | ConfigUpdate configUpdate = Mockito.mock(ConfigUpdate.class); 128 | cCentral.setConfigCheckInterval(-1); 129 | cCentral.addBooleanField("bool", "title", "description", false); 130 | cCentral.addCallback("bool", configUpdate); 131 | 132 | cCentral.refresh(); 133 | reset(client); 134 | when(client.fetchConfig()).thenReturn("{\"bool\": {\"value\": \"0\"}}"); 135 | cCentral.refresh(); 136 | 137 | verify(configUpdate).valueChanged(eq("bool")); 138 | } 139 | 140 | /** 141 | * If configuration value has not changed, callback is not called 142 | */ 143 | @Test 144 | public void noCallback() throws Exception { 145 | when(client.fetchConfig()).thenReturn("{\"bool\": {\"value\": \"1\"}}"); 146 | ConfigUpdate configUpdate = Mockito.mock(ConfigUpdate.class); 147 | cCentral.setConfigCheckInterval(-1); 148 | cCentral.addBooleanField("bool", "title", "description", false); 149 | cCentral.addCallback("bool", configUpdate); 150 | 151 | cCentral.refresh(); 152 | when(client.fetchConfig()).thenReturn("{\"bool\": {\"value\": \"1\"}}"); 153 | cCentral.refresh(); 154 | 155 | verifyNoMoreInteractions(configUpdate); 156 | } 157 | 158 | /** 159 | * Throw exception on addCallback if configuration option is missing 160 | */ 161 | @Test(expected = UnknownConfigException.class) 162 | public void testMethod() throws Exception { 163 | cCentral.addCallback("bool", Mockito.mock(ConfigUpdate.class)); 164 | } 165 | 166 | /** 167 | * Password types, do not log password (verify correct branch) 168 | */ 169 | @Test 170 | public void getPasswordValue() throws Exception { 171 | when(client.fetchConfig()).thenReturn("{\"password_title\": {\"value\": \"pass2\"}}"); 172 | CCEtcdClient.setLogger(logger); 173 | cCentral.addPasswordField("password_title", "title", "description", "pass1"); 174 | 175 | cCentral.refresh(); 176 | 177 | verify(logger).info(eq("Configuration value for '{}' changed."), eq("password_title")); 178 | } 179 | 180 | /** 181 | * Pull configuration on late field definitions 182 | */ 183 | @Test 184 | public void pullConfigLate() throws EtcdAuthenticationException, TimeoutException, EtcdException, IOException { 185 | cCentral.addField("key", "title", "desc", "def"); 186 | cCentral.refresh(); 187 | reset(client); 188 | 189 | cCentral.addField("key2", "title", "desc", "def"); 190 | verify(client).fetchConfig(); 191 | } 192 | 193 | /** Increment with groups */ 194 | @Test 195 | public void incGroups() throws Exception { 196 | cCentral.incrementInstanceCounter("key", "group1", "group2"); 197 | cCentral.refresh(); 198 | 199 | verify(client).sendClientInfo(stringCaptor.capture()); 200 | assertTrue(stringCaptor.getValue().contains("\"c_key.group1.group2\":[0]")); 201 | } 202 | 203 | /** Group parameters are cleaned */ 204 | @Test 205 | public void cleanGroups() throws Exception { 206 | cCentral.incrementInstanceCounter("key", "invalid.character", "second character"); 207 | cCentral.refresh(); 208 | 209 | verify(client).sendClientInfo(stringCaptor.capture()); 210 | assertTrue(stringCaptor.getValue().contains("\"c_key.invalidcharacter.second_character\":[0]")); 211 | } 212 | 213 | /** Increment without groups */ 214 | @Test 215 | public void incNoGroups() throws Exception { 216 | cCentral.incrementInstanceCounter("key"); 217 | cCentral.refresh(); 218 | 219 | verify(client).sendClientInfo(stringCaptor.capture()); 220 | assertTrue(stringCaptor.getValue().contains("\"c_key\":[0]")); 221 | } 222 | 223 | @Test 224 | public void histogram() throws EtcdAuthenticationException, TimeoutException, EtcdException, IOException { 225 | cCentral.addHistogram("latency", 10); 226 | cCentral.addHistogram("latency", 12); 227 | cCentral.addHistogram("latency", 7); 228 | cCentral.setClock(Clock.offset(cCentral.getClock(), Duration.ofMinutes(1))); 229 | cCentral.refresh(); 230 | ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); 231 | verify(client, times(2)).sendClientInfo(captor.capture()); 232 | String data = captor.getAllValues().get(1); 233 | Map values = MAPPER.readValue(data, new TypeReference>() { 234 | }); 235 | @SuppressWarnings("unchecked") 236 | List latencies = (List) values.get("h_latency"); 237 | assertThat(latencies, notNullValue()); 238 | assertThat(latencies, hasItems(12.0, 12.0, 12.0, 10.0)); 239 | } 240 | } -------------------------------------------------------------------------------- /etcd/src/main/java/io/github/slvwolf/CCEtcdClient.java: -------------------------------------------------------------------------------- 1 | package io.github.slvwolf; 2 | 3 | import com.codahale.metrics.ExponentiallyDecayingReservoir; 4 | import com.codahale.metrics.Histogram; 5 | import com.codahale.metrics.Snapshot; 6 | import com.fasterxml.jackson.core.JsonParseException; 7 | import com.fasterxml.jackson.core.JsonProcessingException; 8 | import com.fasterxml.jackson.core.type.TypeReference; 9 | import com.fasterxml.jackson.databind.ObjectMapper; 10 | import mousio.etcd4j.EtcdClient; 11 | import mousio.etcd4j.transport.EtcdNettyClient; 12 | import mousio.etcd4j.transport.EtcdNettyConfig; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.IOException; 17 | import java.net.URI; 18 | import java.time.Clock; 19 | import java.util.HashMap; 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | import java.util.Map; 23 | import java.util.UUID; 24 | 25 | public class CCEtcdClient implements CCClient { 26 | 27 | private static final String CLIENT_VERSION = "java_etcd-0.5.2"; 28 | private static final int METRIC_INTERVAL = 40; 29 | private int configCheckInterval = 40; 30 | private static final ObjectMapper MAPPER = new ObjectMapper(); 31 | private static final String API_VERSION = "1"; 32 | private static Logger LOG = LoggerFactory.getLogger(CCEtcdClient.class); 33 | private final EtcdAccess client; 34 | private Clock clock; 35 | private int startedEpoch; 36 | private HashMap schema; 37 | private HashMap clientData; 38 | private HashMap counters; 39 | private HashMap histograms; 40 | private String clientId; 41 | private long lastConfigCheck; 42 | private long lastMetricUpload; 43 | private static int ETCDmaxFrameSize = 1024 * 200; 44 | 45 | public CCEtcdClient(EtcdAccess client) { 46 | try { 47 | init(); 48 | this.client = client; 49 | client.setClientId(this.getClientId()); 50 | } catch (Throwable e) { 51 | LOG.error("Could not initialise using provided EtcdClient", e); 52 | throw e; 53 | } 54 | } 55 | 56 | public CCEtcdClient(String serviceId, URI[] hosts) { 57 | // TODO: Instead of throwing exception library should work in a dummy mode instead. 58 | if (hosts == null || hosts.length == 0) { 59 | LOG.error("No hosts provided or hosts is null. Can not initialize CCentral."); 60 | throw new RuntimeException("No hosts provided or hosts is null. Can not initialize CCentral."); 61 | } 62 | for (URI host : hosts) { 63 | LOG.info("Creating ETCD connection: {}", host.toASCIIString()); 64 | } 65 | try { 66 | EtcdNettyConfig config = new EtcdNettyConfig() 67 | .setMaxFrameSize(ETCDmaxFrameSize); 68 | EtcdClient cli = new EtcdClient(new EtcdNettyClient(config, hosts)); 69 | init(); 70 | this.client = new EtcdAccess(cli, serviceId, this.getClientId()); 71 | } catch (Throwable e) { 72 | LOG.error("Could not initialise EtcdClient", e); 73 | throw e; 74 | } 75 | } 76 | 77 | public static void setLogger(Logger logger) { 78 | CCEtcdClient.LOG = logger; 79 | } 80 | 81 | public Clock getClock() { 82 | return clock; 83 | } 84 | 85 | public void setClock(Clock clock) { 86 | this.clock = clock; 87 | } 88 | 89 | @Override 90 | public String getApiVersion() { 91 | return API_VERSION; 92 | } 93 | 94 | @Override 95 | public void addCallback(String configuration, ConfigUpdate func) throws UnknownConfigException { 96 | SchemaItem item = schema.get(configuration); 97 | if (item == null) { 98 | throw new UnknownConfigException("Configuration option for '" + configuration + "' is missing"); 99 | } 100 | item.addCallback(func); 101 | } 102 | 103 | @Override 104 | public String getClientId() { 105 | return clientId; 106 | } 107 | 108 | private String filterKey(String key) { 109 | key = key.replace(" ", "_"); 110 | String validKeyChars = "[^a-zA-Z0-9_-]"; 111 | return key.replaceAll(validKeyChars, ""); 112 | } 113 | 114 | private void init() { 115 | LOG.info("Initializing"); 116 | clock = Clock.systemUTC(); 117 | clientId = UUID.randomUUID().toString(); 118 | this.startedEpoch = (int) (clock.millis() / 1000); 119 | schema = new HashMap<>(); 120 | counters = new HashMap<>(); 121 | histograms = new HashMap<>(); 122 | clientData = new HashMap<>(); 123 | addIntField("v", "Version", "Schema version for tracking instances", 0); 124 | lastConfigCheck = 0; 125 | lastMetricUpload = 0; 126 | } 127 | 128 | @Override 129 | public void addField(String key, String title, String description, String defaultValue) { 130 | addFieldType(key, title, description, defaultValue, SchemaItem.Type.STRING); 131 | } 132 | 133 | @Override 134 | public void addIntField(String key, String title, String description, int defaultValue) { 135 | addFieldType(key, title, description, Integer.toString(defaultValue), SchemaItem.Type.INTEGER); 136 | } 137 | 138 | @Override 139 | public void addFloatField(String key, String title, String description, float defaultValue) { 140 | addFieldType(key, title, description, Float.toString(defaultValue), SchemaItem.Type.FLOAT); 141 | } 142 | 143 | @Override 144 | public void addPasswordField(String key, String title, String description, String defaultValue) { 145 | addFieldType(key, title, description, defaultValue, SchemaItem.Type.PASSWORD); 146 | } 147 | 148 | @Override 149 | public void addListField(String key, String title, String description, List defaultValue) { 150 | try { 151 | addFieldType(key, title, description, MAPPER.writeValueAsString(defaultValue), SchemaItem.Type.LIST); 152 | } catch (JsonProcessingException e) { 153 | LOG.error("Could not register list type for key {}: ", key, e); 154 | } 155 | } 156 | 157 | @Override 158 | public void addBooleanField(String key, String title, String description, boolean defaultValue) { 159 | String value; 160 | if (defaultValue) { 161 | value = "1"; 162 | } else { 163 | value = "0"; 164 | } 165 | addFieldType(key, title, description, value, SchemaItem.Type.BOOLEAN); 166 | } 167 | 168 | @Override 169 | public List getConfigList(String key) { 170 | try { 171 | String value = getConfigString(key); 172 | if (value == null) { 173 | return null; 174 | } 175 | return MAPPER.readValue(value, new TypeReference>() { 176 | }); 177 | } catch (JsonParseException e) { 178 | LOG.warn("Could not parse value of configuration key '{}'. Value needs to be a valid json list of strings.", key); 179 | } catch (IOException e) { 180 | LOG.warn("Could not parse value of configuration key '{}'.", key, e); 181 | } 182 | return null; 183 | } 184 | 185 | private void addFieldType(String key, String title, String description, String defaultValue, SchemaItem.Type type) { 186 | key = filterKey(key); 187 | schema.put(key, new SchemaItem(key, title, description, defaultValue, type)); 188 | if (lastConfigCheck > 0) { 189 | LOG.warn("Schema was updated after refresh. This might result in some abnormal behavior on " 190 | + "administration UI and degrades the performance. Before setting any stats or instance " 191 | + "variables always make sure all configurations have been already defined. As a remedy " 192 | + "will now resend the updated schema."); 193 | sendSchema(); 194 | pullConfigData(); 195 | } 196 | } 197 | 198 | @Override 199 | public String getConfig(String key) throws UnknownConfigException { 200 | refresh(); 201 | key = filterKey(key); 202 | SchemaItem item = schema.get(key); 203 | if (item == null) { 204 | throw new UnknownConfigException(key); 205 | } 206 | if (item.configValue == null) { 207 | return item.defaultValue; 208 | } 209 | return item.configValue; 210 | } 211 | 212 | @Override 213 | public Boolean getConfigBool(String key) { 214 | Integer value = getConfigInt(key); 215 | if (value == null) { 216 | return null; 217 | } 218 | return value == 1; 219 | } 220 | 221 | @Override 222 | public Integer getConfigInt(String key) { 223 | try { 224 | return Integer.valueOf(getConfigString(key)); 225 | } catch (NumberFormatException e) { 226 | LOG.warn("Could not convert configuration {} value '{}' to int.", 227 | key, getConfigString(key)); 228 | return null; 229 | } 230 | } 231 | 232 | @Override 233 | public Float getConfigFloat(String key) { 234 | try { 235 | return Float.valueOf(getConfigString(key)); 236 | } catch (NumberFormatException e) { 237 | LOG.warn("Could not convert configuration {} value '{}' to float.", 238 | key, getConfigString(key)); 239 | return null; 240 | } 241 | } 242 | 243 | @Override 244 | public String getConfigString(String key) { 245 | try { 246 | return getConfig(key); 247 | } catch (UnknownConfigException e) { 248 | LOG.warn("Configuration {} was requested before initialized. Always introduce all " + 249 | "configurations with addField method before using them.", key); 250 | return null; 251 | } 252 | } 253 | 254 | @Override 255 | public void addInstanceInfo(String key, String data) { 256 | refresh(); 257 | key = filterKey(key); 258 | clientData.put("k_" + key, data); 259 | } 260 | 261 | @Override 262 | public void addServiceInfo(String key, String data) { 263 | refresh(); 264 | key = filterKey(key); 265 | try { 266 | client.sendServiceInfo(key, data); 267 | } catch (Exception e) { 268 | LOG.error("Failed to add service info: " + e.getMessage(), e); 269 | } 270 | } 271 | 272 | @Override 273 | public void refresh() { 274 | if (lastConfigCheck == 0) { 275 | LOG.info("First refresh, sending Schema"); 276 | sendSchema(); 277 | LOG.debug("Schema updated"); 278 | } 279 | if (lastConfigCheck < (clock.millis() - configCheckInterval * 1000)) { 280 | LOG.debug("Checking for new configuration"); 281 | lastConfigCheck = clock.millis(); 282 | pullConfigData(); 283 | } 284 | if (lastMetricUpload < (clock.millis() - METRIC_INTERVAL * 1000)) { 285 | LOG.debug("Uploading metrics"); 286 | lastMetricUpload = clock.millis(); 287 | sendClientData(); 288 | } 289 | } 290 | 291 | private Counter getCounter(String key, String... groups) { 292 | if (groups.length > 0) { 293 | StringBuilder b = new StringBuilder(filterKey(key)); 294 | for (String group : groups) { 295 | b.append("."); 296 | b.append(filterKey(group)); 297 | } 298 | key = b.toString(); 299 | } else { 300 | key = filterKey(key); 301 | } 302 | Counter counter = counters.get(key); 303 | if (counter == null) { 304 | counter = new Counter(); 305 | counters.put(key, counter); 306 | } 307 | return counter; 308 | } 309 | 310 | @Override 311 | public void incrementInstanceCounter(String key, int amount, String... groups) { 312 | getCounter(key, groups).increment(amount); 313 | refresh(); 314 | } 315 | 316 | @Override 317 | public void incrementInstanceCounter(String key, String... groups) { 318 | getCounter(key, groups).increment(1); 319 | refresh(); 320 | } 321 | 322 | @Override 323 | public void setInstanceCounter(String key, int amount, String... groups) { 324 | getCounter(key, groups).set(amount); 325 | refresh(); 326 | } 327 | 328 | @Override 329 | public void addHistogram(String key, long timeInMilliseconds) { 330 | refresh(); 331 | Histogram histogram = histograms.get(key); 332 | if (histogram == null) { 333 | histogram = new Histogram(new ExponentiallyDecayingReservoir()); 334 | histograms.put(key, histogram); 335 | } 336 | histogram.update(timeInMilliseconds); 337 | } 338 | 339 | 340 | private void sendSchema() { 341 | try { 342 | LOG.info("Sending schema information"); 343 | String schemaJson = MAPPER.writeValueAsString(schema); 344 | client.sendSchema(schemaJson); 345 | } catch (Exception e) { 346 | LOG.error("Failed to send schema: " + e.getMessage(), e); 347 | } 348 | } 349 | 350 | private void pullConfigData() { 351 | try { 352 | LOG.info("Checking configuration changes"); 353 | List callbacks = new LinkedList<>(); 354 | String data = client.fetchConfig(); 355 | Map configMap = MAPPER.readValue(data, new TypeReference>() { 356 | }); 357 | for (Map.Entry entry : configMap.entrySet()) { 358 | SchemaItem schemaItem = schema.get(entry.getKey()); 359 | if (schemaItem == null) { 360 | continue; 361 | } 362 | @SuppressWarnings("unchecked") 363 | String newValue = ((HashMap) (entry.getValue())).get("value").toString(); 364 | // Value changed 365 | if (schemaItem.configValue == null || !schemaItem.configValue.equals(newValue)) { 366 | boolean isFirstUpdate = schemaItem.configValue == null; 367 | String oldValue = schemaItem.configValue == null ? schemaItem.defaultValue : schemaItem.configValue; 368 | schemaItem.configValue = newValue; 369 | if (schemaItem.type.equalsIgnoreCase(SchemaItem.Type.PASSWORD.value)) { 370 | LOG.info("Configuration value for '{}' changed.", schemaItem.key); 371 | } else { 372 | LOG.info("Configuration value for {} changed ({} => {})", schemaItem.key, oldValue, newValue); 373 | } 374 | if (!isFirstUpdate) { 375 | for (ConfigUpdate callback : schemaItem.getCallbacks()) { 376 | callbacks.add(() -> callback.valueChanged(schemaItem.key)); 377 | } 378 | } 379 | } 380 | } 381 | LOG.debug("Configuration pulled successfully"); 382 | for (Runnable callback : callbacks) { 383 | try { 384 | callback.run(); 385 | } catch (Exception exception) { 386 | LOG.warn("Configuration update threw unexpected exception", exception); 387 | } 388 | } 389 | } catch (Exception e) { 390 | LOG.error("Failed to pull configuration data: " + e.getMessage(), e); 391 | } 392 | } 393 | 394 | private void sendClientData() { 395 | LOG.info("Sending client data"); 396 | clientData.put("ts", Integer.toString((int) (clock.millis() / 1000))); 397 | String configVersion = getConfigString("v"); 398 | clientData.put("v", configVersion == null ? "unknown" : configVersion); 399 | clientData.put("cv", CLIENT_VERSION); 400 | clientData.put("av", API_VERSION); 401 | clientData.put("hostname", System.getenv("HOSTNAME")); 402 | clientData.put("lv", System.getProperty("java.version")); 403 | clientData.put("started", startedEpoch); 404 | clientData.put("uinterval", "60"); 405 | 406 | for (Map.Entry entry : counters.entrySet()) { 407 | LinkedList counts = new LinkedList<>(); 408 | counts.add(entry.getValue().getValue()); 409 | clientData.put("c_" + entry.getKey(), counts); 410 | } 411 | 412 | for (Map.Entry entry : histograms.entrySet()) { 413 | Snapshot snapshot = entry.getValue().getSnapshot(); 414 | LinkedList percentiles = new LinkedList<>(); 415 | percentiles.add(snapshot.get75thPercentile()); 416 | percentiles.add(snapshot.get95thPercentile()); 417 | percentiles.add(snapshot.get99thPercentile()); 418 | percentiles.add(snapshot.getMedian()); 419 | clientData.put("h_" + entry.getKey(), percentiles); 420 | } 421 | 422 | try { 423 | String json = MAPPER.writeValueAsString(clientData); 424 | client.sendClientInfo(json); 425 | } catch (Exception e) { 426 | LOG.error("Failed to send client data: " + e.getMessage(), e); 427 | } 428 | } 429 | 430 | /** 431 | * Configuration pull interval, setting this to 0 will cause configuration to be fetched on every get call 432 | * 433 | * @param configCheckInterval Check interval in seconds 434 | */ 435 | public void setConfigCheckInterval(int configCheckInterval) { 436 | this.configCheckInterval = configCheckInterval; 437 | } 438 | } 439 | --------------------------------------------------------------------------------